102 Commits

Author SHA1 Message Date
aee3467918 Refactor dossard PDF generation to standardize layout for 1 participant. Update page size to 8.5x5.5" and adjust positioning of text and QR code. Enhance template resolution logic and improve comments for clarity. Increment version code. 2026-07-24 20:09:56 -04:00
087ef47e15 Refactor dossard layout and background placement for PDF generation. Update dimensions to fit 5.5x8.5" bibs on Letter-sized pages, ensuring proper centering and scaling of background images. Adjust QR code and text positioning to align with new layout parameters. Increment version code. 2026-07-24 19:36:09 -04:00
aabb419e7d Add default and custom dossard templates in JPG format. Update image path resolution to include PNG options and enhance image type detection for PDF generation. Increment version code. 2026-07-24 19:33:05 -04:00
3a7c1f4bb7 Replace default PNG templates with JPG versions and add support for image type detection in PDF generation. Update template resolution logic to include new image formats. Increment version code. 2026-07-24 19:29:49 -04:00
36eb81d8e8 Enhance permission sorting in fxEveAccesShowCompteForm to prioritize 'hors kit total' entries and improve display of action permissions. Adjust select element width for better UI presentation. Increment version code. 2026-07-24 19:27:12 -04:00
d4ab2e2dc7 Refactor permission checks in fxEveAccesHasPermission and fxEveAccesHasAnyPermission functions to enhance logic for 'perm_grants_all' propagation. Update comments for clarity regarding key propagation rules. Increment version code. 2026-07-24 19:23:34 -04:00
8cd023e224 Add functionality to print bibs in PDF format for event management. Introduce new translation keys and implement print button logic in the event details view. Increment version code. 2026-07-24 19:17:30 -04:00
8788f22a81 Add functions to handle 'perm_grants_all' column and update permission checks for event kits. Refactor related logic to respect new permission propagation rules. Increment version code. 2026-07-24 19:10:19 -04:00
fd7930b139 Update SQL for 'Sans inscription en ligne' event type to include icon and ensure correct updates. Add logic to set icon if missing and adjust event attachment query for flexibility. Increment version code. 2026-07-24 17:57:08 -04:00
a68f54be80 Refactor fxImportResultatsSaveQuestionAnswer to ensure que_actif is set correctly during question updates and insertions. Simplify SQL logic for handling pec_id and improve error handling for database queries in fxInscrGestionFetchFicheProfile. Increment version code. 2026-07-24 17:53:03 -04:00
038bd7bd26 Implement fxImportResultatsEnsureQuestionsVisibleFiche function to activate questions during import for MSIN-4482. Update SQL queries to manage visibility of questions based on event type and ensure correct filtering of pec_id. Adjust logic in fxInscrGestionFetchFicheProfile to accommodate non-online registration scenarios. Increment version code. 2026-07-24 17:49:14 -04:00
69a97c0044 Enhance import functionality for events without online registration in MSIN-4482. Introduce new SQL queries and functions to manage event types and guard against invalid imports. Update AJAX handling to enforce type restrictions and improve user feedback in the event panel. Increment version code. 2026-07-24 17:45:47 -04:00
26a470fe1e Refactor fxImportResultatsCommit to improve handling of pec_id and pec_id_original during imports for MSIN-4482. Update SQL queries to ensure pec_actif is set correctly and prevent collisions with unique constraints. Enhance logic for updating participant records based on active status and original IDs. Increment version code. 2026-07-24 17:31:48 -04:00
80c791c7e1 Enhance fxImportResultatsCommit and fxImportResultatsReset functions for MSIN-4482 by updating SQL queries to ensure correct handling of pec_id_original and pec_actif flags during imports. Refactor participant deletion logic to improve performance using subqueries. Add diagnostic SQL for verifying import integrity. Increment version code. 2026-07-24 17:20:19 -04:00
05ce600e09 Refactor fxImportResultatsResetPreview to improve SQL query readability and performance by storing the escaped origin in a variable. Add diagnostic SQL query to count visible participants in the list based on active PEC status. Enhance the event panel UI to display database status with refresh functionality. Increment version code. 2026-07-24 17:06:46 -04:00
c6477f7757 Enhance result import functionality for MSIN-4482 by adding reset and preview capabilities. Implement fxImportResultatsResetPreview to count potential deletions and fxImportResultatsReset to remove imported data based on confirmation. Update AJAX handling in ajax_import_resultats.php to support these new actions. Ensure pec_actif is set to 1 for active entries in the import process. Increment version code. 2026-07-24 17:02:55 -04:00
8498af24f9 Implement fxImportResultatsGroupBlockedByType function to categorize and handle blocked import rows in fxImportResultats for MSIN-4482. Enhance error reporting by adding detailed messages for header rows, missing unique identifiers, unmapped races, and database update failures. Update the commit function to utilize the new error handling structure. Increment version code. 2026-07-24 16:26:45 -04:00
171a48a411 Add temporary unique ID generation for pec_id_original and par_id_original in fxImportResultats for MSIN-4482. Implement fxImportResultatsTempOriginalId function to ensure non-zero unique values during insertion. Update SQL queries to utilize generated IDs. Increment version code. 2026-07-24 16:22:28 -04:00
d63c92e36a Implement header row filtering and unique identifier generation in fxImportResultats for MSIN-4482. Add functions to detect header-like rows and create unique panier/commande identifiers. Enhance error handling during database operations. Increment version code. 2026-07-24 16:19:54 -04:00
8929236e85 Enhance participant import functionality in MSIN-4482 by updating the import phases and adding new fields for emergency contact details. Implement commit action in AJAX handling for result mapping validation. Increment version code. 2026-07-24 16:16:00 -04:00
84e65340b6 Enhance fxImportResultatsRenderEventPanel and ajax_import_resultats.php for improved AJAX handling in MSIN-4482. Update AJAX URL construction to include domain, add error handling for JSON parsing, and ensure panel visibility. Implement output buffering to prevent HTML pollution in JSON responses. Increment version code. 2026-07-24 16:09:37 -04:00
a1b6fa8911 Refactor fxImportResultatsComIdForEve to use org_id instead of com_id for event sharing templates in MSIN-4482. Update SQL queries and comments for clarity. Ensure compatibility with legacy data structures. Increment version code. 2026-07-24 15:44:25 -04:00
1ca75a4ed4 Implement UI enhancements for participant import without transaction in MSIN-4482. Add new styles for import dropzone and integrate import functionality into event management panel. Update navigation for improved access to import features. Increment version code. 2026-07-24 15:39:50 -04:00
db7b2e9f46 Add new project rules for MSIN-4482 — Implement import and quick entry for ChronoTrack without online registration. Document decisions, phases, and scope for development clarity. Increment version code. 2026-07-24 15:27:50 -04:00
b67fdaee78 Remove obsolete inbox telephone rules file from project directory, streamlining the codebase and eliminating unnecessary documentation. Increment version code. 2026-07-24 15:27:25 -04:00
fd250996b2 Update .gitignore to include new entry for local inbox directory used for briefs, ensuring that non-code files are excluded from version control. Increment version code. 2026-07-24 13:33:03 -04:00
797223e976 MSIN-4479 — Update alert messages in bib-assignments.js and inc_fx_promoteur.php to use new dataset keys for error handling. Ensure count function handles null results correctly to prevent JSON pollution. Increment version code to 4.72.869. 2026-07-24 13:05:56 -04:00
5961b40349 Refactor login.php to streamline session handling by moving the require statements for inc_functions.php and inc_fx_messages.php above the session check. This change improves code organization and clarity. Increment version code. 2026-07-23 16:02:51 -04:00
447dc5a7dc MSIN-4478 — Update role delegation logic to prevent total kits from being delegable. Adjust comments and UI messaging to clarify restrictions on promoter invitations. Increment version code. 2026-07-23 15:47:11 -04:00
5406e7e3db MSIN-4478 — Add functionality for managing pending invites and account activation for promoteur access. Implement methods to refresh reset tokens, revoke other roles, and deactivate orphan invite stubs. Update invitation handling to support reactivation of previously deactivated accounts. Increment version code. 2026-07-23 15:22:57 -04:00
685a1fd4f3 MSIN-4469 — Enhance fxBibProductionXmlSetFormula function to support cached values for Excel output, preventing empty totals in Excel/LibreOffice until recalculation occurs. Update fxBibProductionBuildOrderXml to utilize this new functionality for improved total calculations. Increment version code. 2026-07-23 12:55:42 -04:00
f93f6d14ff MSIN-4469 — Refactor fxBibProductionBuildOrderXml function to improve handling of Excel row counts and remove unnecessary empty rows. Update logic for shifting rows and add validation for printer lines in the output. Increment version code. 2026-07-23 12:50:47 -04:00
d15a68285c MSIN-4469 — Update fxBibProductionBuildSupplierXml function to clarify header cell styles for printed bibs. Modify Excel template to reflect new styling conventions for improved clarity in output. Increment version code. 2026-07-23 12:36:15 -04:00
f3233eb19b Implement fxNormalizeCmsPageLinks function to correct internal CMS links in HTML content. Update index.php to utilize this new function for processing page text, enhancing link handling for various content types. Increment version code. 2026-07-23 11:47:36 -04:00
a763a7b230 Update version code to 4.72.868 and date code to 2026-07-23 for production release. Adjust comments to reflect changes related to cache-busting for CSS/JS loader. 2026-07-23 11:18:45 -04:00
f76fb95fcd CON-333 — Refine static database synchronization logic to ensure that the display of differing content is contingent on the presence of actual differences. Update conditional checks to enhance clarity in the output when differences exist. Increment version code. 2026-07-23 10:09:23 -04:00
5b6faf93f9 CON-333 — Add functionality to remove excess "poubelle" entries from the info table during synchronization. Introduce a new action for users to trigger this process, ensuring that only unnecessary entries are deleted while preserving valid labels. Update user interface messaging to clarify the operation and its implications. Increment version code. 2026-07-23 09:28:14 -04:00
88a8010a18 CON-333 — Enhance static synchronization process by adding functionality to automatically remove excess entries during synchronization. Update user messaging to clarify that excess entries will be removed when possible, while ensuring no deletions occur. Revise documentation to reflect these changes in the workflow. Increment version code. 2026-07-23 09:24:27 -04:00
5ceeec7fdd CON-333 — Update documentation and workflow for database promotion processes, emphasizing the restriction of SQL execution to dev preprod environments only. Enhance clarity on deployment steps and introduce guidelines for removing excess roles and permissions during synchronization. Increment version code. 2026-07-23 09:16:19 -04:00
9697b37263 CON-333 — Update role permissions synchronization to use role_code instead of role_id for improved portability across databases. Introduce new functions for handling role permissions and enhance existing synchronization logic to support non-destructive updates. Update user interface to include a new "apply_all" action for comprehensive synchronization without deletions. Increment version code. 2026-07-23 09:00:04 -04:00
18b667d24b CON-333 — Implement soft synchronization for static tables, allowing for insertion of missing rows and updates to existing records without deletion. Introduce new functions for handling insert columns and info row checks. Update dashboard to reflect non-destructive actions and improve user messaging for clarity. Increment version code. 2026-07-23 08:53:44 -04:00
c154196090 MSIN-4328 — Increase AJAX timeout for preview synchronization in the ChronoTrack admin interface to improve user experience during long operations. Enhance error messaging for better clarity on network issues and session handling. Update participant classification function to allow optional payload building. Increment version code. 2026-07-22 21:57:21 -04:00
1306770f2e MSIN-4328 — Introduce verbose logging option in the ChronoTrack API for detailed entry logs. Update push panel UI to include a checkbox for verbose logs and adjust related functions to conditionally log detailed information based on user selection. Increment version code. 2026-07-22 21:49:57 -04:00
ab063aeb23 MSIN-4328 — Simplify the push panel in the ChronoTrack admin interface by removing the detail count display and updating related messaging for clarity. Adjust comments to reflect changes in the exclusivity section. Increment version code. 2026-07-22 21:45:22 -04:00
36631dc201 MSIN-4328 — Remove automatic loading of sync logs in fxChronotrackApiAdminRenderPageScript to streamline user experience. Update comments to clarify log access method, ensuring better understanding of log visibility options. Increment version code. 2026-07-22 21:44:07 -04:00
4851a50279 MSIN-4328 — Enhance ChronoTrack admin panel UI by updating the push panel to display exclusive and detail counts. Improve loading status visuals with a new layout and messaging for better user experience. Adjust CSS for improved responsiveness and clarity. Increment version code. 2026-07-22 21:41:12 -04:00
03cb227b5d MSIN-4328 — Update ChronoTrack admin panel to enhance event form rendering with a focus option for full-screen operation. Modify UI elements for clarity and improve navigation links. Adjust layout for better responsiveness and user experience. Increment version code. 2026-07-22 21:35:36 -04:00
1612d91ac6 MSIN-4328 — Add new functions for rendering event forms and auto-sync features in the ChronoTrack admin panel. Enhance user interface with compact summaries and configuration options for automatic synchronization. Update existing messaging for clarity and improve overall functionality. Increment version code. 2026-07-22 21:28:52 -04:00
e27e2b5bc0 MSIN-4461 — Implement new functions for handling entry notes in ChronoTrack, including indexing questions and mapping responses for participants. Update SQL queries to enhance data retrieval for active questions. Ensure synchronization logic accommodates custom notes effectively. Increment version code. 2026-07-22 18:02:20 -04:00
b18407f64b MSIN-4461 — Update API functions to support entry_note_test for custom notes in ChronoTrack. Modify button titles and messaging in the admin panel for clarity. Enhance synchronization logic to extract entry_note values from entries, ensuring accurate handling of custom notes. Increment version code. 2026-07-22 17:56:38 -04:00
46e34d4dcc MSIN-4328 — Introduce new function for handling payloads in POST API without entry_id, enhancing synchronization logic for bib conflicts. Update existing functions to improve clarity on bib handling and error management during entry processing. Increment version code. 2026-07-22 16:13:19 -04:00
839d865d7c MSIN-4328 — Refine messaging in fxChronotrackApiAdminRenderPushPanel for clarity on sending options. Enhance error handling in fxChronotrackApiSyncPushEvent to manage exceptions gracefully. Update ajax_chronotrack_api.php to ensure proper JSON response formatting. Increment version code. 2026-07-22 15:57:13 -04:00
10e6a09640 MSIN-4328 — Enhance fxChronotrackApiSync with new functions for handling bib conflicts and payload adjustments. Update fxChronotrackApiAdminRenderPageScript to clarify log messaging regarding bib status. Improve synchronization logic for participant entries, ensuring accurate handling of bib assignments and conflicts. Increment version code. 2026-07-22 15:42:15 -04:00
e747b180aa MSIN-4328 — Update messaging in fxChronotrackApiAdminRenderPageScript for clarity on bib status and conditions for sending bibs. Modify logic in fxChronotrackApiSync to ensure bibs are sent immediately upon confirmation status, improving synchronization accuracy. Increment version code. 2026-07-22 15:25:52 -04:00
57e281b46f MSIN-4328 — Enhance logging clarity in fxChronotrackApiSync by detailing participant entry information, including bib status and names. Update the admin page script to provide clearer messaging on log entries for better user understanding. Increment version code. 2026-07-22 15:25:09 -04:00
847f2f57a7 MSIN-4328 — Introduce a new button for full resync in the ChronoTrack admin panel, allowing all eligible participants to be resent regardless of last push date. Update JavaScript to handle the new button's functionality and enhance logging for successful entries. Adjust push preparation logic to accommodate full resync options. Increment version code. 2026-07-22 15:19:51 -04:00
23def197c8 MSIN-4328 — Update SQL queries in ajax_bib_range.php and inc_fx_promoteur.php to set par_maj alongside no_bib and par_date_bib, ensuring accurate timestamping for participant updates. Enhance fxChronotrackApiSync to include par_date_bib in filtering logic. Increment version code. 2026-07-22 15:15:38 -04:00
b9e4395297 MSIN-4444 — Add functions to handle team name retrieval and team group key generation, improving team management in the API. Update logic to classify participants into solo and team groups, ensuring proper handling of team representatives and blocked entries. Adjust messages for clarity in team-related errors. Increment version code. 2026-07-22 14:57:56 -04:00
0a19621949 MSIN-4475 — Refactor CSS for improved layout and responsiveness in assignment modes. Update JavaScript to enable immediate tool display without tile clicks, and adjust PHP for seamless workspace transitions. Increment version code to 4.72.867. 2026-07-22 14:45:33 -04:00
162419069b MSIN-4419 — Enhance refund cancellation hint with updated text and dynamic rescan functionality. Update version code to 4.72.865. 2026-07-22 14:34:19 -04:00
b74d5ac34a MSIN-4475 — Update CSS for badge auto assignment to ensure proper display without overflow. Adjust PHP functions to allow temporary unlocking for promoters and update version code to 4.72.864. 2026-07-22 14:22:43 -04:00
26b658acf8 MSIN-4445 — Refactor free assignment logic to allow all solo races (with or without existing sequences) to be assigned without creating new sequences. Update related functions and improve documentation for clarity. Increment version code to 4.72.863. 2026-07-22 14:14:52 -04:00
25c626be45 MSIN-4474 — Implement return URL preservation for event modifications and participant management. Enhance navigation logic to ensure users return to the original list after actions like save or cancel. Introduce new functions to validate and resolve return URLs, maintaining consistency across the application. Increment version code. 2026-07-22 12:29:03 -04:00
b038537d1c MSIN-4474 — Enhance participant and event access logic by refining conditions for editable fields based on promoter status. Implement legacy access handling for event modifications and ensure proper identity preservation during updates. Increment version code. 2026-07-22 12:25:16 -04:00
4b6e461b32 MSIN-4328 — Refactor API response handling in fxChronotrackApiAdminRenderPageScript to improve error management and user feedback. Update logic to ensure job continuation despite errors in batches, and enhance progress rendering with detailed status messages. Adjust sync entry posting to utilize fixed sub-lots for efficiency, reducing API call volume. Increment version code. 2026-07-22 12:03:56 -04:00
dfab716014 MSIN-4328 — Implement new function to derive registration choice names from entries, enhancing race data handling. Update race label formatting to include registration choice when available, improving clarity in dropdown options. Increment version code. 2026-07-22 11:53:21 -04:00
aa61dfef15 MSIN-4328 — Introduce new functionality to normalize registration choice data for events and enhance race label formatting. Merge reg_choice names by race ID to improve clarity in dropdown labels. Update existing functions to streamline data handling and presentation. Increment version code. 2026-07-22 11:52:34 -04:00
7858823205 MSIN-4328 — Enhance race label formatting by including registration choice names when available. Introduce new functions to index and apply reg_choice labels to race rows, improving clarity in race data presentation. Increment version code. 2026-07-22 11:47:28 -04:00
4bb4d189e6 desjardins.lp@ms1timing.com 2026-07-22 11:33:40 -04:00
9143f7a967 MSIN-4473 — Update promoteur hub link styles to align with button design by adding border-radius. Increment version code to 4.72.862. 2026-07-22 11:29:01 -04:00
ad57a54407 MSIN-4379 & MSIN-4455 — Update auto bib assignment logic for team events. Refactor to utilize existing team bibs and enhance error handling. Implement logic to load used bibs before assignment, ensuring no duplicates. Increment version code. 2026-07-22 11:07:17 -04:00
fd6ec9b876 MSIN-4471 — Refactor PDF generation for improved layout and pagination handling. Adjust header and footer dimensions based on subtitle presence, and implement logic to correctly count and display intro pages. Update SQL documentation to enhance clarity in participant instructions. Increment version code. 2026-07-22 08:32:28 -04:00
db5b1b9da3 MSIN-4471 — Improve image handling in PDF generation by enforcing correct image type detection and ensuring proper width calculations. Update image insertion logic to accommodate various image formats and enhance layout consistency. Increment version code. 2026-07-22 08:04:58 -04:00
a8c588f41f MSIN-4471 — Update image handling and SQL documentation for bib distribution. Enhance image insertion function to support optional maximum width and centering. Modify SQL to include specific image references and improve clarity in participant instructions. Increment version code. 2026-07-22 08:00:47 -04:00
b97fa8a337 MSIN-4471 — Update PDF distribution labels for participant names and checkboxes. Introduce new functions for handling logo paths and inline text rendering in PDF documents. Adjust SQL insert statements for improved clarity and idempotency. Increment version code. 2026-07-22 07:50:44 -04:00
2d706fb628 MSIN-4471 — Enhance bib distribution document handling by adding new columns for participant names and checkboxes. Adjust maximum question display for improved readability. Update version code and documentation content for clarity and usability. Increment version code to 4.72.861. 2026-07-22 07:43:03 -04:00
9233c65351 MSIN-4471 — Refactor PDF margin handling in image and HTML rendering functions to improve layout consistency. Introduce methods for accessing margins in the PDF class, ensuring compatibility with various document structures. Increment version code. 2026-07-22 07:36:30 -04:00
7120453ec8 MSIN-4471 — Add functions for improved PDF document handling, including image path resolution, HTML rendering, and image insertion. Enhance the introduction page keys for document printing. Increment version code. 2026-07-22 07:30:49 -04:00
215d3777db MSIN-3487 — Update QR code generation for event results to support inline PNG rendering, enhancing user experience by allowing direct downloads. Simplify access control and improve code organization with public settings. Increment version code. 2026-07-22 07:12:28 -04:00
22f45f98ff MSIN-3487 — Add function to generate binary PNG QR for event results with optional logo. Update existing QR generation functions for improved URL handling. Enhance UI to display and download event result QR codes. Increment version code. 2026-07-22 07:06:27 -04:00
99aa63f0e1 MSIN-3487 — Add temporary UI for managing additional event fields, including a form for entering and saving result links. Implement feedback messages for link status and enhance navigation with new buttons. Increment version code. 2026-07-22 07:02:00 -04:00
e701fd5543 MSIN-3487 — Enhance kc.php to support QR short link entry for event results and key chains. Implement redirection based on event ID and improve database query handling. Update inc_fonctions.php to include extra event functions. Increment version code. 2026-07-22 06:54:42 -04:00
e85f25c21d MSIN-4470 — Add page numbering functionality to PDF generation for race bibs. Introduce UTF-8 to Windows-1252 text conversion for FPDF compatibility. Update footer and header handling in PDF output. Increment version code. 2026-07-21 17:30:02 -04:00
f74fbc3092 MSIN-4470 — Implement functionality to position the PDF tool detail dynamically based on the selected tool. Update race label handling in production XML generation to avoid repetition. Add default slot for tool detail in the UI. Increment version code to 4.72.860. 2026-07-21 15:30:04 -04:00
f29ed77969 MSIN-4470 — Refactor bib workspace navigation and action buttons for improved accessibility and user experience. Update styles for workspace elements and enhance button interactions. Increment version code to 4.72.858. 2026-07-21 15:19:49 -04:00
31a7c9d012 MSIN-4469 — Introduce development unlock feature for production events, allowing temporary editing of ranges. Update UI elements and language keys for user guidance. Increment version code to 4.72.856. 2026-07-21 14:45:47 -04:00
afdf784215 MSIN-4469 — Enhance production locking mechanism for bib ranges, adding user feedback for printing status and updating related UI elements. Increment version code to 4.72.855. 2026-07-21 14:34:27 -04:00
0a35370a81 MSIN-4469 — Implement locking mechanism for production events to prevent modifications during printing. Add validation checks in AJAX and PHP functions, and update language keys for user feedback. Increment version code. 2026-07-21 14:26:56 -04:00
2cb9417bbf MSIN-4468 — Add validation message for minimum total quantity in race management. Update language keys for improved user feedback. Increment version code. 2026-07-21 13:24:21 -04:00
35f1a15fc7 MSIN-4468 — Enhance quantity validation in AJAX and JavaScript for race management. Implement error handling for total quantity below registrations and update related UI elements. Increment version code to 4.72.853. 2026-07-21 13:19:01 -04:00
39c062eafc MSIN-4468 — Implement total capacity management in AJAX and update related JavaScript functions for quantity handling. Adjust CSS grid layout for statistics display. Increment version code to 4.72.852. 2026-07-21 13:14:38 -04:00
8027155bde Update CSS for bib race summary layout by adding left margin to identity head cell. Increment version code to 4.72.851. 2026-07-21 12:33:06 -04:00
c6be4c38a6 MSIN-4467 — Remove 'anomalies' button from bib tool navigation and update JavaScript logic for tool categorization. Increment version code to 4.72.850. 2026-07-21 12:28:52 -04:00
eedb5d45d4 MSIN-4467 — Update accordion functionality in race detail blocks to allow both panels to be closed while ensuring at most one remains open. Refine comments for clarity. Increment version code to 4.72.849. 2026-07-21 12:17:29 -04:00
6d6e3c68f5 MSIN-4467 — Set default state for race expansion to false in bib tool, allowing users to choose which event details to expand. Update comments for clarity on functionality. Increment version code. 2026-07-21 12:06:58 -04:00
14727ce6b9 MSIN-4467 — Enhance accordion functionality for race detail blocks in JavaScript, ensuring only one panel remains open at a time. Update CSS for secondary headers with new background colors and hover effects. Increment version code to 4.72.848. 2026-07-21 12:02:24 -04:00
52d354bc9f MSIN-4467 — Update CSS for secondary headers to improve visual compactness and styling. Add new language keys for registration quantities and bib assignment in PHP. Increment version code to 4.72.847. 2026-07-21 11:58:24 -04:00
9732763fc8 MSIN-4467 — Refactor CSS for bib race summary and team mode components, enhancing layout with flexbox and improving responsiveness. Update JavaScript to handle team mode selection and display. Increment version code to 4.72.846. 2026-07-21 11:49:44 -04:00
4bc61c370c Update CSS styles for bib race summary and menu components, adjusting border and background colors for enhanced visual appeal. Increment version code to 4.72.845. 2026-07-21 11:36:26 -04:00
62037febe2 MSIN-4467 — Refactor CSS for bib race summary groups, enhancing layout with flexbox for better alignment and spacing. Update padding, borders, and background colors for improved visual consistency. Increment version code. 2026-07-21 11:33:31 -04:00
fd25dacaee MSIN-4467 — Update CSS for bib tool navigation layout, modifying grid structure for improved responsiveness. Enhance JavaScript logic to include additional tools in operations section. Remove deprecated navigation elements from the PHP rendering function. Increment version code. 2026-07-21 11:30:45 -04:00
397d82a458 MSIN-4467 — Enhance bib management workspace with new grouping for quantities and bibs, update CSS for improved layout and styling, and increment version code to 4.72.844. 2026-07-21 11:27:23 -04:00
74 changed files with 11359 additions and 1150 deletions

View File

@ -42,11 +42,12 @@ 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.)
-- Notes : exécution UNIQUEMENT sur dev préprod ; autres env = Navicat structure + sync_static_db
```
- 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/`).
- **Ne jamais** documenter / conseiller dexécuter ces scripts hors **dev préprod** (voir `projet/10-workflow.mdc` — Promotion BD).
## Résumé des changements (agent)
@ -54,6 +55,6 @@ En-tête obligatoire en tête de fichier :
1. **Quoi** — comportement ajouté ou corrigé.
2. **Où** — fichiers principaux touchés.
3. **Déploiement** — SQL, `_VERSION_CODE`, config, étapes manuelles.
3. **Déploiement** — SQL **dev préprod seulement**, puis Navicat structure + outil sync pour les autres env ; `_VERSION_CODE` / config / étapes manuelles.
Ne pas créer de fichiers README ou docs hors demande explicite.

View File

@ -0,0 +1,102 @@
---
description: Projet MS1 hors transaction — import / saisie rapide / ChronoTrack (cadrage produit)
alwaysApply: true
---
# Projet MSIN-4482 — entrée hors transaction (import + terrain)
JIRA : **MSIN-4482**.
Orthographe produit : **ChronoTrack**.
Doc de cadrage — source de vérité pour le développement de ce chantier.
## Intention
Utiliser la suite MS1 (gestion, ChronoTrack, outils promoteur existants) pour des clients **sans** tunnel dinscription / paiement en ligne MS1.
Les participants arrivent surtout par **import fichier** (puis saisie rapide plus tard) ; ensuite sync ChronoTrack.
Aussi utile plus tard le **jour J** sur une course **avec** inscription en ligne : correction / ajout rapide **sans** transaction financière.
## Décisions actées
| Sujet | Décision |
|---|---|
| Source terrain | Toujours **`resultats_*`** — **pas de panier / `no_panier`** pour ces lignes |
| Type événement | **`inscriptions_types_evenements`** — libellé FR **Sans inscription en ligne** / EN **No online registration** (`sql/MSIN-4482-type-sans-inscription-en-ligne.sql`) ; lévénement pointe via `inscriptions_evenements.te_id` |
| Paiement MS1 | Hors scope ; **interdit** le « panier à 0 $ » |
| Clé unique | ID du fichier client = **`external_id` ChronoTrack** au push ; même fichier = fallback import CT |
| Questions import | **Option C** : matcher ; non matchées → preview + **création confirmée** |
| Hub promoteur | **Ne pas toucher pour linstant** — hub actuel tel quel |
| Qui est promoteur | Opérateur MS1 se met **lui-même** promoteur ; droits clients = kits **minimisés via lexistant** |
| Mini-hub | **Plus tard**, au fur et à mesure — pas un livrable immédiat |
| Waiver | **Hors scope** |
| Édition / saisie rapide | Après limport (phase suivante) |
| Finances | Marqueur d**origine** ; import / saisie rapide exclus des rapports paiement |
## Origines participant (marqueur)
| Code | Canal |
|---|---|
| `inscription_en_ligne` | Flux payant actuel |
| `import_fichier` | Bulk modèle fichier |
| `saisie_rapide` | Création unitaire / jour J (plus tard) |
## Phases (réalignées)
1. **UI import superadm** — dropzone, mapping colonnes + épreuves, templates, bouton **Importer**
2. **Écriture `resultats_*`** — upsert clé unique + questions C (sans panier) — en place
3. **Mapping pays** (valeurs fichier → `pay_id`) — **étape 2**
4. **ChronoTrack** — push avec `par_external_id`
5. **Saisie / édition** — fiche rapide (+ jour J)
6. **Hub minimisé** — seulement si/quand nécessaire
## UI import (décisions)
- Emplacement v1 : **superadm fiche événement** (panneau sous ChronoTrack ; ancre « Import »)
- Colonne fichier pour lépreuve → match vers `epr_id` de lévénement
- UX : dropzone ludique + mapping guidé
- Template mapping : table `inscriptions_import_templates` (empreinte en-têtes + eve/com)
## Exemple fichier client (référence)
Fichier type : export ~Échappée (en-têtes déjà normalisés ; loriginal client peut différer).
| Colonne exemple | Cible MS1 |
|---|---|
| Attendee # | `par_external_id` (clé unique / CT) |
| First Name / Last Name | `par_prenom` / `par_nom` |
| Ticket Type | `epr` (valeurs → épreuves MS1) |
| No dossard | `no_bib` |
| Gender (Male/Female/Other) | `par_sexe` (normaliser à lécriture) |
| Age / Birth Date | `par_age` / `par_naissance` |
| Nationalité, urgence, allergies, condition médicale, langue… | questions (option C) |
**Visibilité liste / CT** : les coquilles `resultats_epreuves_commandees` doivent avoir **`pec_actif = 1`** (sinon absentes de la liste inscriptions et du sync). Les `I{eve}X…` = faux `no_commande` technique, pas un vrai panier.
~1800 lignes, ~1012 valeurs distinctes de Ticket Type (distances / jours).
## Fichiers
| Rôle | Fichier |
|---|---|
| SQL templates | `sql/MSIN-4482-import-resultats-templates.sql` |
| SQL type eve | `sql/MSIN-4482-type-sans-inscription-en-ligne.sql` |
| SQL fix liste | `sql/MSIN-4482-fix-pec-actif-import.sql` |
| Logique + UI | `php/inc_fx_import_resultats.php` |
| AJAX | `superadm/ajax_import_resultats.php` |
| Branche fiche eve | `superadm/php/inc_form.php` |
| CSS | `superadm/css/style_blue.css` |
## Hors scope (maintenant)
- Hub promoteur / mini-hub
- Waiver
- Écriture `resultats_*` (prochaine étape après UI)
- Import inverse CT → MS1
## Règles agent
- Changement minimal ; référencer **MSIN-4482**.
- **Pas de panier** pour ces participants — écrire dans `resultats_*`.
- **Ne pas modifier le hub promoteur** tant que ce nest pas redemandé.
- Ne pas inventer de waiver pour « signature de dossier ».
- SQL : **dev préprod** seulement, puis Navicat + sync outil.

View File

@ -1,5 +1,5 @@
---
description: Workflow MS1 — JIRA, validation avant gros changements, code legacy
description: Workflow MS1 — JIRA, validation avant gros changements, code legacy, promotion BD
alwaysApply: true
---
@ -8,10 +8,25 @@ alwaysApply: true
## 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.
- Préfixe courant features : `MSIN-xxxx`. Continuité / ops BD peut aussi être `CON-xxxx`.
- 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`.
## Promotion BD (règle absolue)
Ordre **non négociable** pour faire passer structure + données vers dautres environnements :
1. **Scripts SQL** (`sql/MSIN-*.sql`) → exécutés **uniquement** sur **dev préprod**.
2. **Navicat** → copie de **structure** (préprod → autres env : dev prod, client préprod, client prod).
3. **Outil soft sync** (`php/sync_static_db.php`, CON-333) → peupler / aligner les **données** (manquants + corrections).
**Interdit** : demander ou suggérer de « rejouer le SQL » sur prod, préprod client, ou tout env autre que **dev préprod**.
- Table absente / Erreur lecture ailleurs = dabord **structure Navicat** depuis préprod, **puis** sync outil.
- `info` : jamais de DELETE (auto-création `afficheTexte`). Soft only.
- Catalogues (droits, pages, doc…) : pas dauto-création ; lalignement données passe par loutil, pas par un second import SQL.
## Portée des changements
- **Changement minimal** : corriger uniquement ce qui est demandé.
@ -28,5 +43,6 @@ Quand du code voisin contredit le pattern du fichier ou du module :
## 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).
- Résumer ce qui a changé (fichiers, comportement).
- Pour le SQL : rappeler quil sexécute **seulement en dev préprod**, puis structure Navicat + sync outil pour le reste — **pas** « exécuter ce script en prod ».
- Mentionner les étapes manuelles restantes (déploiement code, Navicat, sync soft, cache).

View File

@ -57,6 +57,26 @@ if ($action !== '' && in_array($action, $arrBibAjaxActions, true)) {
fxBibRequireAjaxAccess();
}
// MSIN-4469 — Le fichier officiel verrouille toute modification des séquences.
$arrBibProductionLockedActions = [
'create',
'delete',
'save',
'toggle_lock',
'save_auto_config',
'batch_global_gen_create',
];
if (in_array($action, $arrBibProductionLockedActions, true)) {
$intProductionEveId = fxBibResolveEveIdFromRequest();
if ($intProductionEveId > 0 && fxBibProductionEventIsLocked($intProductionEveId)) {
echo json_encode([
'success' => false,
'message' => fxBibMsg('bib_v5_production_locked_error'),
]);
exit;
}
}
// =====================
// CREATE
// =====================
@ -715,7 +735,7 @@ if ($action == 'batch_global_free_analysis') {
$intEnd = (int)($_POST['end_bib'] ?? 0);
list($strSort1, $strSort2) = fxBibParseBatchSortFromPost();
$eprIds = json_decode($_POST['epr_ids'] ?? '[]', true);
$tabEprIds = fxBibParseGlobalBatchGeneratedEprIds(is_array($eprIds) ? $eprIds : [], $int_eve_id);
$tabEprIds = fxBibParseGlobalBatchFreeEprIds(is_array($eprIds) ? $eprIds : [], $int_eve_id);
if ($int_eve_id <= 0 || $intStart <= 0 || $intEnd < $intStart) {
echo json_encode([
@ -765,7 +785,7 @@ if ($action == 'batch_global_free_simulate') {
$intEnd = (int)($_POST['end_bib'] ?? 0);
list($strSort1, $strSort2) = fxBibParseBatchSortFromPost();
$eprIds = json_decode($_POST['epr_ids'] ?? '[]', true);
$tabEprIds = fxBibParseGlobalBatchGeneratedEprIds(is_array($eprIds) ? $eprIds : [], $int_eve_id);
$tabEprIds = fxBibParseGlobalBatchFreeEprIds(is_array($eprIds) ? $eprIds : [], $int_eve_id);
if ($int_eve_id <= 0 || $intStart <= 0 || $intEnd < $intStart || $strSort1 === '' || empty($tabEprIds)) {
echo json_encode([
@ -798,7 +818,7 @@ if ($action == 'batch_global_free_plan') {
$intEnd = (int)($_POST['end_bib'] ?? 0);
list($strSort1, $strSort2) = fxBibParseBatchSortFromPost();
$eprIds = json_decode($_POST['epr_ids'] ?? '[]', true);
$tabEprIds = fxBibParseGlobalBatchGeneratedEprIds(is_array($eprIds) ? $eprIds : [], $int_eve_id);
$tabEprIds = fxBibParseGlobalBatchFreeEprIds(is_array($eprIds) ? $eprIds : [], $int_eve_id);
if ($int_eve_id <= 0 || $intStart <= 0 || $intEnd < $intStart || $strSort1 === '' || empty($tabEprIds)) {
echo json_encode([
@ -1108,7 +1128,7 @@ if ($action == 'batch_apply_free_chunk') {
exit;
}
// Même filtre que le panneau : solo sans séquence.
// MSIN-4445 — Solo seulement ; une séquence existante n'empêche pas l'assignation libre.
$intEveId = (int)($_POST['eve_id'] ?? 0);
if ($intEveId > 0) {
$checkEpr = fxBibAssertSoloEpreuveForGlobalBatch($epr_id, $intEveId);
@ -1116,13 +1136,6 @@ if ($action == 'batch_apply_free_chunk') {
echo json_encode($checkEpr);
exit;
}
if (fxBibEpreuveHasAnySequence($epr_id)) {
echo json_encode([
'success' => false,
'message' => fxBibMsg('bib_v4_global_batch_gen_has_seq'),
]);
exit;
}
}
$strSessionKey = fxBibBatchExecSessionKey($epr_id) . '_free';
@ -1261,7 +1274,9 @@ if ($action == 'reset_apply') {
$sql = "
UPDATE resultats_participants
SET no_bib = NULL
SET no_bib = NULL,
par_date_bib = NULL,
par_maj = '" . fxGetDateTime() . "'
WHERE par_id = $par_id
AND is_cancelled = 0
";
@ -1330,7 +1345,9 @@ if ($action == 'reset_all_apply') {
$sql = "
UPDATE resultats_participants
SET no_bib = NULL
SET no_bib = NULL,
par_date_bib = NULL,
par_maj = '" . fxGetDateTime() . "'
WHERE par_id = $par_id
AND is_cancelled = 0
";

View File

@ -1,13 +1,22 @@
<?php
session_start();
// MSIN-4468 — Les libellés AJAX appartiennent toujours au contexte compte.php.
define('MS1_PROMOTEUR_AJAX', true);
$_SERVER['PHP_SELF'] = '/compte.php';
$_SERVER['SCRIPT_NAME'] = '/compte.php';
$vPage = 'compte.php';
require_once "php/inc_fonctions.php";
include_once("php/inc_fx_modifierinscriptions.php");
require_once "php/inc_fx_promoteur.php";
require_once "php/inc_fx_messages.php";
require_once "php/inc_fx_eve_acces.php";
global $vDomaine;
$strAction = $intEvenement = $intNewNoBib = $intEpreuveCommandee = $intEpreuve = $intPartipcipant = $intQuantite = $strLangue = $strStatutCourse = '';
$strAction = $intEvenement = $intNewNoBib = $intEpreuveCommandee = $intEpreuve = $intPartipcipant = $intQuantite = $strStatutCourse = '';
$strLangue = $_SESSION['lang'] ?? 'fr';
$strQuantiteTotale = '';
if (isset($_REQUEST['a'])) {
$strAction = $_REQUEST['a'];
@ -30,22 +39,65 @@ if (isset($_REQUEST['par_id'])) {
if (isset($_REQUEST['qte'])) {
$intQuantite = $_REQUEST['qte'];
}
if (isset($_REQUEST['lang'])) {
if (isset($_REQUEST['total'])) {
$strQuantiteTotale = trim((string)$_REQUEST['total']);
}
if (isset($_REQUEST['lang']) && in_array($_REQUEST['lang'], array('fr', 'en'), true)) {
$strLangue = $_REQUEST['lang'];
}
if (isset($_REQUEST['par_statut_course'])) {
$strStatutCourse = $_REQUEST['par_statut_course'];
}
$vPage = 'compte.php';
$vtexte_page = obtenirTextepage($vPage, $strLangue, 2);
$tabRetour = array();
header('Content-Type: application/json; charset=utf-8');
switch($strAction) {
case 'qte':
$tabRetour = fxUpdateQuantites($intEpreuve, $intQuantite, $strLangue);
break;
case 'qte_total':
// MSIN-4468 — Nouvelle convention V4; l'action legacy qte demeure inchangée.
if (!fxBibRequirePromoteurSession()) {
$tabRetour = array('state' => 'error', 'message' => fxBibMsg('bib_v4_ajax_session_invalid'));
break;
}
if (!fxBibValidateCsrf($_POST['csrf_token'] ?? '')) {
$tabRetour = array('state' => 'error', 'message' => fxBibMsg('bib_v4_ajax_unauthorized'));
break;
}
$intEveId = fxBibResolveEveIdFromRequest();
$blnCanEditQte = !empty($_SESSION['usa_id'])
|| (!empty($_SESSION['com_id'])
&& $intEveId > 0
&& fxEveAccesHasPermission($_SESSION['com_id'], $intEveId, 'epreuves.edit_qte'));
if (!$blnCanEditQte) {
$tabRetour = array('state' => 'error', 'message' => fxBibMsg('bib_v4_ajax_unauthorized'));
break;
}
if ($strQuantiteTotale === ''
|| strlen($strQuantiteTotale) > 7
|| !ctype_digit($strQuantiteTotale)) {
$tabRetour = array('state' => 'error', 'message' => afficheTexte('promoteur_bad_qte_update', 0));
break;
}
$tabRetour = fxUpdateQuantiteTotale($intEpreuve, intval($strQuantiteTotale));
if (($tabRetour['state'] ?? 'error') !== 'success') {
if (($tabRetour['error_code'] ?? '') === 'total_below_registrations') {
$tabRetour['message'] = fxBibMsg(
'bib_v5_qte_total_min',
intval($tabRetour['qte_inscrits'] ?? 0)
);
} else {
$tabRetour['message'] = afficheTexte('promoteur_bad_qte_update', 0);
}
}
break;
case 'no_equipe':
case 'no_bib':
$tabRetour = fxSetBibManuel($strAction, $intEvenement, $intNewNoBib, $intEpreuveCommandee, $intEpreuve, $intPartipcipant, $strLangue);

View File

@ -0,0 +1 @@

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 KiB

83
bib_production_excel.php Normal file
View File

@ -0,0 +1,83 @@
<?php
// MSIN-4469 — Génération et retéléchargement du fichier officiel de production.
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');
require_once('php/inc_fx_eve_acces.php');
$strLangue = $_SESSION['lang'] ?? 'fr';
$strReqLang = $_REQUEST['lang'] ?? '';
if (in_array($strReqLang, ['fr', 'en'], true)) {
$strLangue = $strReqLang;
}
$vPage = 'compte.php';
$vtexte_page = obtenirTextepage($vPage, $strLangue, 2);
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
exit;
}
$intEveId = (int)($_POST['eve_id'] ?? 0);
$strAction = (string)($_POST['action'] ?? '');
fxBibRequireDistPrintAccess();
if (!fxBibProductionUserCanManageEvent($intEveId)) {
http_response_code(403);
header('Content-Type: text/plain; charset=utf-8');
echo fxBibMsg('bib_v4_ajax_unauthorized');
exit;
}
if ($strAction === 'generate') {
$tabResult = fxBibProductionCreateOfficialOrder($intEveId, $strLangue);
if (empty($tabResult['success']) || empty($tabResult['order'])) {
http_response_code(400);
header('Content-Type: text/plain; charset=utf-8');
echo $tabResult['message'] ?? fxBibMsg('bib_v5_production_generation_error');
exit;
}
$tabOrder = $tabResult['order'];
} elseif ($strAction === 'download') {
$intOrderId = (int)($_POST['order_id'] ?? 0);
$tabOrder = fxBibProductionGetOrder($intOrderId, $intEveId);
if (!$tabOrder || (int)($tabOrder['bpo_active_key'] ?? 0) !== 1) {
http_response_code(404);
header('Content-Type: text/plain; charset=utf-8');
echo fxBibMsg('bib_v5_production_file_missing');
exit;
}
} elseif ($strAction === 'development_unlock') {
$tabResult = fxBibProductionDevelopmentUnlock($intEveId);
if (empty($tabResult['success'])) {
http_response_code(403);
header('Content-Type: text/plain; charset=utf-8');
echo $tabResult['message'] ?? fxBibMsg('bib_v4_ajax_unauthorized');
exit;
}
$strAccountPage = $strLangue === 'en' ? '/account' : '/compte';
header(
'Location: ' . $strAccountPage
. '/inc_tableau_gestion_epreuves?promoteur_eve_id='
. urlencode(base64_encode($intEveId))
);
exit;
} else {
http_response_code(400);
exit;
}
if (!fxBibProductionStreamOrder($tabOrder)) {
http_response_code(404);
header('Content-Type: text/plain; charset=utf-8');
echo fxBibMsg('bib_v5_production_file_missing');
exit;
}
exit;

View File

@ -1078,7 +1078,7 @@ ul.ms1-menu-compte li a:hover, ul.ms1-menu-compte li a:active {
margin-top:10px;
}
#module-bib .bib-workspace-nav{
#module-bib .bib-workspace-bar{
display:flex;
align-items:stretch;
gap:4px;
@ -1089,6 +1089,12 @@ ul.ms1-menu-compte li a:hover, ul.ms1-menu-compte li a:active {
overflow-x:auto;
}
#module-bib .bib-workspace-nav{
display:flex;
align-items:stretch;
gap:4px;
}
#module-bib .bib-workspace-tab{
display:inline-flex;
align-items:center;
@ -1107,6 +1113,41 @@ ul.ms1-menu-compte li a:hover, ul.ms1-menu-compte li a:active {
cursor:pointer;
}
/* MSIN-4470 — L'impression PDF reste accessible à droite des espaces principaux. */
#module-bib .bib-workspace-action{
display:flex;
flex:0 0 auto;
margin-left:auto;
}
#module-bib .bib-workspace-action .bib-tool-btn{
width:auto;
min-height:42px;
padding:8px 16px;
border-width:0;
background:transparent;
color:#495057;
font-size:13px;
font-weight:600;
line-height:1.25;
}
#module-bib .bib-workspace-action .bib-tool-btn:not(.is-active):hover,
#module-bib .bib-workspace-action .bib-tool-btn:not(.is-active):focus{
background:#e8f0f8;
color:#1e4a9e;
box-shadow:none;
}
#module-bib .bib-workspace-action .bib-tool-btn.is-active,
#module-bib .bib-workspace-action .bib-tool-btn.is-active:hover,
#module-bib .bib-workspace-action .bib-tool-btn.is-active:focus{
background:#2f5fd0;
color:#fff;
border-color:transparent;
box-shadow:none;
}
#module-bib .bib-workspace-tab:hover,
#module-bib .bib-workspace-tab:focus{
background:#e8f0f8;
@ -1145,9 +1186,37 @@ ul.ms1-menu-compte li a:hover, ul.ms1-menu-compte li a:active {
margin-bottom:16px;
}
#module-bib .bib-preparation-groups{
display:grid;
grid-template-columns:repeat(2, minmax(0, 1fr));
gap:12px;
}
#module-bib .bib-preparation-group{
display:flex;
flex-direction:column;
gap:10px;
min-width:0;
padding:12px;
border:1px solid #c8d6e5;
border-radius:8px;
background:#f8fbfe;
}
#module-bib .bib-preparation-group--quantities{
background:#fafbfc;
}
#module-bib .bib-preparation-group h3{
margin:0;
color:#1e3a6e;
font-size:14px;
font-weight:700;
}
#module-bib .bib-preparation-stats{
display:grid;
grid-template-columns:repeat(4, minmax(0, 1fr));
grid-template-columns:repeat(2, minmax(0, 1fr));
gap:1px;
margin:0;
overflow:hidden;
@ -1201,9 +1270,8 @@ ul.ms1-menu-compte li a:hover, ul.ms1-menu-compte li a:active {
gap:8px;
}
#module-bib .bib-tool-nav--preparation,
#module-bib .bib-tool-nav--operations{
grid-template-columns:repeat(2, minmax(0, 1fr));
grid-template-columns:repeat(3, minmax(0, 1fr));
}
#module-bib .bib-tool-nav--settings{
@ -1211,6 +1279,11 @@ ul.ms1-menu-compte li a:hover, ul.ms1-menu-compte li a:active {
max-width:520px;
}
#module-bib .bib-tool-nav--group{
grid-template-columns:minmax(0, 1fr);
margin-top:auto;
}
#module-bib .bib-tool-btn:nth-child(4){
grid-column:1;
}
@ -1623,6 +1696,7 @@ ul.ms1-menu-compte li a:hover, ul.ms1-menu-compte li a:active {
}
@media (max-width:768px){
#module-bib .bib-workspace-bar,
#module-bib .bib-workspace-nav{
align-items:stretch;
}
@ -1631,6 +1705,10 @@ ul.ms1-menu-compte li a:hover, ul.ms1-menu-compte li a:active {
flex:1 0 auto;
}
#module-bib .bib-preparation-groups{
grid-template-columns:1fr;
}
#module-bib .bib-preparation-stats{
grid-template-columns:repeat(2, minmax(0, 1fr));
}
@ -1947,10 +2025,8 @@ ul.ms1-menu-compte li a:hover, ul.ms1-menu-compte li a:active {
}
.epr-row-detail{
display:grid;
grid-template-columns:minmax(148px, 178px) minmax(0, 1fr);
gap:12px;
align-items:start;
/* MSIN-4467 — Le détail utilise toute la largeur, sans colonne d'identité. */
display:block;
padding:12px;
border-top:1px solid #dbe4f3;
}
@ -1961,7 +2037,7 @@ ul.ms1-menu-compte li a:hover, ul.ms1-menu-compte li a:active {
.bib-race-summary{
display:grid;
grid-template-columns:minmax(220px, 1.4fr) minmax(300px, 1.4fr) auto 32px;
grid-template-columns:minmax(220px, 1.2fr) minmax(180px, .9fr) minmax(270px, 1.3fr) 32px;
gap:14px;
align-items:center;
min-height:64px;
@ -1983,17 +2059,60 @@ ul.ms1-menu-compte li a:hover, ul.ms1-menu-compte li a:active {
.bib-race-summary__identity{
display:flex;
flex-direction:column;
gap:2px;
gap:4px;
min-width:0;
}
.bib-race-summary__identity strong{
.bib-race-summary__identity-head{
display:flex;
align-items:center;
gap:7px;
min-width:0;
}
.bib-race-summary__identity-head strong{
min-width:0;
overflow:hidden;
color:#1e3a6e;
font-size:14px;
line-height:1.3;
text-overflow:ellipsis;
white-space:nowrap;
}
.bib-race-summary__identity span{
.bib-race-summary__identity-head .bib-view-range-cell{
flex:0 0 auto;
margin-left:auto;
}
.bib-race-summary-view{
display:inline-flex;
align-items:center;
justify-content:center;
min-width:26px;
height:24px;
padding:2px 6px;
border:1px solid #b8c9e8;
background:#fff;
color:#2f5fd0;
}
.bib-race-summary-view:hover,
.bib-race-summary-view:focus{
border-color:#8fa8d8;
background:#e8f0fc;
color:#1e4fbf;
}
.bib-race-summary__metadata{
display:flex;
align-items:center;
flex-wrap:wrap;
gap:3px 6px;
min-width:0;
}
.bib-race-summary__metadata span{
overflow:hidden;
color:#6c757d;
font-size:11px;
@ -2001,6 +2120,43 @@ ul.ms1-menu-compte li a:hover, ul.ms1-menu-compte li a:active {
white-space:nowrap;
}
.bib-race-summary__metadata span + span::before{
content:"·";
margin-right:6px;
color:#9aa4b2;
}
.bib-race-summary__group{
display:flex;
flex-direction:column;
justify-content:center;
align-self:stretch;
min-width:0;
padding:5px 10px;
border:1px solid #cbd3dc;
border-radius:6px;
background:#f1f3f5;
}
.bib-race-summary__group--bibs{
border-color:#b8c9e8;
background:#e8f0fc;
}
.bib-race-summary__group h4{
margin:0 0 5px;
color:#1e3a6e;
font-size:11px;
font-weight:800;
letter-spacing:.06em;
text-transform:uppercase;
}
/* MSIN-4470 — Accentue légèrement les deux repères principaux de chaque épreuve. */
.bib-race-summary__group--bibs h4{
color:#1e4fbf;
}
.bib-race-summary__stats{
display:grid;
grid-template-columns:repeat(3, minmax(0, 1fr));
@ -2008,6 +2164,10 @@ ul.ms1-menu-compte li a:hover, ul.ms1-menu-compte li a:active {
margin:0;
}
.bib-race-summary__group--quantities .bib-race-summary__stats{
grid-template-columns:repeat(2, minmax(0, 1fr));
}
.bib-race-summary__stats > div{
min-width:0;
}
@ -2213,11 +2373,86 @@ ul.ms1-menu-compte li a:hover, ul.ms1-menu-compte li a:active {
overflow:hidden;
}
.bib-team-mode-step{
padding:12px 14px;
border-bottom:1px solid #b8c9e8;
background:#f8fafc;
}
.bib-team-mode-step .team-mode-box{
margin:0;
padding:0;
border:0;
background:transparent;
}
.bib-team-mode-step .team-mode-title{
margin:0 0 8px;
color:#1e3a6e;
font-size:13px;
font-weight:700;
}
.bib-team-mode-options{
display:flex;
align-items:center;
flex-wrap:wrap;
gap:8px 24px;
}
.bib-team-mode-options .team-mode-option{
align-items:center;
margin:0;
}
.bib-team-mode-options .team-mode-option input[type="radio"]{
margin-top:0;
}
.epr-col-right .epr-block{
border:none;
border-radius:0;
}
/* MSIN-4467 — En-têtes secondaires plus compacts et moins dominants visuellement. */
.epr-col-right > .epr-block > .epr-header{
padding:8px 10px;
border-bottom:1px solid #b8c9e2;
background:#d9e4f4;
color:#1e3a6e;
font-size:14px;
cursor:pointer;
}
.epr-col-right > .epr-block:not(.is-collapsed) > .epr-header{
background:#c7d7ef;
}
.epr-col-right > .epr-block > .epr-header:hover{
background:#cfddf1;
}
.epr-col-right > .epr-block > .epr-header .epr-block-toggle{
width:26px;
height:26px;
border-color:#9fb2cf;
background:#fff;
color:#2f5fd0;
}
.epr-col-right > .epr-block > .epr-header .epr-block-toggle:hover,
.epr-col-right > .epr-block > .epr-header .epr-block-toggle:focus{
border-color:#7f9bc5;
background:#e3ebf6;
color:#1e4fbf;
}
.epr-col-right > .epr-block > .epr-header .epr-header-summary--seq-empty{
border-color:#c3cede;
background:#fff;
color:#65758b;
}
.epr-col-right .epr-block + .epr-block{
border-top:1px solid #b8c9e8;
}
@ -2242,7 +2477,7 @@ ul.ms1-menu-compte li a:hover, ul.ms1-menu-compte li a:active {
.epr-gestion-stats{
display:grid;
grid-template-columns:repeat(3, minmax(0, 1fr));
grid-template-columns:repeat(4, minmax(0, 1fr));
gap:12px 16px;
margin:0;
}
@ -2370,14 +2605,11 @@ ul.ms1-menu-compte li a:hover, ul.ms1-menu-compte li a:active {
.epr-block-assign .bib-container{
width:100%;
min-width:0;
/* MSIN-4475 — filet mobile étroit seulement ; plus de min-width 1100px (slider inutile). */
overflow-x:auto;
-webkit-overflow-scrolling:touch;
}
.epr-block-assign .bib-container .epr-line{
min-width:720px;
}
.epr-line-header{
background:#fff3cd;
font-weight:700;
@ -2633,11 +2865,16 @@ ul.ms1-menu-compte li a:hover, ul.ms1-menu-compte li a:active {
font-weight:600;
line-height:1.2;
padding:0.35em 0.5em;
white-space:nowrap;
/* MSIN-4475 — peut passer sur 2 lignes si la colonne restante est étroite. */
white-space:normal;
}
.bib-auto-badge--row{
display:inline-block;
display:inline-flex;
align-items:center;
flex-wrap:wrap;
gap:2px;
max-width:100%;
}
.bib-container.batch-mode .bib-auto-badge--row{
@ -2977,21 +3214,23 @@ a.ms1-trad-link.btn-aide-trad{
width:100%;
}
/* MSIN-4379 — Cadenas + view/OK/stats ; badge auto à droite. */
/* MSIN-4379 / MSIN-4469 cadenas utilisateur + impression + view/OK/stats. */
/* MSIN-4429 — colonnes Début | Qté | ou | Fin (alignées sur l'en-tête). */
/* MSIN-4475 — grille compacte (~640px min) pour tenir dans le wrapper page sans slider. */
.epr-line{
display:grid;
grid-template-columns:
42px
minmax(70px, 78px)
36px
36px
minmax(54px, 64px)
minmax(54px, 64px)
minmax(60px, 72px)
minmax(60px, 72px)
20px
minmax(54px, 64px)
minmax(72px, 80px)
40px
minmax(152px, 168px)
minmax(28px, 40px);
minmax(60px, 72px)
minmax(48px, 56px)
36px
minmax(140px, 152px)
minmax(0, 1fr);
gap:6px;
align-items:center;
width:100%;
@ -3000,15 +3239,16 @@ a.ms1-trad-link.btn-aide-trad{
.bib-container.batch-mode .epr-line{
grid-template-columns:
42px
minmax(70px, 78px)
36px
36px
minmax(54px, 64px)
minmax(54px, 64px)
minmax(60px, 72px)
minmax(60px, 72px)
20px
minmax(54px, 64px)
minmax(72px, 80px)
40px
minmax(152px, 168px)
minmax(60px, 72px)
minmax(48px, 56px)
36px
minmax(140px, 152px)
28px;
}
@ -3019,8 +3259,37 @@ a.ms1-trad-link.btn-aide-trad{
min-width:0;
}
.epr-col-production-lock{
display:flex;
align-items:center;
justify-content:center;
min-width:0;
}
.btn-bib-production-lock{
display:flex;
align-items:center;
justify-content:center;
width:38px;
min-height:31px;
padding:4px 0;
border:1px solid #2f4ba0;
background-color:#3f63c9;
color:#fff;
font-size:15px;
line-height:1;
cursor:help;
}
.btn-bib-production-lock:hover,
.btn-bib-production-lock:focus{
border-color:#263d83;
background-color:#3656ad;
color:#fff;
}
.btn-bib-lock{
width:100%;
width:38px;
min-height:31px;
padding:4px 0;
border-width:1px;
@ -3146,7 +3415,7 @@ a.ms1-trad-link.btn-aide-trad{
.epr-col-stats{
display:grid;
grid-template-columns:56px 48px 48px;
grid-template-columns:minmax(44px, 52px) minmax(40px, 48px) minmax(40px, 48px);
gap:4px;
align-items:center;
justify-content:start;
@ -3173,11 +3442,12 @@ a.ms1-trad-link.btn-aide-trad{
}
.epr-col-batch{
/* MSIN-4475 — badge auto dans la colonne fluide restante (pas de min-width forcé). */
overflow:hidden;
min-width:0;
display:flex;
align-items:center;
justify-content:flex-end;
justify-content:flex-start;
gap:4px;
}
@ -3212,6 +3482,26 @@ a.ms1-trad-link.btn-aide-trad{
gap:10px;
}
.bib-assignment-step__heading{
flex:1 1 auto;
min-width:0;
}
.bib-assignment-step__actions{
flex:0 0 auto;
margin-left:auto;
}
.bib-assignment-step__actions .epr-identity-reset-all{
padding:0;
border:0;
background:transparent;
}
.bib-assignment-step__actions .epr-identity-reset-all .btn-reset-all-epr{
width:auto;
}
.bib-assignment-step__number{
display:inline-flex;
align-items:center;
@ -3319,22 +3609,33 @@ a.ms1-trad-link.btn-aide-trad{
/* MOBILE */
@media (max-width:768px){
.epr-row-detail{
grid-template-columns: 1fr;
padding:8px;
}
.bib-race-summary{
grid-template-columns:minmax(0, 1fr) auto;
}
.bib-race-summary__stats{
.bib-race-summary__group{
grid-column:1 / -1;
padding:8px 10px;
}
.bib-race-summary__group--quantities{
grid-row:2;
}
.bib-race-summary__status{
grid-column:1;
.bib-assignment-step__header{
flex-wrap:wrap;
}
.bib-assignment-step__actions{
flex:1 0 100%;
margin-left:36px;
}
.bib-race-summary__group--bibs{
grid-row:3;
justify-self:start;
}
.bib-race-toggle{
@ -5714,6 +6015,11 @@ button.inscr-gestion-list-remis:hover{
padding-top: 1rem;
}
/* MSIN-4473 — liens GESTION / RAPPORTS : mêmes coins que #module-bib .bib-tool-btn */
.promoteur-hub-link {
border-radius: 8px;
}
/* ===== Hub promoteur : resume flash (haut) + tuiles par epreuve (collapse) ===== */
.promoteur-hub-card-row {
align-items: stretch;

49
dossard_print.php Normal file
View File

@ -0,0 +1,49 @@
<?php
/**
* MSIN-4485 — Endpoint PDF dossard (1 participant).
* Isolé du tunnel / panier ; droit inscriptions_gestion.dossard_print.
*/
session_start();
$_SERVER['PHP_SELF'] = '/compte.php';
$_SERVER['SCRIPT_NAME'] = '/compte.php';
require_once __DIR__ . '/php/inc_fonctions.php';
require_once __DIR__ . '/php/inc_fx_dossard_print.php';
$strLangue = $_SESSION['lang'] ?? 'fr';
$reqLang = $_REQUEST['lang'] ?? $_REQUEST['lng'] ?? '';
if (!empty($reqLang) && in_array($reqLang, array('fr', 'en'), true)) {
$strLangue = $reqLang;
}
$vPage = 'compte.php';
$vtexte_page = obtenirTextepage($vPage, $strLangue, 2);
$intParId = intval($_GET['par_id'] ?? $_POST['par_id'] ?? 0);
$intEveId = intval($_GET['eve_id'] ?? $_POST['eve_id'] ?? 0);
$intComId = intval($_SESSION['com_id'] ?? 0);
if ($intComId <= 0 || $intParId <= 0 || $intEveId <= 0) {
http_response_code(403);
header('Content-Type: text/plain; charset=utf-8');
echo 'Accès refusé.';
exit;
}
if (!fxDossardPrintCan($intComId, $intEveId)) {
http_response_code(403);
header('Content-Type: text/plain; charset=utf-8');
echo 'Droit insuffisant.';
exit;
}
$arrRow = fxDossardPrintLoadParticipant($intParId, $intEveId);
if ($arrRow === null) {
http_response_code(404);
header('Content-Type: text/plain; charset=utf-8');
echo 'Participant introuvable.';
exit;
}
fxDossardPrintOutputPdf($arrRow);

0
images/doc/.gitkeep Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 821 KiB

View File

@ -450,7 +450,10 @@ if ($strLangue == 'fr') {
strpath = '/book/';
}
document.location.href = '<?php echo $vDomaine; ?>' + strpath + code + '/' + intEprId + '?promoteur=1&action=mod&pec_id=' + intPecId;
// MSIN-4474 — ramener à la liste d'origine après Enregistrer / Annuler
document.location.href = '<?php echo $vDomaine; ?>' + strpath + code + '/' + intEprId
+ '?promoteur=1&action=mod&pec_id=' + intPecId
+ '&return_url=' + encodeURIComponent(window.location.href);
});
<?php
}
@ -1326,7 +1329,10 @@ if ($strLangue == 'fr') {
<?php
break;
case 'reserver.php':
if (($tabEvenement['general']['eve_epreuve_modifiable'] == 1 || $tabEvenement['general']['eve_epreuve_modifiable_client'] == 1) && $strAction == 'mod' && !isset($_SESSION['no_panier']) && fxCheckAccesEpreuve('mod', $intEpreuve, $tabEvenement['general']['eve_id'], $_GET['pec_id'], $_GET['promoteur'], '')) { // si le promoteur ou le participant peut changer l'épreuve
// MSIN-4474 — même périmètre que le HTML (inc_fx_reserver) : accès legacy, v2 ou admin
$intPecIdSwitch = isset($_GET['pec_id']) ? $_GET['pec_id'] : 0;
$intPromoteurSwitch = isset($_GET['promoteur']) ? $_GET['promoteur'] : 0;
if (($tabEvenement['general']['eve_epreuve_modifiable'] == 1 || $tabEvenement['general']['eve_epreuve_modifiable_client'] == 1) && $strAction == 'mod' && !isset($_SESSION['no_panier']) && fxCheckAccesEpreuve('mod', $intEpreuve, $tabEvenement['general']['eve_id'], $intPecIdSwitch, $intPromoteurSwitch, '')) { // si le promoteur ou le participant peut changer l'épreuve
?>
$("#sel_epreuve").change(function () {
$("#btn_changer_epreuve").show();
@ -1350,9 +1356,13 @@ if ($strLangue == 'fr') {
} else {
echo '';
} ?>';
//sl
// alert('<?php echo $vDomaine?>/' + strPage + '/' + strUrl + '/' + $("#sel_epreuve").val() + '?action=mod&switch=true' + strPromoteur + '&pec_id=<?php echo $_GET['pec_id']; ?>');
document.location.href = '<?php echo $vDomaine?>/' + strPage + '/' + strUrl + '/' + $("#sel_epreuve").val() + '?action=mod&switch=true' + strPromoteur + '&pec_id=<?php echo $_GET['pec_id']; ?>';
// MSIN-4474 — préserver return_url au switch d'épreuve
var strReturn = '';
var $returnUrl = $("#return_url");
if ($returnUrl.length && $returnUrl.val()) {
strReturn = '&return_url=' + encodeURIComponent($returnUrl.val());
}
document.location.href = '<?php echo $vDomaine?>/' + strPage + '/' + strUrl + '/' + $("#sel_epreuve").val() + '?action=mod&switch=true' + strPromoteur + '&pec_id=<?php echo isset($_GET['pec_id']) ? $_GET['pec_id'] : ''; ?>' + strReturn;
});
$("#frm_participant").valid();
<?php

View File

@ -58,7 +58,7 @@ require_once("inc_header.php");
}
}
echo str_replace('%liste_evenements%', $strListeEvenements, fxUnescape($recPage['pag_texte_' . $strLangue], 1));
echo str_replace('%liste_evenements%', $strListeEvenements, fxNormalizeCmsPageLinks(fxUnescape($recPage['pag_texte_' . $strLangue], 1)));
} else {
?>
<h1><?php echo fxUnescape($recPage['pag_titre_' . $strLangue], 1); ?></h1>
@ -66,12 +66,12 @@ require_once("inc_header.php");
switch ($strCode) {
case 'cookie':
$strCookieContent = trim((string) $recPage['pag_texte_' . $strLangue]);
echo $strCookieContent !== '' ? fxUnescape($recPage['pag_texte_' . $strLangue], 1) : fxCookiePolicyPageContent($strLangue);
echo $strCookieContent !== '' ? fxNormalizeCmsPageLinks(fxUnescape($recPage['pag_texte_' . $strLangue], 1)) : fxCookiePolicyPageContent($strLangue);
break;
case 'confidentialite':
$strPrivacyContent = trim((string) $recPage['pag_texte_' . $strLangue]);
echo $strPrivacyContent !== '' ? fxUnescape($recPage['pag_texte_' . $strLangue], 1) : fxPrivacyPolicyPageContent($strLangue);
echo $strPrivacyContent !== '' ? fxNormalizeCmsPageLinks(fxUnescape($recPage['pag_texte_' . $strLangue], 1)) : fxPrivacyPolicyPageContent($strLangue);
break;
case 'evenements':
@ -85,7 +85,7 @@ require_once("inc_header.php");
}
}
echo str_replace('%liste_evenements%', $strListeEvenements, fxUnescape($recPage['pag_texte_' . $strLangue], 1));
echo str_replace('%liste_evenements%', $strListeEvenements, fxNormalizeCmsPageLinks(fxUnescape($recPage['pag_texte_' . $strLangue], 1)));
break;
case 'contact_send':
print_r($_POST);
@ -93,7 +93,7 @@ require_once("inc_header.php");
break;
case 'contact':
$arrEvenements = fxGetEvenements('', $strLangue, 1, true);
$strText = fxUnescape($recPage['pag_texte_' . $strLangue], 1);
$strText = fxNormalizeCmsPageLinks(fxUnescape($recPage['pag_texte_' . $strLangue], 1));
$contact_promoteurs = fxShowListeEvenementsContact($arrEvenements, $strLangue);
//$strText = str_replace('%evenements%', $strListeEvenements, $strText);
@ -152,7 +152,7 @@ require_once("inc_header.php");
break;
case 'contactsend':
$arrEvenements = fxGetEvenements('', $strLangue, 1, true);
$strText = fxUnescape($recPage['pag_texte_' . $strLangue], 1);
$strText = fxNormalizeCmsPageLinks(fxUnescape($recPage['pag_texte_' . $strLangue], 1));
$strListeEvenements = fxShowListeEvenementsContact($arrEvenements, $strLangue);
$strText = str_replace('%evenements%', $strListeEvenements, $strText);
echo $strText;

View File

@ -534,15 +534,28 @@
if (races) {
races.classList.toggle('bib-ui-hidden', workspaceId !== 'preparation');
}
// MSIN-4475 — Modes d'assignation : afficher les choix tout de suite (sans clic tuile).
if (workspaceId === 'operations' && options.keepTool !== true) {
var autoTool = panel.getAttribute('data-bib-auto-tool');
if (autoTool) {
bibOpenTool(autoTool);
}
}
}
function bibWorkspaceForTool(toolId) {
if (toolId === 'event_config') {
return 'settings';
}
if (toolId === 'anomalies' || toolId === 'dist_print') {
if (toolId === 'global_batch') {
return 'operations';
}
// MSIN-4470 — L'action PDF est globale et s'ouvre depuis la barre supérieure
// sans déplacer l'utilisateur vers un autre espace de travail.
if (toolId === 'dist_print') {
return bibActiveWorkspace;
}
return 'preparation';
}
@ -625,16 +638,46 @@
}
var btn = moduleBib.querySelector('.bib-tool-btn[data-bib-tool="' + toolId + '"]');
titleEl.textContent = btn
? (btn.getAttribute('data-bib-tool-label') || '')
if (btn) {
titleEl.textContent = btn.getAttribute('data-bib-tool-label') || '';
return;
}
// MSIN-4475 — Titre depuis le panneau (outil ouvert auto, sans tuile).
var autoPanel = moduleBib.querySelector(
'[data-bib-workspace-panel][data-bib-auto-tool="' + toolId + '"]'
);
titleEl.textContent = autoPanel
? (autoPanel.getAttribute('data-bib-auto-tool-label') || '')
: '';
}
function bibCloseTool() {
// MSIN-4470 — Le panneau PDF s'ouvre immédiatement sous la barre supérieure.
function bibPositionToolDetail(toolId) {
var detail = document.getElementById('bib-tool-detail');
var defaultSlot = document.getElementById('bib-tool-detail-default-slot');
if (!detail || !defaultSlot || !defaultSlot.parentNode) {
return;
}
if (toolId === 'dist_print') {
var firstWorkspacePanel = moduleBib.querySelector('[data-bib-workspace-panel]');
if (firstWorkspacePanel && firstWorkspacePanel.parentNode) {
firstWorkspacePanel.parentNode.insertBefore(detail, firstWorkspacePanel);
}
return;
}
defaultSlot.parentNode.insertBefore(detail, defaultSlot.nextSibling);
}
function bibCloseTool(options) {
options = options || {};
if (!bibActiveTool) {
return;
}
var closedTool = bibActiveTool;
bibRestoreToolContent(bibActiveTool);
bibInvalidateToolSlot(bibActiveTool);
bibActiveTool = null;
@ -650,6 +693,14 @@
if (detail) {
detail.classList.add('bib-ui-hidden');
}
bibPositionToolDetail(null);
// MSIN-4475 — Fermer les modes (plus de tuile) ramène à la préparation.
if (options.returnToPreparation === true
&& closedTool === 'global_batch'
&& bibActiveWorkspace === 'operations') {
bibSetWorkspace('preparation', { keepTool: true });
}
}
function bibSyncToolNavBadges(toolId) {
@ -790,6 +841,7 @@
bibActiveTool = toolId;
bibSetActiveToolButton(toolId);
bibSetToolDetailTitle(toolId);
bibPositionToolDetail(toolId);
var detail = document.getElementById('bib-tool-detail');
if (detail) {
@ -1983,6 +2035,15 @@
bibOpenTool(toolBtn.getAttribute('data-bib-tool'));
});
// MSIN-4469 — Le téléchargement part dans un nouvel onglet;
// lécran principal revient aussitôt à Quantités et assignation.
moduleBib.addEventListener('submit', function (e) {
var productionForm = e.target.closest('.bib-production-return-preparation-form');
if (productionForm && moduleBib.contains(productionForm)) {
bibSetWorkspace('preparation');
}
});
moduleBib.addEventListener('click', function (e) {
var workspaceTab = e.target.closest('.bib-workspace-tab');
if (workspaceTab && moduleBib.contains(workspaceTab)) {
@ -2002,7 +2063,7 @@
if (e.target.closest('.bib-tool-detail__close')) {
e.preventDefault();
bibCloseTool();
bibCloseTool({ returnToPreparation: true });
return;
}
@ -2219,17 +2280,6 @@
function bibSyncRaceSummaryStatus(row) {
if (!row) return;
var pendingEl = row.querySelector('.bib-race-summary-pending');
var rangesEl = row.querySelector('.bib-race-summary-ranges');
var statusEl = row.querySelector('.bib-race-summary__status');
var pending = parseInt(pendingEl ? pendingEl.textContent : '0', 10) || 0;
var ranges = parseInt(rangesEl ? rangesEl.textContent : '0', 10) || 0;
var ready = pending === 0 && ranges > 0;
if (statusEl) {
statusEl.classList.toggle('is-ready', ready);
statusEl.textContent = statusEl.getAttribute(ready ? 'data-label-ready' : 'data-label-incomplete') || '';
}
bibRefreshPreparationOverview();
}
@ -2274,7 +2324,7 @@
return false;
}
// MSIN-4379 — Mise à jour affichage quantités (panneau gestion + pastille en-tête).
// MSIN-4468 — Met à jour la capacité totale éditable et la disponibilité calculée.
function bibApplyQteData(data) {
if (!data || typeof data !== 'object') {
return;
@ -2290,18 +2340,14 @@
return;
}
let elOrigine = document.getElementById('qte_origine_' + eprId);
let elAjustement = document.getElementById('qte_ajustement_' + eprId);
let elTotale = document.getElementById('qte_totale_' + eprId);
let elCourante = document.getElementById('qte_courante_' + eprId);
let elInscrits = document.getElementById('qte_inscrits_' + eprId);
let elCheckin = document.getElementById('qte_checkin_' + eprId);
let elRestante = document.getElementById('qte_restante_' + eprId);
if (elOrigine) {
elOrigine.textContent = row.qte_origine;
}
if (elAjustement) {
elAjustement.textContent = row.qte_ajustement;
if (elTotale) {
elTotale.value = row.qte_courante;
}
if (elCourante) {
elCourante.textContent = row.qte_courante;
@ -2309,11 +2355,23 @@
if (elInscrits) {
elInscrits.textContent = row.qte_inscrits;
}
let gestionPanel = document.querySelector(
'.epr-gestion-panel[data-epr-id="' + eprId + '"]'
);
if (gestionPanel && row.qte_inscrits !== undefined) {
gestionPanel.dataset.qteInscrits = row.qte_inscrits;
}
if (elCheckin) {
elCheckin.textContent = row.qte_checkin;
}
if (elRestante) {
elRestante.value = row.qte_restante;
elRestante.textContent = row.qte_restante;
}
let summaryAvailable = document.querySelector(
'.epr-row[data-epr-id="' + eprId + '"] .bib-race-summary-available'
);
if (summaryAvailable && row.qte_restante !== undefined) {
summaryAvailable.textContent = row.qte_restante;
}
let dispoBadge = document.querySelector(
@ -2325,29 +2383,42 @@
dispoBadge.textContent = label + ' : ' + row.qte_restante;
}
let btnQte = document.querySelector(
'.epr-gestion-panel[data-epr-id="' + eprId + '"] .btn_qte'
let btnQteTotal = document.querySelector(
'.epr-gestion-panel[data-epr-id="' + eprId + '"] .btn_qte_total'
);
if (btnQte) {
btnQte.setAttribute('data-qte_initiale', row.qte_restante);
if (btnQteTotal) {
btnQteTotal.setAttribute('data-qte_initiale', row.qte_courante);
}
});
}
function bibSaveQte(btn) {
function bibSaveQteTotal(btn) {
let eprId = btn.getAttribute('data-epr_id');
if (!eprId) {
return;
}
let input = document.getElementById('qte_restante_' + eprId);
let input = document.getElementById('qte_totale_' + eprId);
if (!input) {
return;
}
let intQte = parseInt(input.value, 10);
if (isNaN(intQte) || intQte < 0) {
alert(bibJs('err-generic'));
let strQteTotale = String(input.value || '').trim();
let intQteTotale = Number(strQteTotale);
if (!/^\d+$/.test(strQteTotale)
|| !Number.isSafeInteger(intQteTotale)
|| intQteTotale > 9999999) {
// MSIN-4479 — data-js-err-generic → dataset.jsErrGeneric
alert(bibJs('jsErrGeneric') || 'Erreur');
return;
}
let gestionPanel = btn.closest('.epr-gestion-panel');
let intNbInscriptions = gestionPanel
? parseInt(gestionPanel.dataset.qteInscrits, 10) || 0
: 0;
if (intQteTotale < intNbInscriptions) {
alert(bibJsFmt('jsQteTotalMin', intNbInscriptions));
return;
}
@ -2359,10 +2430,11 @@
},
credentials: 'same-origin',
body:
'a=qte'
'a=qte_total'
+ '&epr_id=' + encodeURIComponent(eprId)
+ '&qte=' + encodeURIComponent(intQte)
+ '&total=' + encodeURIComponent(intQteTotale)
+ '&lang=' + encodeURIComponent(moduleBib.dataset.lang || 'fr')
+ '&csrf_token=' + encodeURIComponent(moduleBib.dataset.csrfToken || '')
})
.then(function (res) {
return res.json();
@ -2372,10 +2444,10 @@
bibApplyQteData(result.data);
return;
}
alert(bibJs('err-generic'));
alert((result && result.message) || bibJs('jsErrGeneric') || 'Erreur');
})
.catch(function () {
alert(bibJs('server-error'));
alert(bibJs('jsServerError') || bibJs('jsErrGeneric') || 'Erreur serveur');
})
.finally(function () {
bibLoaderHide();
@ -2447,6 +2519,41 @@
}
}
/** MSIN-4467 — Accordéon du détail : au plus un panneau ouvert, mais les deux peuvent être fermés. */
function bibActivateRaceDetailBlock(block) {
if (!block) {
return;
}
let column = block.closest('.epr-col-right');
if (!column) {
return;
}
let btnToggle = block.querySelector('.epr-block-toggle');
if (!block.classList.contains('is-collapsed')) {
if (btnToggle) {
bibToggleCollapsibleBlock(block, btnToggle);
}
return;
}
column.querySelectorAll('.epr-block-gestion, .epr-block-assign').forEach(function (otherBlock) {
if (otherBlock === block || otherBlock.classList.contains('is-collapsed')) {
return;
}
let otherToggle = otherBlock.querySelector('.epr-block-toggle');
if (otherToggle) {
bibToggleCollapsibleBlock(otherBlock, otherToggle);
}
});
if (btnToggle) {
bibToggleCollapsibleBlock(block, btnToggle);
}
}
function bibCollapseGlobalBatchSection(section) {
if (!section || section.classList.contains('is-collapsed')) {
return;
@ -2506,6 +2613,15 @@
return;
}
let raceBlockHeader = e.target.closest(
'.epr-block-gestion > .epr-header--collapsible, .epr-block-assign > .epr-header--collapsible'
);
if (raceBlockHeader && moduleBib.contains(raceBlockHeader)) {
e.preventDefault();
bibActivateRaceDetailBlock(raceBlockHeader.closest('.epr-block'));
return;
}
let anomalyHdr = e.target.closest('.bib-anomaly-block-header');
if (anomalyHdr && moduleBib.contains(anomalyHdr) && !e.target.closest('.epr-block-toggle')) {
e.preventDefault();
@ -3691,7 +3807,22 @@
+ '&epr_id=' + encodeURIComponent(box.dataset.eprId)
+ '&team_mode=' + encodeURIComponent(teamRadio.value)
+ bibAjaxLangSuffix()
}, { loader: false });
}, { loader: false })
.then(function (res) { return res.json(); })
.then(function (data) {
if (!data.success) {
alert(data.message || bibJs('jsErrGeneric'));
return;
}
let row = teamRadio.closest('.epr-row');
let summary = row ? row.querySelector('.bib-race-summary-team-mode') : null;
let optionLabel = teamRadio.closest('.team-mode-option');
let optionText = optionLabel ? optionLabel.querySelector('span') : null;
if (summary && optionText) {
summary.textContent = optionText.textContent.trim();
}
});
return;
}
@ -3784,10 +3915,10 @@
moduleBib.addEventListener('click', function (e) {
let btnQte = e.target.closest('.btn_qte');
if (btnQte && moduleBib.contains(btnQte)) {
let btnQteTotal = e.target.closest('.btn_qte_total');
if (btnQteTotal && moduleBib.contains(btnQteTotal)) {
e.preventDefault();
bibSaveQte(btnQte);
bibSaveQteTotal(btnQteTotal);
return;
}

View File

@ -438,14 +438,19 @@
var $cancelBtn = $('#annuler_fiche');
if ($cancelBtn.length && $cancelBtn.is(':visible')) {
if (!$('#inscr-gestion-refund-cancel-hint').length) {
// MSIN-4419 — texte Info + marqueur crayon (rescan car injection post-AJAX).
var $hint = $('<div id="inscr-gestion-refund-cancel-hint" style="margin-top:8px;padding:10px;border:1px solid #ffe1a6;background:#fff8e6;border-radius:8px;font-size:13px;display:flex;align-items:center;flex-wrap:wrap;gap:10px;"></div>');
$hint.append($('<span></span>').text(refundCancelHint));
var $hintText = $('<span></span>').text(refundCancelHint);
$hint.append($hintText);
$hint.append(
$('<button type="button" class="btn btn-danger btn-sm rounded-0"></button>')
.text($.trim($cancelBtn.text()))
.on('click', function () { $cancelBtn.trigger('click'); })
);
$hint.insertAfter($('#inscr-gestion-refund-panel'));
if (typeof window.MS1_ADMIN_TRAD_SCAN === 'function') {
window.MS1_ADMIN_TRAD_SCAN($hintText[0]);
}
}
$cancelBtn.addClass('d-none');
}

87
kc.php
View File

@ -1,39 +1,72 @@
<?php
/**
* MSIN-3487 — Point d'entrée QR court.
*
* Modes :
* 1) Résultats événement :
* /kc.php?t=eve&id={eve_id}
* → inscriptions_evenements_extra.eve_resultats_url
* → sinon fallback https://ms1.ca
*
* 2) Key chain (porte-clés / véhicules) — inchangé :
* /kc.php?param1={pk_key_chain}
* → key_chain.key_chain_http
* → sinon insert + fallback https://ms1.ca
*/
session_start();
global $objDatabase, $vDomaine, $strLangue;
require_once "php/inc_fonctions.php";
require_once 'php/inc_fonctions.php';
if (!empty($_GET['param1']))
$key_chain_id = $_GET['param1'];
else
$key_chain_id = 0;
$defaut_landingpage="https://ms1.ca";
$strType = isset($_GET['t']) ? strtolower(trim((string) $_GET['t'])) : '';
$sqlkey_chain = "SELECT * FROM key_chain WHERE pk_key_chain = " . $key_chain_id ;
$infoCompte = $objDatabase->fxGetRow($sqlkey_chain);
if (!$infoCompte==null){
$mem_count=$infoCompte['key_chain_count']+1;
$update_plus="";
if ($infoCompte['key_chain_com_id']==0 && isset($_SESSION['com_id'])){
$update_plus="key_chain_com_id=".$_SESSION['com_id'].",";}
$sqlUpdate = "UPDATE key_chain SET ".$update_plus."key_chain_adress_ip='". $_SERVER['REMOTE_ADDR'] ."',key_chain_count = ".$mem_count." WHERE pk_key_chain = " . $key_chain_id ;
$qryUpdate = $objDatabase->fxQuery($sqlUpdate);
header("Location: ".$infoCompte['key_chain_http']);
}else{
if (isset($_SESSION['com_id'])){
$mem_com_id=$_SESSION['com_id'];
}else{
$mem_com_id=0;
// --- MSIN-3487 : redirect résultats par eve_id ---
if ($strType === 'eve') {
$intEveId = 0;
if (isset($_GET['id'])) {
$intEveId = intval($_GET['id']);
} elseif (isset($_GET['eve_id'])) {
$intEveId = intval($_GET['eve_id']);
}
header('Location: ' . fxEveExtraResolveResultatsUrl($intEveId));
exit;
}
$sqlUpdate = "insert key_chain SET key_chain_com_id=".$mem_com_id.",key_chain_timestamp=now(),key_chain_http='".$defaut_landingpage."',key_chain_adress_ip='". $_SERVER['REMOTE_ADDR'] ."',key_chain_count = 1,pk_key_chain=" . $key_chain_id;
// --- Key chain historique ---
$key_chain_id = !empty($_GET['param1']) ? intval($_GET['param1']) : 0;
$defaut_landingpage = fxEveExtraResultatsFallbackUrl();
$sqlkey_chain = 'SELECT * FROM key_chain WHERE pk_key_chain = ' . $key_chain_id;
$infoCompte = $objDatabase->fxGetRow($sqlkey_chain);
if (!$infoCompte == null) {
$mem_count = $infoCompte['key_chain_count'] + 1;
$update_plus = '';
if ($infoCompte['key_chain_com_id'] == 0 && isset($_SESSION['com_id'])) {
$update_plus = 'key_chain_com_id=' . intval($_SESSION['com_id']) . ',';
}
$sqlUpdate = 'UPDATE key_chain SET ' . $update_plus
. "key_chain_adress_ip='" . $objDatabase->fxEscape($_SERVER['REMOTE_ADDR']) . "',"
. 'key_chain_count = ' . intval($mem_count)
. ' WHERE pk_key_chain = ' . $key_chain_id;
$qryUpdate = $objDatabase->fxQuery($sqlUpdate);
header("Location: $defaut_landingpage" );
header('Location: ' . $infoCompte['key_chain_http']);
} else {
if (isset($_SESSION['com_id'])) {
$mem_com_id = intval($_SESSION['com_id']);
} else {
$mem_com_id = 0;
}
$sqlUpdate = 'INSERT key_chain SET key_chain_com_id=' . $mem_com_id
. ',key_chain_timestamp=now(),'
. "key_chain_http='" . $objDatabase->fxEscape($defaut_landingpage) . "',"
. "key_chain_adress_ip='" . $objDatabase->fxEscape($_SERVER['REMOTE_ADDR']) . "',"
. 'key_chain_count = 1,pk_key_chain=' . $key_chain_id;
$qryUpdate = $objDatabase->fxQuery($sqlUpdate);
header('Location: ' . $defaut_landingpage);
}
exit;

File diff suppressed because it is too large Load Diff

View File

@ -48,6 +48,22 @@ function fxChronotrackApiNormalizeEntityList($mixPayload, $strEntityKey) {
}
}
// MSIN-4328 — clé CT pour reg_choice (souvent event_reg_choice)
if ($strEntityKey === 'reg_choice') {
foreach (array('event_reg_choice', 'registration_choice') 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) {
@ -235,6 +251,7 @@ function fxChronotrackApiFormatRaceRegChoicesLabel($arrEntity) {
}
function fxChronotrackApiFormatRaceLabel($arrEntity) {
// MSIN-4328 — nom de race seul ; le reg_choice est ajouté au mapping via ApplyRegChoiceLabels
$strName = fxChronotrackApiRaceField($arrEntity, array('race_name', 'display_name', 'name', 'title'));
if ($strName !== '') {
return $strName;
@ -244,24 +261,15 @@ function fxChronotrackApiFormatRaceLabel($arrEntity) {
$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) {
if ($strTag !== '') {
$arrExtras[] = 'tag ' . $strTag;
}
if ($strType !== '') {
$arrExtras[] = $strType;
}
if ($strDist !== '' && $strDist !== $strName) {
if ($strDist !== '') {
$arrExtras[] = $strDist;
}
if ($strTeam !== '') {
@ -269,10 +277,7 @@ function fxChronotrackApiFormatRaceLabel($arrEntity) {
}
$arrExtras = array_values(array_unique($arrExtras));
if ($strName === '') {
$strName = $strDist !== '' ? $strDist : ('Race ' . fxChronotrackApiEntityId($arrEntity));
}
$strName = $strDist !== '' ? $strDist : ('Race ' . fxChronotrackApiEntityId($arrEntity));
if (count($arrExtras) === 0) {
return $strName;
@ -281,6 +286,154 @@ function fxChronotrackApiFormatRaceLabel($arrEntity) {
return $strName . ' — ' . implode(' · ', $arrExtras);
}
/**
* MSIN-4328 — Fusionne deux index race_id → noms reg_choice.
*
* @param array<int, string[]> $arrA
* @param array<int, string[]> $arrB
* @return array<int, string[]>
*/
function fxChronotrackApiMergeRegChoiceNamesByRaceId($arrA, $arrB) {
if (!is_array($arrA)) {
$arrA = array();
}
if (!is_array($arrB) || count($arrB) === 0) {
return $arrA;
}
foreach ($arrB as $intRaceId => $arrNames) {
$intRaceId = intval($intRaceId);
if ($intRaceId <= 0 || !is_array($arrNames)) {
continue;
}
if (!isset($arrA[$intRaceId])) {
$arrA[$intRaceId] = array();
}
foreach ($arrNames as $strName) {
$strName = trim((string)$strName);
if ($strName === '') {
continue;
}
$arrA[$intRaceId][] = $strName;
}
$arrA[$intRaceId] = array_values(array_unique($arrA[$intRaceId]));
}
return $arrA;
}
/**
* MSIN-4328 — Indexe les noms de reg_choice par race_id parent (payload event/race).
*
* @return array<int, string[]>
*/
function fxChronotrackApiRegChoiceNamesByRaceId($mixPayload) {
$arrByRace = array();
foreach (fxChronotrackApiCollectRegChoiceEntities($mixPayload) as $arrChoice) {
if (!is_array($arrChoice)) {
continue;
}
$intParent = intval(fxChronotrackApiRaceField($arrChoice, array('race_id', 'event_race_id')));
if ($intParent <= 0 && isset($arrChoice['race']) && is_array($arrChoice['race'])) {
$intParent = intval(fxChronotrackApiEntityId($arrChoice['race']));
}
if ($intParent <= 0) {
continue;
}
$strName = fxChronotrackApiRaceField($arrChoice, array(
'reg_choice_name', 'registration_choice_name', 'choice_name', 'display_name', 'name', 'title',
));
if ($strName === '' && isset($arrChoice['reg_choice_id']) && $arrChoice['reg_choice_id'] !== '') {
$strName = 'reg_choice ' . $arrChoice['reg_choice_id'];
}
if ($strName === '') {
continue;
}
if (!isset($arrByRace[$intParent])) {
$arrByRace[$intParent] = array();
}
$arrByRace[$intParent][] = $strName;
}
foreach ($arrByRace as $intRaceId => $arrNames) {
$arrByRace[$intRaceId] = array_values(array_unique($arrNames));
}
return $arrByRace;
}
/**
* MSIN-4328 — Filet : déduit race_id → reg_choice_name depuis les entries CT.
*
* @return array<int, string[]>
*/
function fxChronotrackApiRegChoiceNamesFromEntriesPayload($mixPayload) {
$arrByRace = array();
$arrEntries = fxChronotrackApiNormalizeEntityList($mixPayload, 'entry');
if (count($arrEntries) === 0 && is_array($mixPayload) && isset($mixPayload['event']) && is_array($mixPayload['event'])) {
$arrEntries = fxChronotrackApiNormalizeEntityList($mixPayload['event'], 'entry');
}
foreach ($arrEntries as $arrEntry) {
if (!is_array($arrEntry)) {
continue;
}
$intRaceId = intval(fxChronotrackApiRaceField($arrEntry, array('race_id', 'event_race_id')));
if ($intRaceId <= 0) {
continue;
}
$strName = fxChronotrackApiRaceField($arrEntry, array(
'reg_choice_name', 'registration_choice_name', 'choice_name',
));
if ($strName === '') {
continue;
}
if (!isset($arrByRace[$intRaceId])) {
$arrByRace[$intRaceId] = array();
}
$arrByRace[$intRaceId][] = $strName;
}
foreach ($arrByRace as $intRaceId => $arrNames) {
$arrByRace[$intRaceId] = array_values(array_unique($arrNames));
}
return $arrByRace;
}
/**
* MSIN-4328 — Ajoute « (reg_choice) » au label de chaque course du mapping.
*/
function fxChronotrackApiApplyRegChoiceLabelsToRaceRows(&$arrRaceRows, $arrNamesByRaceId) {
if (!is_array($arrRaceRows) || !is_array($arrNamesByRaceId)) {
return;
}
foreach ($arrRaceRows as &$arrRow) {
if (!is_array($arrRow)) {
continue;
}
$intId = intval($arrRow['ct_race_id'] ?? 0);
$strRc = '';
if ($intId > 0 && !empty($arrNamesByRaceId[$intId]) && is_array($arrNamesByRaceId[$intId])) {
$strRc = implode(', ', $arrNamesByRaceId[$intId]);
}
if ($strRc === '' && isset($arrRow['raw']) && is_array($arrRow['raw'])) {
$strRc = fxChronotrackApiFormatRaceRegChoicesLabel($arrRow['raw']);
if ($strRc === '') {
$strRc = fxChronotrackApiRaceField($arrRow['raw'], array(
'reg_choice_name', 'registration_choice_name', 'choice_name', 'reg_choice_label',
));
}
}
if ($strRc === '') {
continue;
}
$arrRow['reg_choice_label'] = $strRc;
$strBase = isset($arrRow['name']) && $arrRow['name'] !== ''
? $arrRow['name']
: (isset($arrRow['label']) ? $arrRow['label'] : 'Course');
// Évite double parenthèses si FormatRaceLabel a déjà collé le même texte
$strExpected = $strBase . ' (' . $strRc . ')';
if (!isset($arrRow['label']) || $arrRow['label'] !== $strExpected) {
$arrRow['label'] = $strExpected;
}
}
unset($arrRow);
}
function fxChronotrackApiEntityStartEpoch($arrEntity) {
foreach (array(
'race_planned_start_time', 'race_start_time', 'event_start_time',
@ -625,6 +778,29 @@ function fxChronotrackApiFetchRacesForEvent($intCtEventId) {
}
$arrRaceRows = fxChronotrackApiParseRaceRowsFromPayload($arrApi['json'], $intCtEventId);
// MSIN-4328 — enrichir libellés dropdown CT : race_name (reg_choice)
$arrNamesByRace = fxChronotrackApiRegChoiceNamesByRaceId($arrApi['json']);
if (count($arrRaceRows) > 0) {
$arrEventApi = fxChronotrackApiOAuthApiGet('event/' . $intCtEventId);
if (($arrEventApi['state'] ?? '') === 'ok') {
$arrNamesByRace = fxChronotrackApiMergeRegChoiceNamesByRaceId(
$arrNamesByRace,
fxChronotrackApiRegChoiceNamesByRaceId($arrEventApi['json'] ?? null)
);
}
// Filet : noms visibles CT souvent présents sur les entries
if (count($arrNamesByRace) === 0) {
$arrEntryApi = fxChronotrackApiOAuthApiGet('event/' . $intCtEventId . '/entry');
if (($arrEntryApi['state'] ?? '') === 'ok') {
$arrNamesByRace = fxChronotrackApiRegChoiceNamesFromEntriesPayload(
$arrEntryApi['json'] ?? null
);
}
}
}
fxChronotrackApiApplyRegChoiceLabelsToRaceRows($arrRaceRows, $arrNamesByRace);
if (count($arrRaceRows) > 0) {
return array(
'state' => 'ok',

File diff suppressed because it is too large Load Diff

View File

@ -12,6 +12,7 @@ include_once APPLICATION_PATH . 'php/inc_mysql.php';
include_once APPLICATION_PATH . 'php/inc_dates.php';
include_once APPLICATION_PATH . 'php/inc_mail.php';
include_once APPLICATION_PATH . 'php/inc_fx_memberships.php';
include_once APPLICATION_PATH . 'php/inc_fx_eve_extra.php'; // MSIN-3487 — extra événement (lien résultats, etc.)
include_once APPLICATION_PATH . 'php/inc_fx_liste_attente.php';// inclure les fonctions des abonemennts
include_once APPLICATION_PATH . 'php/recaptcha.class.php';
@ -368,6 +369,44 @@ function fxGetPage($code)
return $recPage;
}
/**
* Corrige les liens internes CMS issus de TinyMCE (relative_urls).
* Ex. href="page/cookie/fr" ou href="page/confidentialite/page/cookie/fr."
* → /page/cookie/fr. Ne touche pas http(s), mailto, #, ni les /page/ déjà corrects.
*/
function fxNormalizeCmsPageLinks($strHtml)
{
if ($strHtml === null || $strHtml === '') {
return $strHtml;
}
return preg_replace_callback(
'#\bhref=(["\'])([^"\']*)\1#i',
function ($arrMatch) {
$strQuote = $arrMatch[1];
$strUrl = rtrim($arrMatch[2], '.');
if ($strUrl === ''
|| $strUrl[0] === '#'
|| stripos($strUrl, 'mailto:') === 0
|| preg_match('#^(?:https?:)?//#i', $strUrl)
) {
return $arrMatch[0];
}
// Dernier segment page/{code}/{fr|en} (y compris chemins imbriqués TinyMCE)
if (preg_match('#(?:^|/)page/([\w\-]+)/(fr|en)/?$#i', $strUrl, $arrPage)
&& preg_match('#(^|/)page/#i', $strUrl)
) {
return 'href=' . $strQuote . '/page/' . $arrPage[1] . '/' . strtolower($arrPage[2]) . $strQuote;
}
return $arrMatch[0];
},
$strHtml
);
}
// fonction qui détermine le url de retour (JSP)
// créé : 2012-07-02
function fxGetReferer()
@ -386,6 +425,57 @@ function fxGetReferer()
return $url_retour;
}
/**
* MSIN-4474 — URL de retour après modif inscription (même hôte uniquement).
* Préserve la liste d'origine (ex. inscriptions_gestion) via GET/POST return_url.
*/
function fxSafeReturnUrl($strUrl, $strFallback = '')
{
global $vDomaine;
$strUrl = trim((string)$strUrl);
$strFallback = trim((string)$strFallback);
if ($strUrl === '') {
return $strFallback;
}
$arrDom = parse_url($vDomaine);
$arrUrl = parse_url($strUrl);
if ($arrUrl === false || empty($arrUrl['host']) || empty($arrDom['host'])) {
return $strFallback;
}
if (stripos($arrUrl['host'], $arrDom['host']) === false) {
return $strFallback;
}
return $strUrl;
}
/**
* MSIN-4474 — Résout l'URL de retour pour le formulaire modifier (promoteur/client).
* Ignore le referer s'il pointe déjà vers reserver/book (après Suivant / switch).
*/
function fxModInscriptionReturnUrl($strFallback = '')
{
$strCandidate = '';
if (!empty($_POST['return_url'])) {
$strCandidate = $_POST['return_url'];
} elseif (!empty($_GET['return_url'])) {
$strCandidate = $_GET['return_url'];
} elseif (!empty($_SERVER['HTTP_REFERER'])) {
$strRef = (string)$_SERVER['HTTP_REFERER'];
if (!preg_match('#/(reserver|book)(/|\\?|$)#i', $strRef)) {
$strCandidate = $strRef;
}
}
return fxSafeReturnUrl($strCandidate, $strFallback);
}
// fonction pour un message via la page contact (JSP)
// param : données post, langue
// créé : 2012-07-03

View File

@ -463,11 +463,13 @@ function fxAdminTradMarkerScriptHtml() {
textNode.parentNode.replaceChild(frag, textNode);
}
function scan() {
if (!document.body) {
// root optionnel : rescan après HTML dynamique (ex. hint remboursement AJAX).
function scan(root) {
var start = root && root.nodeType ? root : document.body;
if (!start) {
return;
}
var walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, null);
var walker = document.createTreeWalker(start, NodeFilter.SHOW_TEXT, null);
var nodes = [];
while (walker.nextNode()) {
nodes.push(walker.currentNode);
@ -475,8 +477,10 @@ function fxAdminTradMarkerScriptHtml() {
nodes.forEach(replaceMarkers);
}
window.MS1_ADMIN_TRAD_SCAN = scan;
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", scan);
document.addEventListener("DOMContentLoaded", function () { scan(); });
} else {
scan();
}

View File

@ -142,6 +142,43 @@ function fxBibFreeDuplicateRisk($int_eve_id, $tabPlan, $intStart = 0, $intEnd =
];
}
/**
* MSIN-4445 — Valide et ordonne les epr_id du bloc libre (solo, avec ou sans séquence).
*
* @return int[]
*/
function fxBibParseGlobalBatchFreeEprIds($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;
}
$tabWanted[$epr_id] = true;
}
if (empty($tabWanted)) {
return [];
}
$tabIds = array_keys($tabWanted);
sort($tabIds, SORT_NUMERIC);
return $tabIds;
}
/**
* 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.
@ -152,7 +189,7 @@ function fxBibBuildFreeRangePlan($int_eve_id, $tabEprIds, $intStart, $intEnd, $s
$int_eve_id = (int)$int_eve_id;
$intStart = (int)$intStart;
$intEnd = (int)$intEnd;
$tabEprIds = fxBibParseGlobalBatchGeneratedEprIds($tabEprIds, $int_eve_id);
$tabEprIds = fxBibParseGlobalBatchFreeEprIds($tabEprIds, $int_eve_id);
$intCursor = $intStart;
$tabPlan = [];
@ -242,13 +279,7 @@ function fxBibValidateFreeRangePlan($int_eve_id, $tabPlan, $intStart, $intEnd, $
];
}
if (fxBibEpreuveHasAnySequence($epr_id)) {
return [
'success' => false,
'message' => fxBibMsg('bib_v4_global_batch_gen_has_seq'),
];
}
// MSIN-4445 — OK même si une séquence existe déjà (aucune création de séquence).
$blnHasAssignable = true;
}

View File

@ -25,11 +25,12 @@ function fxBibEpreuveHasAnySequence($epr_id) {
}
/**
* Épreuves solo sans aucune séquence, triées par epr_id (ordre page).
* Épreuves solo actives (avec ou sans séquence), triées par epr_id.
* MSIN-4445 — utilisé par l'assignation libre.
*
* @return array[] lignes inscriptions_epreuves
*/
function fxBibGetSoloEpreuvesWithoutSequence($int_eve_id, $tabEpreuves = null) {
function fxBibGetSoloEpreuves($int_eve_id, $tabEpreuves = null) {
global $objDatabase;
$int_eve_id = (int)$int_eve_id;
@ -52,6 +53,21 @@ function fxBibGetSoloEpreuvesWithoutSequence($int_eve_id, $tabEpreuves = null) {
continue;
}
$out[] = $epr;
}
return $out;
}
/**
* É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) {
$out = [];
foreach (fxBibGetSoloEpreuves($int_eve_id, $tabEpreuves) as $epr) {
if (fxBibEpreuveHasAnySequence((int)$epr['epr_id'])) {
continue;
}
@ -90,10 +106,16 @@ 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. */
/** MSIN-4445 — Bloc libre : au moins 1 épreuve solo (avec ou sans séquence). */
function fxBibGlobalBatchHasFreeBlock($int_eve_id, $tabEpreuves) {
return count(fxBibGetSoloEpreuves($int_eve_id, $tabEpreuves)) >= 1;
}
/** Tuile / coquille assignation globale visible si l'un des blocs est utilisable. */
function fxBibGlobalBatchToolIsAvailable($int_eve_id, $tabEpreuves) {
return fxBibGlobalBatchHasExistingBlock($int_eve_id, $tabEpreuves)
|| fxBibGlobalBatchHasGeneratedBlock($int_eve_id, $tabEpreuves);
|| fxBibGlobalBatchHasGeneratedBlock($int_eve_id, $tabEpreuves)
|| fxBibGlobalBatchHasFreeBlock($int_eve_id, $tabEpreuves);
}
/**
@ -266,6 +288,13 @@ function fxBibCreateGeneratedRanges($int_eve_id, $tabPlan, $strLangue = 'fr') {
$int_eve_id = (int)$int_eve_id;
$tabCreated = [];
if (fxBibProductionEventIsLocked($int_eve_id)) {
return [
'success' => false,
'message' => fxBibMsg('bib_v5_production_locked_error'),
];
}
foreach ($tabPlan as $item) {
if (!empty($item['skip'])) {
continue;

View File

@ -0,0 +1,926 @@
<?php
/**
* MSIN-4469 — Production Excel des dossards.
*
* Une commande active représente un instantané immuable des séquences. Le fichier
* peut être téléchargé de nouveau, mais une seconde génération est refusée tant
* que la commande active n'a pas été remplacée par un futur garde-fou.
*/
function fxBibProductionTemplatePath() {
return __DIR__ . DIRECTORY_SEPARATOR . 'templates'
. DIRECTORY_SEPARATOR . 'MSIN-4469-commande-dossards.xlsx';
}
function fxBibProductionStorageRoot() {
return dirname(__DIR__) . DIRECTORY_SEPARATOR . 'data'
. DIRECTORY_SEPARATOR . 'bib-production';
}
function fxBibProductionGetActiveOrder($intEveId) {
global $objDatabase;
$intEveId = (int)$intEveId;
if ($intEveId <= 0) {
return null;
}
return $objDatabase->fxGetRow(
"SELECT *
FROM inscriptions_bib_production_orders
WHERE eve_id = $intEveId
AND bpo_active_key = 1
LIMIT 1"
);
}
function fxBibProductionGetOrder($intOrderId, $intEveId = 0) {
global $objDatabase;
$intOrderId = (int)$intOrderId;
$intEveId = (int)$intEveId;
if ($intOrderId <= 0) {
return null;
}
$strWhereEve = $intEveId > 0 ? " AND eve_id = $intEveId" : '';
return $objDatabase->fxGetRow(
"SELECT *
FROM inscriptions_bib_production_orders
WHERE bpo_id = $intOrderId
$strWhereEve
LIMIT 1"
);
}
function fxBibProductionEventIsLocked($intEveId) {
return fxBibProductionGetActiveOrder($intEveId) !== null;
}
/**
* MSIN-4469 — Déverrouillage temporaire (dev) : même droit que la génération
* (promoteur avec dossards.manage, ou superadmin).
*/
function fxBibProductionDevelopmentUnlock($intEveId) {
global $objDatabase;
$intEveId = (int)$intEveId;
if ($intEveId <= 0 || !fxBibProductionUserCanManageEvent($intEveId)) {
return ['success' => false, 'message' => fxBibMsg('bib_v4_ajax_unauthorized')];
}
$tabOrder = fxBibProductionGetActiveOrder($intEveId);
if (!$tabOrder) {
return ['success' => true];
}
$strBy = fxBibGetCurrentPromoteurDisplayName();
$objDatabase->fxQuery(
"UPDATE inscriptions_bib_production_orders
SET bpo_status = 'development_unlocked',
bpo_active_key = NULL,
bpo_superseded_at = NOW(),
bpo_superseded_by = '" . $objDatabase->fxEscape($strBy) . "',
bpo_superseded_reason = 'MSIN-4469 development unlock'
WHERE bpo_id = " . (int)$tabOrder['bpo_id'] . "
AND bpo_active_key = 1"
);
return ['success' => true];
}
function fxBibProductionEpreuveIsLocked($intEprId) {
global $objDatabase;
$intEprId = (int)$intEprId;
if ($intEprId <= 0) {
return false;
}
$intEveId = (int)$objDatabase->fxGetVar(
"SELECT eve_id FROM inscriptions_epreuves WHERE epr_id = $intEprId LIMIT 1"
);
return fxBibProductionEventIsLocked($intEveId);
}
/**
* Vérifie l'accès réel à l'événement, en plus de la session et du jeton CSRF.
*/
function fxBibProductionUserCanManageEvent($intEveId) {
$intEveId = (int)$intEveId;
if ($intEveId <= 0 || !fxBibRequirePromoteurSession()) {
return false;
}
if (!empty($_SESSION['usa_id'])) {
return true;
}
$intComId = (int)($_SESSION['com_id'] ?? 0);
return $intComId > 0
&& function_exists('fxEveAccesHasPermission')
&& fxEveAccesHasPermission($intComId, $intEveId, 'dossards.manage');
}
/**
* Charge les épreuves actives et toutes leurs séquences, dans l'ordre d'affichage.
*/
function fxBibProductionCollectSnapshot($intEveId, $strLangue = 'fr') {
global $objDatabase;
$intEveId = (int)$intEveId;
$strLangue = in_array($strLangue, ['fr', 'en'], true) ? $strLangue : 'fr';
$tabEvent = $objDatabase->fxGetRow(
"SELECT eve_id, eve_nom_fr, eve_nom_en
FROM inscriptions_evenements
WHERE eve_id = $intEveId
LIMIT 1"
);
if (!$tabEvent) {
return ['success' => false, 'message' => fxBibMsg('bib_v5_production_event_invalid')];
}
$tabEpreuves = $objDatabase->fxGetResults(
"SELECT *
FROM inscriptions_epreuves
WHERE eve_id = $intEveId
AND epr_actif = 1
ORDER BY epr_tri, epr_id"
);
if (!is_array($tabEpreuves)) {
$tabEpreuves = [];
}
$tabRaces = [];
$intBibCount = 0;
$intRangeCount = 0;
foreach ($tabEpreuves as $tabEpreuve) {
$intEprId = (int)($tabEpreuve['epr_id'] ?? 0);
if ($intEprId <= 0) {
continue;
}
// MSIN-4469 — La commande porte sur toute la séquence à imprimer,
// même lorsquaucun coureur ni aucun dossard assigné ne sy trouve encore.
$tabRangesRaw = $objDatabase->fxGetResults(
"SELECT epr_bib_id, epr_bib_start, epr_bib_finish
FROM inscriptions_epreuves_bib
WHERE epr_id = $intEprId
ORDER BY epr_bib_start, epr_bib_id"
);
if (!is_array($tabRangesRaw)) {
continue;
}
$tabRanges = [];
foreach ($tabRangesRaw as $tabRange) {
$intStart = (int)($tabRange['epr_bib_start'] ?? 0);
$intFinish = (int)($tabRange['epr_bib_finish'] ?? 0);
if ($intStart <= 0 || $intFinish < $intStart) {
return ['success' => false, 'message' => fxBibMsg('bib_v5_production_range_invalid')];
}
$intQuantity = $intFinish - $intStart + 1;
$intBibCount += $intQuantity;
$intRangeCount++;
$tabRanges[] = [
'epr_bib_id' => (int)$tabRange['epr_bib_id'],
'start' => $intStart,
'finish' => $intFinish,
'quantity' => $intQuantity,
];
}
if (!empty($tabRanges)) {
$tabRaces[] = [
'epr_id' => $intEprId,
// Le libellé type/catégorie est le nom court retenu pour la V1.
'label' => fxBibEpreuveDisplayName($tabEpreuve, $strLangue),
'ranges' => $tabRanges,
];
}
}
if ($intRangeCount === 0) {
return ['success' => false, 'message' => fxBibMsg('bib_v5_production_no_ranges')];
}
return [
'success' => true,
'snapshot' => [
'event' => [
'eve_id' => $intEveId,
'name' => trim((string)($tabEvent['eve_nom_' . $strLangue] ?? '')),
],
'language' => $strLangue,
'bib_count' => $intBibCount,
'range_count' => $intRangeCount,
'races' => $tabRaces,
],
];
}
function fxBibProductionSafeFilePart($strValue) {
$strValue = trim((string)$strValue);
if (function_exists('iconv')) {
$strAscii = @iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $strValue);
if ($strAscii !== false) {
$strValue = $strAscii;
}
}
$strValue = strtolower($strValue);
$strValue = preg_replace('/[^a-z0-9]+/', '-', $strValue);
$strValue = trim((string)$strValue, '-');
return $strValue !== '' ? $strValue : 'evenement';
}
function fxBibProductionEnsureDirectory($strDirectory) {
if (is_dir($strDirectory)) {
return true;
}
return @mkdir($strDirectory, 0770, true) || is_dir($strDirectory);
}
function fxBibProductionXmlLoad($strXml) {
$objDom = new DOMDocument('1.0', 'UTF-8');
$objDom->preserveWhiteSpace = false;
$objDom->formatOutput = false;
if (!@$objDom->loadXML($strXml)) {
throw new Exception('MSIN-4469 invalid OpenXML part');
}
return $objDom;
}
/** @return DOMElement|null */
function fxBibProductionXmlFindRow(DOMXPath $objXpath, $intRow) {
$objRow = $objXpath->query('//m:sheetData/m:row[@r="' . (int)$intRow . '"]')->item(0);
return $objRow instanceof DOMElement ? $objRow : null;
}
function fxBibProductionXmlClearCell(DOMElement $objCell) {
while ($objCell->firstChild) {
$objCell->removeChild($objCell->firstChild);
}
$objCell->removeAttribute('t');
}
function fxBibProductionXmlFindCell(DOMElement $objRow, $strColumn) {
foreach ($objRow->childNodes as $objChild) {
if ($objChild instanceof DOMElement && $objChild->localName === 'c') {
$strRef = $objChild->getAttribute('r');
if (preg_replace('/[0-9]+/', '', $strRef) === $strColumn) {
return $objChild;
}
}
}
return null;
}
function fxBibProductionXmlEnsureCell(
DOMDocument $objDom,
DOMElement $objRow,
$strColumn,
$intRow,
$strStyle = ''
) {
$objCell = fxBibProductionXmlFindCell($objRow, $strColumn);
if (!$objCell) {
$objCell = $objDom->createElementNS(
'http://schemas.openxmlformats.org/spreadsheetml/2006/main',
'c'
);
$objRow->appendChild($objCell);
}
$objCell->setAttribute('r', $strColumn . (int)$intRow);
if ($strStyle !== '') {
$objCell->setAttribute('s', $strStyle);
}
return $objCell;
}
function fxBibProductionXmlSetInlineString(
DOMDocument $objDom,
DOMElement $objRow,
$strColumn,
$intRow,
$strValue,
$strStyle = ''
) {
$strNs = 'http://schemas.openxmlformats.org/spreadsheetml/2006/main';
$objCell = fxBibProductionXmlEnsureCell($objDom, $objRow, $strColumn, $intRow, $strStyle);
fxBibProductionXmlClearCell($objCell);
$objCell->setAttribute('t', 'inlineStr');
$objInline = $objDom->createElementNS($strNs, 'is');
$objText = $objDom->createElementNS($strNs, 't');
$objText->appendChild($objDom->createTextNode((string)$strValue));
$objInline->appendChild($objText);
$objCell->appendChild($objInline);
}
function fxBibProductionXmlSetNumber(
DOMDocument $objDom,
DOMElement $objRow,
$strColumn,
$intRow,
$mixValue,
$strStyle = ''
) {
$strNs = 'http://schemas.openxmlformats.org/spreadsheetml/2006/main';
$objCell = fxBibProductionXmlEnsureCell($objDom, $objRow, $strColumn, $intRow, $strStyle);
fxBibProductionXmlClearCell($objCell);
$objValue = $objDom->createElementNS($strNs, 'v');
$objValue->appendChild($objDom->createTextNode((string)$mixValue));
$objCell->appendChild($objValue);
}
function fxBibProductionXmlSetFormula(
DOMDocument $objDom,
DOMElement $objRow,
$strColumn,
$intRow,
$strFormula,
$strStyle = '',
$mixCachedValue = null
) {
$strNs = 'http://schemas.openxmlformats.org/spreadsheetml/2006/main';
$objCell = fxBibProductionXmlEnsureCell($objDom, $objRow, $strColumn, $intRow, $strStyle);
fxBibProductionXmlClearCell($objCell);
$objFormula = $objDom->createElementNS($strNs, 'f');
$objFormula->appendChild($objDom->createTextNode($strFormula));
$objCell->appendChild($objFormula);
// MSIN-4469 — Valeur calculée en cache : sinon Excel/LibreOffice peut
// afficher le TOTAL vide tant que le recalcul n'a pas tourné.
if ($mixCachedValue !== null && $mixCachedValue !== '') {
$objValue = $objDom->createElementNS($strNs, 'v');
$objValue->appendChild($objDom->createTextNode((string)$mixCachedValue));
$objCell->appendChild($objValue);
}
}
function fxBibProductionXmlCloneRow(DOMElement $objTemplateRow, $intTargetRow) {
$objRow = $objTemplateRow->cloneNode(true);
$objRow->setAttribute('r', (string)$intTargetRow);
foreach ($objRow->childNodes as $objChild) {
if ($objChild instanceof DOMElement && $objChild->localName === 'c') {
$strColumn = preg_replace('/[0-9]+/', '', $objChild->getAttribute('r'));
$objChild->setAttribute('r', $strColumn . $intTargetRow);
fxBibProductionXmlClearCell($objChild);
}
}
return $objRow;
}
function fxBibProductionXmlShiftReference($strReference, $intFromRow, $intOffset) {
return preg_replace_callback(
'/([A-Z]+)([0-9]+)/',
function ($tabMatch) use ($intFromRow, $intOffset) {
$intRow = (int)$tabMatch[2];
if ($intRow >= $intFromRow) {
$intRow += $intOffset;
}
return $tabMatch[1] . $intRow;
},
$strReference
);
}
function fxBibProductionBuildSupplierXml($strXml, array $tabSnapshot, array $tabLabels) {
$objDom = fxBibProductionXmlLoad($strXml);
$objXpath = new DOMXPath($objDom);
$objXpath->registerNamespace('m', 'http://schemas.openxmlformats.org/spreadsheetml/2006/main');
$objSheetData = $objXpath->query('//m:sheetData')->item(0);
$objHeaderTemplate = fxBibProductionXmlFindRow($objXpath, 1);
$objDataTemplate = fxBibProductionXmlFindRow($objXpath, 2);
if (!$objSheetData || !$objHeaderTemplate || !$objDataTemplate) {
throw new Exception('MSIN-4469 invalid supplier template');
}
while ($objSheetData->firstChild) {
$objSheetData->removeChild($objSheetData->firstChild);
}
// MSIN-4469 — Convention imprimeur : cellules bleues = imprimées sur le dossard.
// Colonne A (numéro) = bleu ; colonne B (épreuve) = neutre (info, non imprimée).
$objHeader = fxBibProductionXmlCloneRow($objHeaderTemplate, 1);
$strHeaderStyleA = (string)(fxBibProductionXmlFindCell($objHeader, 'A')->getAttribute('s'));
$strHeaderStyleB = (string)(fxBibProductionXmlFindCell($objHeader, 'B')->getAttribute('s'));
fxBibProductionXmlSetInlineString($objDom, $objHeader, 'A', 1, $tabLabels['bib'], $strHeaderStyleA);
fxBibProductionXmlSetInlineString($objDom, $objHeader, 'B', 1, $tabLabels['race'], $strHeaderStyleB);
$objSheetData->appendChild($objHeader);
$strDataStyleA = (string)(fxBibProductionXmlFindCell($objDataTemplate, 'A')->getAttribute('s'));
$strDataStyleB = (string)(fxBibProductionXmlFindCell($objDataTemplate, 'B')->getAttribute('s'));
$intRow = 2;
foreach ($tabSnapshot['races'] as $intRaceIndex => $tabRace) {
if ($intRaceIndex > 0) {
$intRow++;
}
foreach ($tabRace['ranges'] as $tabRange) {
for ($intBib = (int)$tabRange['start']; $intBib <= (int)$tabRange['finish']; $intBib++) {
$objRow = fxBibProductionXmlCloneRow($objDataTemplate, $intRow);
fxBibProductionXmlSetNumber($objDom, $objRow, 'A', $intRow, $intBib, $strDataStyleA);
fxBibProductionXmlSetInlineString(
$objDom,
$objRow,
'B',
$intRow,
$tabRace['label'],
$strDataStyleB
);
$objSheetData->appendChild($objRow);
$intRow++;
}
}
}
$objDimension = $objXpath->query('//m:dimension')->item(0);
if ($objDimension instanceof DOMElement) {
$objDimension->setAttribute('ref', 'A1:B' . max(1, $intRow - 1));
}
return $objDom->saveXML();
}
function fxBibProductionBuildOrderXml($strXml, array $tabSnapshot, array $tabLabels) {
$objDom = fxBibProductionXmlLoad($strXml);
$objXpath = new DOMXPath($objDom);
$objXpath->registerNamespace('m', 'http://schemas.openxmlformats.org/spreadsheetml/2006/main');
$objSheetData = $objXpath->query('//m:sheetData')->item(0);
$objBaseEvent = fxBibProductionXmlFindRow($objXpath, 4);
$objBaseSecond = fxBibProductionXmlFindRow($objXpath, 5);
$objBaseBlank = fxBibProductionXmlFindRow($objXpath, 6);
$objTotal = fxBibProductionXmlFindRow($objXpath, 31);
if (!$objSheetData || !$objBaseEvent || !$objBaseSecond || !$objTotal) {
throw new Exception('MSIN-4469 invalid order template');
}
if (!$objBaseBlank) {
$objBaseBlank = $objBaseSecond;
}
$tabOrderRows = [];
foreach ($tabSnapshot['races'] as $tabRace) {
foreach ($tabRace['ranges'] as $intRangeIndex => $tabRange) {
$tabOrderRows[] = [
// MSIN-4470 — Une ligne reste réservée à chaque séquence,
// mais le nom de l'épreuve n'est répété que sur la première.
'label' => $intRangeIndex === 0 ? $tabRace['label'] : '',
'start' => (int)$tabRange['start'],
'finish' => (int)$tabRange['finish'],
'quantity' => (int)$tabRange['quantity'],
];
}
}
// MSIN-4469 — Le gabarit a 9 emplacements ; on n'en garde que le nombre
// réel de séquences (sinon lignes jaunes vides avant le TOTAL).
$intTemplateSlots = 9;
$intSlotCount = count($tabOrderRows);
$intRowDelta = ($intSlotCount - $intTemplateSlots) * 3;
$intTotalRow = 31 + $intRowDelta;
$tabRemove = [];
foreach ($objXpath->query('//m:sheetData/m:row') as $objRow) {
if (!($objRow instanceof DOMElement)) {
continue;
}
$intCurrentRow = (int)$objRow->getAttribute('r');
if ($intCurrentRow >= 4 && $intCurrentRow <= 30) {
$tabRemove[] = $objRow;
}
}
foreach ($tabRemove as $objRow) {
$objSheetData->removeChild($objRow);
}
if ($intRowDelta !== 0) {
foreach ($objXpath->query('//m:sheetData/m:row') as $objRow) {
if (!($objRow instanceof DOMElement)) {
continue;
}
$intCurrentRow = (int)$objRow->getAttribute('r');
if ($intCurrentRow < 31) {
continue;
}
$intShiftedRow = $intCurrentRow + $intRowDelta;
$objRow->setAttribute('r', (string)$intShiftedRow);
foreach ($objRow->childNodes as $objCell) {
if ($objCell instanceof DOMElement && $objCell->localName === 'c') {
$strColumn = preg_replace('/[0-9]+/', '', $objCell->getAttribute('r'));
$objCell->setAttribute('r', $strColumn . $intShiftedRow);
}
}
}
foreach ($objXpath->query('//m:mergeCell') as $objMerge) {
if (!($objMerge instanceof DOMElement)) {
continue;
}
$objMerge->setAttribute(
'ref',
fxBibProductionXmlShiftReference($objMerge->getAttribute('ref'), 31, $intRowDelta)
);
}
}
$objTotal = fxBibProductionXmlFindRow($objXpath, $intTotalRow);
if (!$objTotal) {
throw new Exception('MSIN-4469 total row missing');
}
for ($intSlot = 0; $intSlot < $intSlotCount; $intSlot++) {
$intRow = 4 + ($intSlot * 3);
$objEventRow = fxBibProductionXmlCloneRow($objBaseEvent, $intRow);
$objSecondRow = fxBibProductionXmlCloneRow($objBaseSecond, $intRow + 1);
$objBlankRow = fxBibProductionXmlCloneRow($objBaseBlank, $intRow + 2);
$tabRow = $tabOrderRows[$intSlot];
$objCellF = fxBibProductionXmlFindCell($objEventRow, 'F');
$strStyleK = $objCellF ? (string)$objCellF->getAttribute('s') : '';
fxBibProductionXmlSetInlineString($objDom, $objEventRow, 'A', $intRow, $tabRow['label']);
fxBibProductionXmlSetNumber($objDom, $objEventRow, 'F', $intRow, $tabRow['quantity']);
fxBibProductionXmlSetNumber($objDom, $objEventRow, 'G', $intRow, $tabRow['start']);
fxBibProductionXmlSetNumber($objDom, $objEventRow, 'H', $intRow, $tabRow['finish']);
fxBibProductionXmlSetFormula($objDom, $objEventRow, 'I', $intRow, 'H' . $intRow . '-G' . $intRow . '+1');
fxBibProductionXmlSetFormula($objDom, $objEventRow, 'J', $intRow, 'I' . $intRow . '=F' . $intRow);
// MSIN-4469 — À côté de la quantité : lignes Imprimeur pour cette séquence.
fxBibProductionXmlSetNumber($objDom, $objEventRow, 'K', $intRow, $tabRow['quantity'], $strStyleK);
// MSIN-4469 — V1 : une puce par dossard, avec le même numéro.
fxBibProductionXmlSetNumber($objDom, $objEventRow, 'L', $intRow, $tabRow['quantity']);
fxBibProductionXmlSetNumber($objDom, $objEventRow, 'M', $intRow, $tabRow['start']);
fxBibProductionXmlSetNumber($objDom, $objEventRow, 'N', $intRow, $tabRow['finish']);
fxBibProductionXmlSetFormula($objDom, $objEventRow, 'O', $intRow, 'G' . $intRow . '=M' . $intRow);
fxBibProductionXmlSetFormula($objDom, $objEventRow, 'P', $intRow, 'H' . $intRow . '=N' . $intRow);
$objSheetData->insertBefore($objEventRow, $objTotal);
$objSheetData->insertBefore($objSecondRow, $objTotal);
$objSheetData->insertBefore($objBlankRow, $objTotal);
}
$objTitleRow = fxBibProductionXmlFindRow($objXpath, 1);
if ($objTitleRow) {
fxBibProductionXmlSetInlineString(
$objDom,
$objTitleRow,
'A',
1,
$tabSnapshot['event']['name']
);
}
// MSIN-4469 — Colonne K : validation quantité vs lignes onglet Imprimeur (hors vides).
$objHeaderRow = fxBibProductionXmlFindRow($objXpath, 3);
if ($objHeaderRow) {
$objHeaderCellF = fxBibProductionXmlFindCell($objHeaderRow, 'F');
$strHeaderStyleK = $objHeaderCellF
? (string)$objHeaderCellF->getAttribute('s')
: '';
fxBibProductionXmlSetInlineString(
$objDom,
$objHeaderRow,
'K',
3,
isset($tabLabels['printer_lines']) ? $tabLabels['printer_lines'] : 'Lignes Imprimeur',
$strHeaderStyleK
);
}
$intBibTotal = 0;
foreach ($tabOrderRows as $tabRow) {
$intBibTotal += (int)$tabRow['quantity'];
}
// Reprendre la ligne TOTAL après les insertions (référence DOM à jour).
$objTotal = fxBibProductionXmlFindRow($objXpath, $intTotalRow);
if (!$objTotal) {
throw new Exception('MSIN-4469 total row missing');
}
fxBibProductionXmlSetInlineString($objDom, $objTotal, 'A', $intTotalRow, $tabLabels['total']);
fxBibProductionXmlSetFormula(
$objDom,
$objTotal,
'F',
$intTotalRow,
'SUM(F4:F' . ($intTotalRow - 1) . ')',
'',
$intBibTotal
);
fxBibProductionXmlSetFormula(
$objDom,
$objTotal,
'I',
$intTotalRow,
'SUM(I4:I' . ($intTotalRow - 1) . ')',
'',
$intBibTotal
);
fxBibProductionXmlSetFormula(
$objDom,
$objTotal,
'L',
$intTotalRow,
'SUM(L4:L' . ($intTotalRow - 1) . ')',
'',
$intBibTotal
);
// COUNT ignore l'en-tête texte et les lignes blanches entre épreuves.
fxBibProductionXmlSetFormula(
$objDom,
$objTotal,
'K',
$intTotalRow,
'COUNT(Imprimeur!A:A)',
'',
$intBibTotal
);
fxBibProductionXmlSetFormula(
$objDom,
$objTotal,
'J',
$intTotalRow,
'F' . $intTotalRow . '=K' . $intTotalRow,
'',
'1'
);
$objDimension = $objXpath->query('//m:dimension')->item(0);
if ($objDimension instanceof DOMElement) {
$objDimension->setAttribute('ref', 'A1:Q' . (48 + $intRowDelta));
}
return $objDom->saveXML();
}
function fxBibProductionBuildWorkbook(array $tabSnapshot, $strDestination, array $tabLabels) {
$strTemplate = fxBibProductionTemplatePath();
if (!is_file($strTemplate)) {
throw new Exception('MSIN-4469 template missing');
}
if (!class_exists('ZipArchive') || !class_exists('DOMDocument')) {
throw new Exception('MSIN-4469 ZIP/XML extensions missing');
}
if (!copy($strTemplate, $strDestination)) {
throw new Exception('MSIN-4469 template copy failed');
}
$objZip = new ZipArchive();
if ($objZip->open($strDestination) !== true) {
throw new Exception('MSIN-4469 workbook open failed');
}
try {
$strSupplierXml = $objZip->getFromName('xl/worksheets/sheet1.xml');
$strOrderXml = $objZip->getFromName('xl/worksheets/sheet2.xml');
if ($strSupplierXml === false || $strOrderXml === false) {
throw new Exception('MSIN-4469 worksheet missing');
}
$strSupplierXml = fxBibProductionBuildSupplierXml($strSupplierXml, $tabSnapshot, $tabLabels);
$strOrderXml = fxBibProductionBuildOrderXml($strOrderXml, $tabSnapshot, $tabLabels);
$objZip->deleteName('xl/worksheets/sheet1.xml');
$objZip->addFromString('xl/worksheets/sheet1.xml', $strSupplierXml);
$objZip->deleteName('xl/worksheets/sheet2.xml');
$objZip->addFromString('xl/worksheets/sheet2.xml', $strOrderXml);
$strWorkbookXml = $objZip->getFromName('xl/workbook.xml');
if ($strWorkbookXml !== false) {
$objWorkbookDom = fxBibProductionXmlLoad($strWorkbookXml);
$objWorkbookXpath = new DOMXPath($objWorkbookDom);
$objWorkbookXpath->registerNamespace('m', 'http://schemas.openxmlformats.org/spreadsheetml/2006/main');
$objCalcPr = $objWorkbookXpath->query('//m:calcPr')->item(0);
if ($objCalcPr instanceof DOMElement) {
$objCalcPr->setAttribute('fullCalcOnLoad', '1');
$objCalcPr->setAttribute('forceFullCalc', '1');
}
$objZip->deleteName('xl/workbook.xml');
$objZip->addFromString('xl/workbook.xml', $objWorkbookDom->saveXML());
}
} catch (Exception $objException) {
$objZip->close();
@unlink($strDestination);
throw $objException;
}
$objZip->close();
}
/**
* Crée la commande officielle et verrouille immédiatement les séquences via
* bpo_active_key. Une requête concurrente retrouve la commande déjà active.
*/
function fxBibProductionCreateOfficialOrder($intEveId, $strLangue = 'fr') {
global $objDatabase;
$intEveId = (int)$intEveId;
$tabExisting = fxBibProductionGetActiveOrder($intEveId);
if ($tabExisting) {
return ['success' => true, 'order' => $tabExisting, 'existing' => true];
}
$tabCollected = fxBibProductionCollectSnapshot($intEveId, $strLangue);
if (empty($tabCollected['success'])) {
return $tabCollected;
}
$tabSnapshot = $tabCollected['snapshot'];
$strSnapshot = json_encode($tabSnapshot, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if ($strSnapshot === false) {
return ['success' => false, 'message' => fxBibMsg('bib_v5_production_generation_error')];
}
$strCreatedBy = fxBibGetCurrentPromoteurDisplayName();
$intComId = (int)($_SESSION['com_id'] ?? 0);
$intUsaId = (int)($_SESSION['usa_id'] ?? 0);
$strEventPart = fxBibProductionSafeFilePart($tabSnapshot['event']['name']);
$objDatabase->fxQuery('START TRANSACTION');
$objDatabase->fxGetVar(
"SELECT eve_id FROM inscriptions_evenements WHERE eve_id = $intEveId FOR UPDATE"
);
$tabExisting = fxBibProductionGetActiveOrder($intEveId);
if ($tabExisting) {
$objDatabase->fxQuery('COMMIT');
return ['success' => true, 'order' => $tabExisting, 'existing' => true];
}
$intVersion = (int)$objDatabase->fxGetVar(
"SELECT COALESCE(MAX(bpo_version), 0) + 1
FROM inscriptions_bib_production_orders
WHERE eve_id = $intEveId"
);
if ($intVersion <= 0) {
$intVersion = 1;
}
$strFileName = 'commande-dossards-' . $strEventPart . '-v' . $intVersion . '.xlsx';
$strInsert = "
INSERT INTO inscriptions_bib_production_orders (
eve_id,
bpo_com_id,
bpo_usa_id,
bpo_status,
bpo_active_key,
bpo_version,
bpo_file_name,
bpo_file_path,
bpo_sha256,
bpo_snapshot,
bpo_created_at,
bpo_created_by
) VALUES (
$intEveId,
$intComId,
$intUsaId,
'generating',
1,
$intVersion,
'" . $objDatabase->fxEscape($strFileName) . "',
'',
'',
'" . $objDatabase->fxEscape($strSnapshot) . "',
NOW(),
'" . $objDatabase->fxEscape($strCreatedBy) . "'
)
";
$qryInsert = $objDatabase->fxQuery($strInsert);
$intOrderId = $qryInsert ? (int)$objDatabase->fxGetLastId() : 0;
if ($intOrderId <= 0) {
$objDatabase->fxQuery('ROLLBACK');
$tabExisting = fxBibProductionGetActiveOrder($intEveId);
if ($tabExisting) {
return ['success' => true, 'order' => $tabExisting, 'existing' => true];
}
return ['success' => false, 'message' => fxBibMsg('bib_v5_production_generation_error')];
}
foreach ($tabSnapshot['races'] as $tabRace) {
foreach ($tabRace['ranges'] as $tabRange) {
$qryRange = $objDatabase->fxQuery(
"INSERT INTO inscriptions_bib_production_ranges (
bpo_id,
eve_id,
epr_id,
epr_bib_id,
bpr_epr_label,
bpr_start,
bpr_finish,
bpr_quantity
) VALUES (
$intOrderId,
$intEveId,
" . (int)$tabRace['epr_id'] . ",
" . (int)$tabRange['epr_bib_id'] . ",
'" . $objDatabase->fxEscape($tabRace['label']) . "',
" . (int)$tabRange['start'] . ",
" . (int)$tabRange['finish'] . ",
" . (int)$tabRange['quantity'] . "
)"
);
if ($qryRange === false) {
$objDatabase->fxQuery('ROLLBACK');
return ['success' => false, 'message' => fxBibMsg('bib_v5_production_generation_error')];
}
}
}
$objDatabase->fxQuery('COMMIT');
$strRelativeDir = 'eve-' . $intEveId . DIRECTORY_SEPARATOR . 'order-' . $intOrderId;
$strAbsoluteDir = fxBibProductionStorageRoot() . DIRECTORY_SEPARATOR . $strRelativeDir;
$strAbsoluteFile = $strAbsoluteDir . DIRECTORY_SEPARATOR . $strFileName;
try {
if (!fxBibProductionEnsureDirectory($strAbsoluteDir)) {
throw new Exception('MSIN-4469 storage unavailable');
}
$strStorageRoot = fxBibProductionStorageRoot();
if (fxBibProductionEnsureDirectory($strStorageRoot)) {
$strDenyFile = $strStorageRoot . DIRECTORY_SEPARATOR . '.htaccess';
if (!is_file($strDenyFile)) {
@file_put_contents($strDenyFile, "Deny from all\n");
}
}
fxBibProductionBuildWorkbook(
$tabSnapshot,
$strAbsoluteFile,
[
'bib' => fxBibTexte('bib_v5_production_col_bib', 0),
'race' => fxBibTexte('bib_v5_production_col_race', 0),
'total' => fxBibTexte('bib_v5_production_total', 0),
'title' => fxBibTexte('bib_v5_production_excel_title', 0),
'printer_lines' => fxBibTexte('bib_v5_production_col_printer_lines', 0),
]
);
if (!is_file($strAbsoluteFile)) {
throw new Exception('MSIN-4469 workbook not written');
}
$strRelativeFile = str_replace('\\', '/', $strRelativeDir . DIRECTORY_SEPARATOR . $strFileName);
$strSha256 = hash_file('sha256', $strAbsoluteFile);
$objDatabase->fxQuery(
"UPDATE inscriptions_bib_production_orders
SET bpo_status = 'ready',
bpo_file_path = '" . $objDatabase->fxEscape($strRelativeFile) . "',
bpo_sha256 = '" . $objDatabase->fxEscape($strSha256) . "'
WHERE bpo_id = $intOrderId
AND bpo_active_key = 1"
);
} catch (Exception $objException) {
if (is_file($strAbsoluteFile)) {
@unlink($strAbsoluteFile);
}
$objDatabase->fxQuery(
"UPDATE inscriptions_bib_production_orders
SET bpo_status = 'failed',
bpo_active_key = NULL,
bpo_error = '" . $objDatabase->fxEscape($objException->getMessage()) . "'
WHERE bpo_id = $intOrderId"
);
return ['success' => false, 'message' => fxBibMsg('bib_v5_production_generation_error')];
}
return [
'success' => true,
'order' => fxBibProductionGetOrder($intOrderId, $intEveId),
'existing' => false,
];
}
function fxBibProductionOrderAbsolutePath(array $tabOrder) {
$strRelative = str_replace('\\', '/', (string)($tabOrder['bpo_file_path'] ?? ''));
if ($strRelative === '' || strpos($strRelative, '..') !== false || strpos($strRelative, "\0") !== false) {
return '';
}
return fxBibProductionStorageRoot() . DIRECTORY_SEPARATOR
. str_replace('/', DIRECTORY_SEPARATOR, $strRelative);
}
function fxBibProductionStreamOrder(array $tabOrder) {
if (($tabOrder['bpo_status'] ?? '') !== 'ready') {
return false;
}
$strFile = fxBibProductionOrderAbsolutePath($tabOrder);
if ($strFile === '' || !is_file($strFile)) {
return false;
}
while (ob_get_level() > 0) {
ob_end_clean();
}
$strDownloadName = basename((string)$tabOrder['bpo_file_name']);
header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
header('Content-Disposition: attachment; filename="' . str_replace('"', '', $strDownloadName) . '"');
header('Content-Length: ' . filesize($strFile));
header('Cache-Control: private, no-store, max-age=0');
header('X-Content-Type-Options: nosniff');
readfile($strFile);
return true;
}

View File

@ -0,0 +1,302 @@
<?php
/**
* MSIN-4485 — Impression dossard PDF (1 participant).
*
* Format FINAL : demi-feuille Lettre coupée sur le 11"
* → papier 8.5" large × 5.5" haut (paysage)
* → PDF page = exactement cette taille
* → fond pleine page + n° / nom / QR alignés
*
* Impression : papier personnalisé 8.5 × 5.5, échelle 100 %, sans « ajuster à la page ».
*/
define('MSIN_DOSSARD_PRINT_PERM', 'inscriptions_gestion.dossard_print');
/**
* Taille page = feuille coupée (pouces → mm).
* Lettre 8.5×11 coupée en 2 sur le 11" → 8.5 × 5.5.
*
* @return array{0:float,1:float} [largeur, hauteur]
*/
function fxDossardPrintPageSizeMm()
{
return array(8.5 * 25.4, 5.5 * 25.4);
}
/** Dossier des fonds JPG/PNG (même ratio 8.5:5.5 recommandé). */
function fxDossardPrintTemplatesDir()
{
return dirname(__DIR__) . '/assets/dossard_templates';
}
/**
* Fond : eve_{id}.jpg|png sinon default.jpg|png.
*
* @return string|null
*/
function fxDossardPrintResolveTemplatePath($intEveId)
{
$strDir = fxDossardPrintTemplatesDir();
$intEveId = intval($intEveId);
$arrCandidates = array();
if ($intEveId > 0) {
$arrCandidates[] = $strDir . '/eve_' . $intEveId . '.jpg';
$arrCandidates[] = $strDir . '/eve_' . $intEveId . '.jpeg';
$arrCandidates[] = $strDir . '/eve_' . $intEveId . '.png';
}
$arrCandidates[] = $strDir . '/default.jpg';
$arrCandidates[] = $strDir . '/default.jpeg';
$arrCandidates[] = $strDir . '/default.png';
foreach ($arrCandidates as $strPath) {
if (is_file($strPath)) {
return $strPath;
}
}
return null;
}
/**
* Type FPDF d'après le fichier réel.
*
* @return string JPG|PNG|GIF|''
*/
function fxDossardPrintImageType($strPath)
{
$arrInfo = @getimagesize($strPath);
if (!is_array($arrInfo) || empty($arrInfo[2])) {
return '';
}
if ((int)$arrInfo[2] === IMAGETYPE_JPEG) {
return 'JPG';
}
if ((int)$arrInfo[2] === IMAGETYPE_PNG) {
return 'PNG';
}
if ((int)$arrInfo[2] === IMAGETYPE_GIF) {
return 'GIF';
}
return '';
}
/** Texte FPDF (core fonts = Windows-1252). */
function fxDossardPrintPdfText($str)
{
$str = (string)$str;
if (function_exists('iconv')) {
$strConverted = @iconv('UTF-8', 'Windows-1252//TRANSLIT', $str);
if ($strConverted !== false) {
return $strConverted;
}
}
if (function_exists('mb_convert_encoding')) {
return (string)mb_convert_encoding($str, 'Windows-1252', 'UTF-8');
}
return $str;
}
/** Droit impression (hors kit total). */
function fxDossardPrintCan($intComId, $intEveId)
{
if (!function_exists('fxEveAccesHasPermission')) {
require_once __DIR__ . '/inc_fx_eve_acces.php';
}
return fxEveAccesHasPermission(intval($intComId), intval($intEveId), MSIN_DOSSARD_PRINT_PERM);
}
/**
* @return array|null
*/
function fxDossardPrintLoadParticipant($intParId, $intEveId)
{
global $objDatabase;
$intParId = intval($intParId);
$intEveId = intval($intEveId);
if ($intParId <= 0 || $intEveId <= 0) {
return null;
}
$sql = 'SELECT p.par_id, p.par_prenom, p.par_nom, p.no_bib, p.eve_id, p.epr_id, p.pec_id,
e.pec_id_original, e.pec_actif, e.is_cancelled
FROM resultats_participants p
INNER JOIN resultats_epreuves_commandees e
ON e.pec_id_original = p.pec_id AND e.epr_id = p.epr_id
WHERE p.par_id = ' . $intParId . '
AND p.eve_id = ' . $intEveId . '
LIMIT 1';
$row = $objDatabase->fxGetRow($sql);
return ($row === null || $row === false) ? null : $row;
}
function fxDossardPrintQrUrl($intEveId)
{
if (!function_exists('fxKcEveResultatsQrUrl')) {
require_once __DIR__ . '/inc_fx_eve_extra.php';
}
return fxKcEveResultatsQrUrl(intval($intEveId));
}
/**
* @return string|false chemin PNG temp
*/
function fxDossardPrintBuildQrTempFile($intEveId)
{
require_once __DIR__ . '/../libs/phpqrcode-master/qrlib.php';
$strUrl = fxDossardPrintQrUrl($intEveId);
if ($strUrl === '') {
return false;
}
$strTmp = tempnam(sys_get_temp_dir(), 'ms1dossardqr_');
if ($strTmp === false) {
return false;
}
$strPng = $strTmp . '.png';
@unlink($strTmp);
QRcode::png($strUrl, $strPng, QR_ECLEVEL_M, 6, 1);
if (!is_file($strPng)) {
return false;
}
return $strPng;
}
/**
* Layout page 8.5" × 5.5" (mm) + overlays.
* Coordonnées calibrées pour le template comic (case QR bas-droite).
*
* @return array<string,mixed>
*/
function fxDossardPrintLayout()
{
$arrSize = fxDossardPrintPageSizeMm();
$fltW = $arrSize[0]; // 215.9
$fltH = $arrSize[1]; // 139.7
// Case QR du design : bas-droite (~1.1" dans le cadre)
$fltQr = 28.0;
return array(
'page_w' => $fltW,
'page_h' => $fltH,
// N° centré dans la zone blanche centrale
'bib_y' => 52,
'bib_font' => 72,
'bib_h' => 28,
// Nom sous le numéro
'name_y' => 84,
'name_font' => 14,
// QR dans la case « SCANNEZ POUR RÉSULTATS »
'qr_size' => $fltQr,
'qr_x' => $fltW - $fltQr - 11.5,
'qr_y' => $fltH - $fltQr - 14.5,
);
}
/**
* Génère et envoie le PDF (exit).
*
* @param array $arrRow
*/
function fxDossardPrintOutputPdf($arrRow)
{
$intEveId = intval($arrRow['eve_id'] ?? 0);
$strTemplate = fxDossardPrintResolveTemplatePath($intEveId);
if ($strTemplate === null) {
http_response_code(500);
header('Content-Type: text/plain; charset=utf-8');
echo 'Template dossard introuvable (assets/dossard_templates/default.jpg).';
exit;
}
$strFpdf = dirname(__DIR__) . '/fpdf182/fpdf.php';
if (!is_file($strFpdf)) {
$strDocRoot = $_SERVER['DOCUMENT_ROOT'] ?? '';
$strFpdf = rtrim((string)$strDocRoot, '/\\') . '/fpdf182/fpdf.php';
}
if (!is_file($strFpdf)) {
http_response_code(500);
header('Content-Type: text/plain; charset=utf-8');
echo 'FPDF introuvable (fpdf182/fpdf.php).';
exit;
}
require_once $strFpdf;
$arrLayout = fxDossardPrintLayout();
$strBib = trim((string)($arrRow['no_bib'] ?? ''));
if ($strBib === '' || $strBib === '0') {
$strBib = '-';
}
$strPrenom = trim(function_exists('fxUnescape') ? fxUnescape($arrRow['par_prenom'] ?? '') : (string)($arrRow['par_prenom'] ?? ''));
$strNom = trim(function_exists('fxUnescape') ? fxUnescape($arrRow['par_nom'] ?? '') : (string)($arrRow['par_nom'] ?? ''));
$strName = trim($strPrenom . ' ' . $strNom);
$strQrFile = fxDossardPrintBuildQrTempFile($intEveId);
$strTplType = fxDossardPrintImageType($strTemplate);
// Page = feuille coupée 8.5" × 5.5".
// FPDF (_getpagesize) range si w>h ; 'L' remet w=8.5 h=5.5.
$pdf = new FPDF('L', 'mm', array($arrLayout['page_w'], $arrLayout['page_h']));
$pdf->SetAutoPageBreak(false);
$pdf->SetMargins(0, 0, 0);
$pdf->AddPage();
// Fond pleine page — ratio template ≈ 8.5:5.5 → pas de déformation
$pdf->Image(
$strTemplate,
0,
0,
$arrLayout['page_w'],
$arrLayout['page_h'],
$strTplType
);
// Numéro
$pdf->SetTextColor(0, 0, 0);
$pdf->SetFont('Helvetica', 'B', intval($arrLayout['bib_font']));
$pdf->SetXY(20, $arrLayout['bib_y']);
$pdf->Cell(
$arrLayout['page_w'] - 40,
$arrLayout['bib_h'],
fxDossardPrintPdfText($strBib),
0,
0,
'C'
);
// Nom
if ($strName !== '') {
$pdf->SetFont('Helvetica', 'B', intval($arrLayout['name_font']));
$pdf->SetXY(20, $arrLayout['name_y']);
$pdf->Cell($arrLayout['page_w'] - 40, 8, fxDossardPrintPdfText($strName), 0, 0, 'C');
}
// QR dans la case du template
if ($strQrFile !== false) {
$pdf->Image(
$strQrFile,
$arrLayout['qr_x'],
$arrLayout['qr_y'],
$arrLayout['qr_size'],
$arrLayout['qr_size'],
'PNG'
);
@unlink($strQrFile);
}
$strSafeBib = preg_replace('/[^a-zA-Z0-9_-]+/', '-', $strBib);
$strFilename = 'dossard-' . $strSafeBib . '-par' . intval($arrRow['par_id']) . '.pdf';
$pdf->Output('I', $strFilename);
exit;
}

View File

@ -97,7 +97,7 @@ function fxEveAccesGetRoles($blnActifOnly = true)
return $objDatabase->fxGetResults($sql);
}
/** Kits proposables par un promoteur (role_delegable = 1, hors migration_legacy). */
/** Kits proposables par un promoteur (role_delegable = 1, hors migration_legacy / kit total). */
function fxEveAccesGetDelegableRoles()
{
global $objDatabase;
@ -116,11 +116,18 @@ function fxEveAccesGetDelegableRoles()
return array();
}
// MSIN-4478 — jamais offrir un kit total (rapports / argent / tout) en invitation promoteur.
$strNoGrantsAll = '';
if (fxEveAccesHasGrantsAllColumn()) {
$strNoGrantsAll = ' AND role_grants_all = 0';
}
$sql = "SELECT role_id, role_code, role_label_fr, role_label_en, role_description_fr
FROM inscriptions_eve_roles
WHERE role_actif = 1
AND role_delegable = 1
AND role_code <> 'migration_legacy'
$strNoGrantsAll
ORDER BY role_tri ASC, role_label_fr ASC";
$tab = $objDatabase->fxGetResults($sql);
@ -142,8 +149,9 @@ function fxEveAccesRoleIsDelegable($intRoleId)
return false;
}
$strGrantsCol = fxEveAccesHasGrantsAllColumn() ? ', role_grants_all' : ', 0 AS role_grants_all';
$row = $objDatabase->fxGetRow(
"SELECT role_id, role_code, role_delegable, role_actif
"SELECT role_id, role_code, role_delegable, role_actif $strGrantsCol
FROM inscriptions_eve_roles WHERE role_id = $intRoleId LIMIT 1"
);
if ($row === null || intval($row['role_actif']) !== 1) {
@ -152,6 +160,10 @@ function fxEveAccesRoleIsDelegable($intRoleId)
if (($row['role_code'] ?? '') === 'migration_legacy') {
return false;
}
// MSIN-4478 — kit total = non délégable (même si la case est cochée en BD).
if (intval($row['role_grants_all'] ?? 0) === 1) {
return false;
}
return intval($row['role_delegable'] ?? 0) === 1;
}
@ -398,6 +410,52 @@ function fxEveAccesCatalogPermIsActive($strPermKey)
return ($intNb != null && intval($intNb) > 0);
}
/** MSIN-4484 — Colonne catalogue perm_grants_all presente ? */
function fxEveAccesHasPermGrantsAllColumn()
{
global $objDatabase;
static $blnHas = null;
if ($blnHas !== null) {
return $blnHas;
}
if (!fxEveAccesIsEnabled()) {
$blnHas = false;
return $blnHas;
}
$tab = $objDatabase->fxGetResults("SHOW COLUMNS FROM inscriptions_eve_permissions LIKE 'perm_grants_all'");
$blnHas = ($tab != null && count($tab) > 0);
return $blnHas;
}
/**
* MSIN-4484 — Cle active et incluse dans un kit role_grants_all ?
* perm_grants_all = 0 → hors propagation (matrice / extra seulement).
*/
function fxEveAccesCatalogPermPropagatesWithGrantsAll($strPermKey)
{
global $objDatabase;
if (!fxEveAccesIsEnabled() || trim((string)$strPermKey) === '') {
return false;
}
if (!fxEveAccesHasPermGrantsAllColumn()) {
return fxEveAccesCatalogPermIsActive($strPermKey);
}
$sql = "SELECT COUNT(*) FROM inscriptions_eve_permissions
WHERE perm_key = '" . $objDatabase->fxEscape($strPermKey) . "'
AND perm_actif = 1
AND perm_grants_all = 1";
$intNb = $objDatabase->fxGetVar($sql);
return ($intNb != null && intval($intNb) > 0);
}
/** Compte avec au moins un kit role_grants_all actif sur cet evenement. */
function fxEveAccesComEventHasGrantsAllKit($intComId, $intEveId)
{
@ -472,8 +530,10 @@ function fxEveAccesHasPermission($intComId, $intEveId, $strPermKey)
return false;
}
if (fxEveAccesComEventHasGrantsAllKit($intComId, $intEveId)) {
return fxEveAccesCatalogPermIsActive($strPermKey);
// MSIN-4484 — kit total = cles propagées ; cles hors kit total → matrice / extra seulement.
if (fxEveAccesComEventHasGrantsAllKit($intComId, $intEveId)
&& fxEveAccesCatalogPermPropagatesWithGrantsAll($strPermKey)) {
return true;
}
$sql = "SELECT COUNT(*) FROM " . fxEveAccesPermSql() . "
@ -493,14 +553,13 @@ function fxEveAccesHasAnyPermission($intComId, $intEveId, $arrPermKeys)
return false;
}
// MSIN-4484 — kit total pour les cles propagées ; sinon (ou cles hors total) → explicite.
if (fxEveAccesComEventHasGrantsAllKit($intComId, $intEveId)) {
foreach ($arrPermKeys as $strKey) {
if (fxEveAccesCatalogPermIsActive($strKey)) {
if (fxEveAccesCatalogPermPropagatesWithGrantsAll($strKey)) {
return true;
}
}
return false;
}
$arrEsc = array();
@ -571,8 +630,14 @@ function fxEveAccesGetCatalogPermissions($strScope = null, $blnActifOnly = true)
return array();
}
$sql = "SELECT perm_key, perm_group, perm_scope, perm_label_fr, perm_label_en, perm_phase, perm_tri
FROM inscriptions_eve_permissions";
// MSIN-4484 — perm_grants_all pour UI matrice (badge hors kit total).
$strGrantsCol = fxEveAccesHasPermGrantsAllColumn()
? ', perm_grants_all'
: ', 1 AS perm_grants_all';
$sql = "SELECT perm_key, perm_group, perm_scope, perm_label_fr, perm_label_en, perm_phase, perm_tri"
. $strGrantsCol
. " FROM inscriptions_eve_permissions";
$arrWhere = array();
if ($blnActifOnly) {
@ -1691,7 +1756,18 @@ function fxEveAccesShowCompteForm($intComId)
$arrAcces = fxEveAccesListByComId($intComId);
$arrExtras = fxEveAccesListExtraByComId($intComId);
$arrRoles = fxEveAccesGetRoles(true);
// MSIN-4485 — extras = actions ; clés hors kit total en tête de liste.
$arrActionPerms = fxEveAccesGetCatalogPermissions('action');
if ($arrActionPerms != null && is_array($arrActionPerms)) {
usort($arrActionPerms, function ($a, $b) {
$intA = isset($a['perm_grants_all']) ? intval($a['perm_grants_all']) : 1;
$intB = isset($b['perm_grants_all']) ? intval($b['perm_grants_all']) : 1;
if ($intA !== $intB) {
return $intA <=> $intB; // 0 (hors total) avant 1
}
return strcmp((string)($a['perm_label_fr'] ?? ''), (string)($b['perm_label_fr'] ?? ''));
});
}
$strT = urlencode($_GET['t'] ?? '');
$intComId = intval($intComId);
@ -1743,8 +1819,13 @@ function fxEveAccesShowCompteForm($intComId)
$strPermOptions = '<option value="">—</option>';
if ($arrActionPerms != null) {
foreach ($arrActionPerms as $rowPerm) {
$blnHorsTotal = isset($rowPerm['perm_grants_all']) && intval($rowPerm['perm_grants_all']) === 0;
$strLabel = (string)($rowPerm['perm_label_fr'] ?? $rowPerm['perm_key']);
if ($blnHorsTotal) {
$strLabel = '★ ' . $strLabel . ' (hors kit total)';
}
$strPermOptions .= '<option value="' . htmlspecialchars($rowPerm['perm_key'], ENT_QUOTES, 'UTF-8') . '">'
. htmlspecialchars($rowPerm['perm_label_fr'], ENT_QUOTES, 'UTF-8') . '</option>';
. htmlspecialchars($strLabel, ENT_QUOTES, 'UTF-8') . '</option>';
}
}
require_once __DIR__ . '/inc_fx_superadm_v2_ui.php';
@ -1815,7 +1896,7 @@ function fxEveAccesShowCompteForm($intComId)
</td>
<td class="align-middle text-right text-nowrap">
<?php if ($blnHasKit) { ?>
<select id="eve-extra-<?= $intEveId ?>" class="custom-select custom-select-sm d-inline-block" style="width:96px;vertical-align:middle;" aria-label="Droit individuel">
<select id="eve-extra-<?= $intEveId ?>" class="custom-select custom-select-sm d-inline-block" style="width:220px;max-width:40vw;vertical-align:middle;" aria-label="Droit individuel">
<?= $strPermOptions ?>
</select>
<button type="button" class="btn btn-outline-warning btn-sm py-0 btn-eve-add-extra" data-eve-id="<?= $intEveId ?>" title="Ajouter un droit individuel" style="border-color:#e67e22;color:#e67e22;">+</button>

View File

@ -71,6 +71,11 @@ function fxEveAccesPromoteurT($strClef, $strLangue = 'fr')
'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.',
),
// MSIN-4478 — réinvite d'un stub jamais activé
'eve_acces_promoteur_ok_pending' => array(
'fr' => 'Invitation mise à jour. Un courriel avec le lien de mot de passe a été envoyé.',
'en' => 'Invitation updated. 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é.',
@ -176,6 +181,145 @@ function fxEveAccesPromoteurHasInviteTeamColumn()
return $blnHas;
}
/**
* MSIN-4478 — compte stub d'invitation jamais vraiment utilisé (pas de 1re visite).
*/
function fxEveAccesPromoteurCompteIsPendingInvite($arrCompte)
{
if (!is_array($arrCompte) || empty($arrCompte['com_id'])) {
return false;
}
$strLastVisit = trim((string)($arrCompte['com_lastvisit'] ?? ''));
$blnNeverVisited = ($strLastVisit === '' || strpos($strLastVisit, '0000-00-00') === 0);
if (!$blnNeverVisited) {
return false;
}
if (!fxEveAccesPromoteurHasInviteTeamColumn()) {
return false;
}
return intval($arrCompte['com_invite_team'] ?? 0) === 1;
}
/**
* MSIN-4478 — nouveau jeton reset pour renvoyer le mail « créez votre mot de passe ».
*
* @return string token ('' si colonnes absentes)
*/
function fxEveAccesPromoteurRefreshResetToken($intComId)
{
global $objDatabase;
$intComId = intval($intComId);
if ($intComId <= 0) {
return '';
}
$strToken = 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($strToken) . "'";
}
$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) {
return '';
}
$objDatabase->fxQuery(
'UPDATE inscriptions_comptes SET ' . implode(', ', $arrSet)
. ' WHERE com_id = ' . $intComId
);
return $strToken;
}
/**
* MSIN-4478 — un seul kit actif/invite par personne+événement : révoque les autres rôles.
*/
function fxEveAccesPromoteurRevokeOtherKits($intComId, $intEveId, $intKeepRoleId, $intActorComId)
{
global $objDatabase;
$intComId = intval($intComId);
$intEveId = intval($intEveId);
$intKeepRoleId = intval($intKeepRoleId);
$intActorComId = intval($intActorComId);
if ($intComId <= 0 || $intEveId <= 0 || $intKeepRoleId <= 0 || !fxEveAccesIsEnabled()) {
return;
}
$tab = $objDatabase->fxGetResults(
"SELECT ea_id FROM inscriptions_eve_acces
WHERE com_id = $intComId
AND eve_id = $intEveId
AND role_id <> $intKeepRoleId
AND ea_statut IN ('actif', 'invite')"
);
if ($tab == null) {
return;
}
foreach ($tab as $row) {
fxEveAccesRevoke(intval($row['ea_id']), $intActorComId);
}
}
/**
* MSIN-4478 — après revoke : si stub jamais activé et plus aucun accès, désactive le compte (libère le courriel).
*/
function fxEveAccesPromoteurDeactivateOrphanInviteStub($intComId)
{
global $objDatabase;
$intComId = intval($intComId);
if ($intComId <= 0 || !fxEveAccesIsEnabled()) {
return false;
}
$arrCompte = $objDatabase->fxGetRow(
'SELECT com_id, com_actif, com_invite_team, com_lastvisit
FROM inscriptions_comptes WHERE com_id = ' . $intComId . ' LIMIT 1'
);
if (empty($arrCompte) || !fxEveAccesPromoteurCompteIsPendingInvite($arrCompte)) {
return false;
}
if (intval($arrCompte['com_actif'] ?? 0) !== 1) {
return false;
}
$intLeft = intval($objDatabase->fxGetVar(
"SELECT COUNT(*) FROM inscriptions_eve_acces
WHERE com_id = $intComId
AND ea_statut IN ('actif', 'invite')"
));
if ($intLeft > 0) {
return false;
}
$arrSet = array('com_actif = 0');
$tabResetTok = $objDatabase->fxGetResults("SHOW COLUMNS FROM inscriptions_comptes LIKE 'com_reset_token'");
if ($tabResetTok != null && count($tabResetTok) > 0) {
$arrSet[] = "com_reset_token = ''";
}
$objDatabase->fxQuery(
'UPDATE inscriptions_comptes SET ' . implode(', ', $arrSet)
. ' WHERE com_id = ' . $intComId
);
return true;
}
/**
* MSIN-4401 — crée un compte MS1 minimal pour invitation promoteur.
*
@ -371,7 +515,7 @@ function fxEveAccesPromoteurSendInviteMail($arrCompte, $arrEve, $arrRole, $strLa
}
/**
* MSIN-4401 — invitation / assignation par promoteur.
* MSIN-4401 / MSIN-4478 — invitation / assignation par promoteur.
*
* @return array{state:string,message:string,created?:bool}
*/
@ -417,8 +561,13 @@ function fxEveAccesPromoteurInvite($intEveId, $intGrantedBy, $arrPost, $strLangu
return array('state' => 'error', 'message' => fxEveAccesPromoteurT('eve_acces_promoteur_err_generic', $strLangue));
}
$strCompteCols = 'com_id, com_prenom, com_nom, com_courriel, com_login, com_actif, com_lastvisit';
if (fxEveAccesPromoteurHasInviteTeamColumn()) {
$strCompteCols .= ', com_invite_team';
}
$rowExisting = $objDatabase->fxGetRow(
"SELECT com_id, com_prenom, com_nom, com_courriel, com_login, com_actif
"SELECT $strCompteCols
FROM inscriptions_comptes
WHERE com_courriel = '" . $objDatabase->fxEscape($strCourriel) . "'
ORDER BY com_actif DESC, com_id DESC
@ -426,13 +575,24 @@ function fxEveAccesPromoteurInvite($intEveId, $intGrantedBy, $arrPost, $strLangu
);
$blnCreated = false;
$blnPending = false;
$strResetToken = '';
$arrCompte = null;
if (!empty($rowExisting) && is_array($rowExisting)) {
$blnPending = fxEveAccesPromoteurCompteIsPendingInvite($rowExisting);
// MSIN-4478 — stub désactivé après revoke : on le réactive pour réinviter.
if (intval($rowExisting['com_actif']) !== 1) {
return array('state' => 'error', 'message' => fxEveAccesPromoteurT('eve_acces_promoteur_err_inactive', $strLangue));
if (!$blnPending) {
return array('state' => 'error', 'message' => fxEveAccesPromoteurT('eve_acces_promoteur_err_inactive', $strLangue));
}
$objDatabase->fxQuery(
'UPDATE inscriptions_comptes SET com_actif = 1 WHERE com_id = ' . intval($rowExisting['com_id'])
);
$rowExisting['com_actif'] = 1;
}
$arrCompte = $rowExisting;
if ($strPrenom !== '' && trim((string)$arrCompte['com_prenom']) === '') {
$objDatabase->fxQuery(
@ -448,6 +608,10 @@ function fxEveAccesPromoteurInvite($intEveId, $intGrantedBy, $arrPost, $strLangu
);
$arrCompte['com_nom'] = $strNom;
}
if ($blnPending) {
$strResetToken = fxEveAccesPromoteurRefreshResetToken(intval($arrCompte['com_id']));
}
} else {
if ($strPrenom === '' || $strNom === '') {
return array('state' => 'error', 'message' => fxEveAccesPromoteurT('eve_acces_promoteur_err_nom', $strLangue));
@ -463,6 +627,7 @@ function fxEveAccesPromoteurInvite($intEveId, $intGrantedBy, $arrPost, $strLangu
return array('state' => 'error', 'message' => fxEveAccesPromoteurT('eve_acces_promoteur_err_generic', $strLangue));
}
$blnCreated = true;
$blnPending = true;
$strResetToken = (string)($arrCreate['reset_token'] ?? '');
$arrCompte = array(
'com_id' => intval($arrCreate['com_id']),
@ -470,17 +635,29 @@ function fxEveAccesPromoteurInvite($intEveId, $intGrantedBy, $arrPost, $strLangu
'com_nom' => $strNom,
'com_courriel' => $strCourriel,
'com_login' => $strCourriel,
'com_invite_team' => 1,
'com_lastvisit' => '',
);
}
// MSIN-4478 — changer de kit = remplacer, pas empiler.
fxEveAccesPromoteurRevokeOtherKits(
intval($arrCompte['com_id']),
$intEveId,
$intRoleId,
$intGrantedBy
);
// Stub jamais activé → statut invite + mail mot de passe (même au 2e envoi).
$strGrantStatut = $blnPending ? 'invite' : 'actif';
$arrGrant = fxEveAccesGrant(
intval($arrCompte['com_id']),
$intEveId,
$intRoleId,
$intJours,
$intGrantedBy,
'MSIN-4401 invitation promoteur',
$blnCreated ? 'invite' : 'actif'
'MSIN-4478 invitation promoteur',
$strGrantStatut
);
if (($arrGrant['state'] ?? '') !== 'success') {
@ -492,22 +669,35 @@ function fxEveAccesPromoteurInvite($intEveId, $intGrantedBy, $arrPost, $strLangu
// 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);
fxEveAccesPromoteurSendInviteMail(
$arrCompte,
$arrEve,
$arrRole,
$strLangue,
$blnPending,
$strResetToken
);
} catch (Throwable $e) {
// ignore
}
if ($blnCreated) {
$strMsg = fxEveAccesPromoteurT('eve_acces_promoteur_ok_created', $strLangue);
} elseif ($blnPending) {
$strMsg = fxEveAccesPromoteurT('eve_acces_promoteur_ok_pending', $strLangue);
} else {
$strMsg = fxEveAccesPromoteurT('eve_acces_promoteur_ok_existing', $strLangue);
}
return array(
'state' => 'success',
'created' => $blnCreated,
'message' => $blnCreated
? fxEveAccesPromoteurT('eve_acces_promoteur_ok_created', $strLangue)
: fxEveAccesPromoteurT('eve_acces_promoteur_ok_existing', $strLangue),
'message' => $strMsg,
);
}
/**
* MSIN-4401 — révocation par le promoteur (team.manage ou auteur du grant / super admin).
* MSIN-4401 / MSIN-4478 — révocation par le promoteur (team.manage ou auteur du grant / super admin).
*/
function fxEveAccesPromoteurRevoke($intEaId, $intActorComId, $intEveId)
{
@ -531,7 +721,12 @@ function fxEveAccesPromoteurRevoke($intEaId, $intActorComId, $intEveId)
return array('state' => 'error');
}
return fxEveAccesRevoke($intEaId, $intActorComId);
$arrResult = fxEveAccesRevoke($intEaId, $intActorComId);
if (($arrResult['state'] ?? '') === 'success') {
fxEveAccesPromoteurDeactivateOrphanInviteStub(intval($tab['com_id'] ?? 0));
}
return $arrResult;
}
/**
@ -592,27 +787,12 @@ function fxEveAccesPromoteurResendMail($intEaId, $intActorComId, $intEveId, $str
$strLastVisit = trim((string)($arrCompte['com_lastvisit'] ?? ''));
$blnInviteTeam = intval($arrCompte['com_invite_team'] ?? 0) === 1;
$blnPending = ($strStatut === 'invite')
|| fxEveAccesPromoteurCompteIsPendingInvite($arrCompte)
|| ($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
);
}
$strResetToken = fxEveAccesPromoteurRefreshResetToken($intComId);
}
try {

149
php/inc_fx_eve_extra.php Normal file
View File

@ -0,0 +1,149 @@
<?php
/**
* MSIN-3487 — Métadonnées supplémentaires d'événement
* (table inscriptions_evenements_extra).
*
* Fonctions indépendantes : utilisables depuis kc.php, impression dossards,
* hub promoteur, etc. Ne pas mélanger avec key_chain (QR physiques).
*/
/**
* URL de repli si aucun lien résultats n'est configuré pour l'événement.
* Aligné sur le défaut historique de kc.php / key_chain.
*/
function fxEveExtraResultatsFallbackUrl()
{
return 'https://ms1.ca';
}
/**
* Lit la ligne extra d'un événement (ou null).
*
* @param int $intEveId
* @return array|null
*/
function fxEveExtraGetRow($intEveId)
{
global $objDatabase;
$intEveId = intval($intEveId);
if ($intEveId <= 0) {
return null;
}
$sql = 'SELECT * FROM inscriptions_evenements_extra WHERE eve_id = ' . $intEveId . ' LIMIT 1';
$tabRow = $objDatabase->fxGetRow($sql);
return ($tabRow === null || $tabRow === false) ? null : $tabRow;
}
/**
* Retourne le lien résultats brut, ou null s'il est absent / vide.
*
* @param int $intEveId
* @return string|null
*/
function fxEveExtraGetResultatsUrl($intEveId)
{
$tabRow = fxEveExtraGetRow($intEveId);
if ($tabRow === null) {
return null;
}
$strUrl = trim((string) ($tabRow['eve_resultats_url'] ?? ''));
return ($strUrl !== '') ? $strUrl : null;
}
/**
* Lien résultats à utiliser pour une redirection :
* URL configurée, sinon fallback (ms1.ca).
*
* @param int $intEveId
* @return string
*/
function fxEveExtraResolveResultatsUrl($intEveId)
{
$strUrl = fxEveExtraGetResultatsUrl($intEveId);
return ($strUrl !== null) ? $strUrl : fxEveExtraResultatsFallbackUrl();
}
/**
* Crée ou met à jour le lien résultats pour un événement.
*
* @param int $intEveId
* @param string $strUrl Chaîne vide = effacer le lien (NULL en BD)
* @return bool
*/
function fxEveExtraSetResultatsUrl($intEveId, $strUrl)
{
global $objDatabase;
$intEveId = intval($intEveId);
if ($intEveId <= 0) {
return false;
}
$strUrl = trim((string) $strUrl);
if ($strUrl === '') {
$strSqlUrl = 'NULL';
} else {
$strSqlUrl = "'" . $objDatabase->fxEscape($strUrl) . "'";
}
// MSIN-3487 — upsert : une ligne max par eve_id
$sql = 'INSERT INTO inscriptions_evenements_extra (eve_id, eve_resultats_url, eve_extra_maj)'
. ' VALUES (' . $intEveId . ', ' . $strSqlUrl . ', NOW())'
. ' ON DUPLICATE KEY UPDATE'
. ' eve_resultats_url = VALUES(eve_resultats_url),'
. ' eve_extra_maj = NOW()';
return (bool) $objDatabase->fxQuery($sql);
}
/**
* URL courte à encoder dans un QR (kc.php redirige vers les résultats).
* Exemple : https://ms1inscription.com/kc.php?t=eve&id=760
*
* @param int $intEveId
* @param string|null $strBaseUrl Défaut = $vDomaine
* @return string
*/
function fxKcEveResultatsQrUrl($intEveId, $strBaseUrl = null)
{
global $vDomaine;
$intEveId = intval($intEveId);
if ($strBaseUrl === null || trim((string) $strBaseUrl) === '') {
$strBaseUrl = !empty($vDomaine) ? $vDomaine : 'https://ms1inscription.com';
}
return rtrim($strBaseUrl, '/') . '/kc.php?t=eve&id=' . $intEveId;
}
/**
* PNG binaire du QR résultats événement (avec logo MS1).
* MSIN-3487
*
* @param int $intEveId
* @param string|null $strBaseUrl
* @param int $intModuleSize
* @param bool $blnWithLogo
* @return string|false
*/
function fxKcEveResultatsQrPngBinary($intEveId, $strBaseUrl = null, $intModuleSize = 10, $blnWithLogo = true)
{
require_once __DIR__ . '/inc_fx_kc_qr.php';
$intEveId = intval($intEveId);
if ($intEveId <= 0) {
return false;
}
return fxKcQrGeneratePngBinary(
fxKcEveResultatsQrUrl($intEveId, $strBaseUrl),
$intModuleSize,
$blnWithLogo
);
}

File diff suppressed because it is too large Load Diff

View File

@ -71,11 +71,7 @@ function fxInscrGestionCanDo($intComId, $intEveId, $strActionPerm) {
return false;
}
// MSIN-4401 — kit propriétaire / total (grants_all) = toutes les actions, sans cocher chaque droit.
if (fxEveAccesComEventHasGrantsAllKit(intval($intComId), intval($intEveId))) {
return true;
}
// MSIN-4401 / MSIN-4484 — kit total via HasPermission (respecte perm_grants_all = 0).
if (fxEveAccesHasPermission(intval($intComId), intval($intEveId), $strActionPerm)) {
return true;
}
@ -177,12 +173,7 @@ function fxInscrGestionCanScan($intComId, $intEveId) {
$intComId = intval($intComId);
$intEveId = intval($intEveId);
// MSIN-4401 — kit propriétaire / total (grants_all) = tout, sans cocher chaque droit.
if (fxEveAccesComEventHasGrantsAllKit($intComId, $intEveId)) {
return true;
}
// Même règle hub / compte /mobile : droit explicite scanner QR.
// MSIN-4401 / MSIN-4484 — kit total via HasAnyPermission (respecte perm_grants_all = 0).
if (fxEveAccesHasAnyPermission($intComId, $intEveId, fxInscrGestionPermKeysScan())) {
return true;
}
@ -502,8 +493,9 @@ function fxInscrGestionFallbackTexte($strClef, $strLangue) {
'inscr_gestion_err_refund' => $blnFr ? 'Erreur remboursement.' : 'Refund error.',
'inscr_gestion_refund_ok' => $blnFr ? 'Remboursement effectué.' : 'Refund completed.',
'inscr_gestion_refund_cancel_hint' => $blnFr
? 'Votre transaction a été remboursée. Si vous voulez aussi l\'annuler dans le système d\'inscription :'
: 'Your transaction has been refunded. If you also want to cancel it in the registration system:',
// MSIN-4419 — hint post-remboursement + bouton annuler.
? 'Votre transaction a été remboursée. Si vous voulez aussi annuler le participant cliquez sur annuler'
: 'Your transaction has been refunded. If you also want to cancel the participant, click cancel',
'inscr_gestion_col_participant' => $blnFr ? 'Participant' : 'Participant',
'inscr_gestion_col_commande' => $blnFr ? 'Commande' : 'Order',
'inscr_gestion_col_dossard' => $blnFr ? 'Dossard' : 'Bib',
@ -534,6 +526,11 @@ function fxInscrGestionFallbackTexte($strClef, $strLangue) {
'inscr_gestion_audit_field_is_cancelled' => $blnFr ? 'Annulation' : 'Cancellation',
// MSIN-4328 — bouton superadmin vers fiche ChronoTrack
'inscr_gestion_btn_chronotrack' => $blnFr ? 'Voir dans ChronoTrack' : 'View in ChronoTrack',
// MSIN-4485 — impression dossard PDF
'inscr_gestion_btn_dossard_print' => $blnFr ? 'Imprimer le dossard' : 'Print bib',
'inscr_gestion_btn_dossard_print_title' => $blnFr
? 'Générer le PDF du dossard'
: 'Generate bib PDF',
'inscr_gestion_list_count_one' => $blnFr ? '%d inscription' : '%d registration',
'inscr_gestion_list_count_many' => $blnFr ? '%d inscriptions' : '%d registrations',
'inscr_gestion_list_page' => $blnFr ? 'Page %d / %d' : 'Page %d / %d',
@ -1253,6 +1250,7 @@ function fxInscrGestionFetchFicheProfile($arrRow, $strLangue) {
$intParId = (int)$arrRow['par_id'];
$intPecId = (int)$arrRow['pec_id_original'];
$intPecPk = isset($arrRow['pec_id']) ? (int)$arrRow['pec_id'] : 0;
$intEveId = (int)$arrRow['eve_id'];
$strLangCol = ($strLangue === 'en') ? 'en' : 'fr';
@ -1289,6 +1287,15 @@ function fxInscrGestionFetchFicheProfile($arrRow, $strLangue) {
}
$intParIdOriginal = (int)$tabProfile['par_id_original'];
// MSIN-4482 — hors tx : accepter pec_id PK ou pec_id_original (réponses import).
// Pas de IFNULL(que_actif) / pas de requête te_nom ici : une SQL en erreur = plantage
// (fxGetResults → mysqli_num_rows(false)).
$strPecClause = 'rq.pec_id = ' . $intPecId;
if ($intPecPk > 0 && $intPecPk !== $intPecId) {
$strPecClause = '(rq.pec_id = ' . $intPecId . ' OR rq.pec_id = ' . $intPecPk . ')';
}
$sqlQuestions = 'SELECT rq.que_choix_' . $strLangCol . ' AS que_choix,'
. ' iq.que_cart_label_' . $strLangCol . ' AS que_label,'
. ' rq.que_question_' . $strLangCol . ' AS que_question,'
@ -1297,7 +1304,7 @@ function fxInscrGestionFetchFicheProfile($arrRow, $strLangue) {
. ' INNER JOIN inscriptions_questions iq ON iq.que_id = rq.que_id'
. ' WHERE iq.que_cart_show = 1'
. ' AND iq.eve_id = ' . $intEveId
. ' AND rq.pec_id = ' . $intPecId
. ' AND ' . $strPecClause
. ' AND (rq.par_id = ' . $intParId . ' OR rq.par_id = ' . $intParIdOriginal . ')'
. ' AND rq.que_actif = 1'
. " AND TRIM(COALESCE(rq.que_choix_" . $strLangCol . ", '')) <> ''"
@ -2461,6 +2468,21 @@ function fxInscrGestionRenderFiche($intEveId, $strLangue, $strBaseUrl, $arrReq,
}
}
}
// MSIN-4485 — Imprimer le dossard (droit hors kit total)
if (!function_exists('fxDossardPrintCan')) {
require_once __DIR__ . '/inc_fx_dossard_print.php';
}
if (fxDossardPrintCan($intComId, $intRowEveId)) {
$strPrintUrl = rtrim((string)$vDomaine, '/') . '/dossard_print.php'
. '?par_id=' . intval($arrRow['par_id'])
. '&eve_id=' . intval($intRowEveId);
echo ' <a class="btn btn-outline-primary btn-sm inscr-gestion-btn-dossard-print" href="'
. fxInscrGestionEsc($strPrintUrl) . '" target="_blank" rel="noopener noreferrer"'
. ' title="' . fxInscrGestionEsc(fxInscrGestionT('inscr_gestion_btn_dossard_print_title')) . '">'
. '<i class="fa fa-print" aria-hidden="true"></i>&nbsp;'
. fxInscrGestionEsc(fxInscrGestionT('inscr_gestion_btn_dossard_print'))
. '</a>';
}
echo '</div>';
$strDetailsType = $blnEquipe && fxIsParEquipe($arrRow['pec_id_original'], $arrRow['epr_id'])

View File

@ -123,11 +123,17 @@ function fxKcQrOverlayLogo($strQrPath, $fltLogoRatio = 0.22)
}
/**
* Génère un QR key_chain prêt pour l'impression.
* Génère un PNG QR pour une URL quelconque (niveau H, logo optionnel).
* MSIN-3487 — réutilisé key_chain + QR résultats événement.
*
* @param string $strUrl
* @param string $strOutputFile
* @param int $intModuleSize
* @param bool $blnWithLogo
* @return string Chemin du fichier généré
*/
function fxKcQrGeneratePng($intParam1, $strBaseUrl, $strOutputFile, $intModuleSize = 18, $blnWithLogo = true)
function fxKcQrGeneratePngFromUrl($strUrl, $strOutputFile, $intModuleSize = 18, $blnWithLogo = true)
{
$strUrl = rtrim($strBaseUrl, '/') . '/kc.php?param1=' . intval($intParam1);
$strDir = dirname($strOutputFile);
if (!is_dir($strDir)) {
mkdir($strDir, 0755, true);
@ -137,7 +143,7 @@ function fxKcQrGeneratePng($intParam1, $strBaseUrl, $strOutputFile, $intModuleSi
$strUrl,
$strOutputFile,
QR_ECLEVEL_H,
$intModuleSize,
max(4, (int) $intModuleSize),
2
);
@ -145,6 +151,45 @@ function fxKcQrGeneratePng($intParam1, $strBaseUrl, $strOutputFile, $intModuleSi
fxKcQrOverlayLogo($strOutputFile);
}
return $strOutputFile;
}
/**
* Génère un PNG QR en mémoire (binaire) pour une URL.
*
* @param string $strUrl
* @param int $intModuleSize
* @param bool $blnWithLogo
* @return string|false
*/
function fxKcQrGeneratePngBinary($strUrl, $intModuleSize = 10, $blnWithLogo = true)
{
$strTmpBase = tempnam(sys_get_temp_dir(), 'kcqr_');
if ($strTmpBase === false) {
return false;
}
@unlink($strTmpBase);
$strTmpPng = $strTmpBase . '.png';
fxKcQrGeneratePngFromUrl($strUrl, $strTmpPng, $intModuleSize, $blnWithLogo);
if (!is_file($strTmpPng)) {
return false;
}
$binPng = file_get_contents($strTmpPng);
@unlink($strTmpPng);
return ($binPng === false || $binPng === '') ? false : $binPng;
}
/**
* 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);
fxKcQrGeneratePngFromUrl($strUrl, $strOutputFile, $intModuleSize, $blnWithLogo);
return [
'param1' => intval($intParam1),
'url' => $strUrl,

View File

@ -927,8 +927,11 @@ LEFT JOIN inscriptions_panier_epreuves ee on e.epr_id=ee.epr_id
$intAge = fxGetAge($strNaissance, $tabEpreuve['epr_date_age']);
// if ($strPrenom != '' && $strNomFamille != '' && $strSexe != '' && $strNaissance != '') {
// MSIN-4474 — ne pas écraser l'identité si prénom/nom absents du POST (champs disabled)
if ((isset($_SESSION['no_panier']) && $tabData['promoteur'] == 0) || (!isset($_SESSION['no_panier']) && $tabData['promoteur'] == 1 && $tabEvenement['general']['te_id'] != 13) || (!isset($_SESSION['no_panier']) && $tabData['promoteur'] == 0 && $i > 1)) {
$strFields = ", par_prenom = '" . $objDatabase->fxEscape($strPrenom) . "', par_nom = '" . $objDatabase->fxEscape($strNomFamille) . "', par_naissance = '" . $objDatabase->fxEscape($strNaissance) . "', par_sexe = '" . $objDatabase->fxEscape($strSexe) . "'";
if (isset($tabData['par_prenom_' . $i]) && isset($tabData['par_nom_' . $i])) {
$strFields = ", par_prenom = '" . $objDatabase->fxEscape($strPrenom) . "', par_nom = '" . $objDatabase->fxEscape($strNomFamille) . "', par_naissance = '" . $objDatabase->fxEscape($strNaissance) . "', par_sexe = '" . $objDatabase->fxEscape($strSexe) . "'";
}
}
$sqlUpdatePart = "UPDATE " . $strTableSqlPart . " SET " . $strModMarker . "epr_id = " . intval($intEprId) . ", par_age = " . intval($intAge) . ", pro_id = " . intval($intProvince) . ", par_codepostal = '" . $objDatabase->fxEscape($strCodepostal) . "', pay_id = " . intval($intPays) . ", par_adresse = '" . $objDatabase->fxEscape($strAdresse) . "', par_adresse2 = '" . $objDatabase->fxEscape($strAdresse2) . "', par_ville = '" . $objDatabase->fxEscape($strVille) . "', par_maj = '" . fxGetDateTime() . "', par_telephone1 = '" . $objDatabase->fxEscape($strTelephone1) . "', par_telephone2 = '" . $objDatabase->fxEscape($strTelephone2) . "', par_nom_equipe = '" . $objDatabase->fxEscape($strParNomEquipe) . "', par_courriel = '" . $objDatabase->fxEscape($strCourriel) . "', par_equipe = " . intval($tabEpreuve['epr_equipe']) . $strCancel . $strFields . ", par_contact_urgence_nom = '" . $objDatabase->fxEscape($strUrgenceNom) . "', par_contact_urgence_telephone = '" . $objDatabase->fxEscape($strUrgenceTel) . "' WHERE " . $strWhereSql . " = " . intval($intParId);
@ -1270,9 +1273,14 @@ LEFT JOIN inscriptions_panier_epreuves ee on e.epr_id=ee.epr_id
else
$strPage = 'account';
if ($_POST['promoteur'] == 1)
header("Location: " . $vDomaine . '/' . $strPage . '/inc_tableau_promoteur?promoteur_eve_id=' . urlencode(base64_encode($tabEvenement['general']['eve_id'])));
else
if ($_POST['promoteur'] == 1) {
// MSIN-4474 — retour à la page d'origine (gestion / promoteur), sinon tableau promoteur legacy
$strReturnAfterMod = fxModInscriptionReturnUrl('');
if ($strReturnAfterMod === '') {
$strReturnAfterMod = $vDomaine . '/' . $strPage . '/inc_tableau_promoteur?promoteur_eve_id=' . urlencode(base64_encode($tabEvenement['general']['eve_id']));
}
header("Location: " . $strReturnAfterMod);
} else
header("Location: " . $vDomaine . '/' . $strPage);
} else {
header("Location: " . $tabData['url']);

View File

@ -181,7 +181,8 @@ function fxShowInfosParticipants(&$tabRules, &$tabMessages, &$tabCalendriers, $t
$strFieldDisabled = 'disabled_';
}
} elseif ($strAction == 'mod') {
if ((!isset($_SESSION ['no_panier']) && !fxIsPromoteur($_SESSION['com_info']['com_id'], $tabEpreuve['eve_id'])) || intval($tabEpreuve['tep_id']) == 4) {
// MSIN-4474 — promoteur=1 (URL) : garder prénom/nom éditables (évite POST vide → nom effacé)
if ((!isset($_SESSION ['no_panier']) && intval($intPromoteur) != 1 && !fxIsPromoteur($_SESSION['com_info']['com_id'], $tabEpreuve['eve_id'])) || intval($tabEpreuve['tep_id']) == 4) {
$strDisabled = "disabled";
$strFieldDisabled = 'disabled_';
}
@ -284,7 +285,9 @@ function fxShowInfosParticipants(&$tabRules, &$tabMessages, &$tabCalendriers, $t
<?php
} else {
if ($tabInfos[$intCtrInfos]['cha_nom'] == 'par_nom' || $tabInfos[$intCtrInfos]['cha_nom'] == 'par_prenom') {
if ($strAction == 'mod' && !isset($_SESSION ['no_panier']) && $intPromoteur == 0 && $intCtr == 1) {
// MSIN-4474 — hidden de secours si le champ visible est disabled (client ou promoteur)
if ($strAction == 'mod' && !isset($_SESSION ['no_panier']) && $intCtr == 1
&& (intval($intPromoteur) == 0 || trim($strDisabled) != '')) {
?>
<input type="hidden" name="<?php echo $strField; ?>"
value="<?php echo (isset($tabParticipant[$intCtr][$tabInfos[$intCtrInfos]['cha_nom']])) ? ($tabParticipant[$intCtr][$tabInfos[$intCtrInfos]['cha_nom']]) : (""); ?>">

File diff suppressed because it is too large Load Diff

View File

@ -642,10 +642,10 @@ function fxPromoteurHubGetEventLinks($arrItem, $strLangue)
$blnLegacyPromoteur = fxIsPromoteur($intComId, $intEveId);
// Menus « tout ouvert » legacy seulement si PAS de kit V2 sur cet événement
$blnLegacyMenus = $blnLegacyPromoteur && !$blnHasV2;
$blnGrantsAll = $blnHasV2 && fxEveAccesComEventHasGrantsAllKit($intComId, $intEveId);
$fnCan = function ($arrKeys) use ($intComId, $intEveId, $blnSuper, $blnLegacyMenus, $blnGrantsAll) {
if ($blnSuper || $blnLegacyMenus || $blnGrantsAll) {
// MSIN-4484 — kit total : passer par HasAnyPermission (exclut perm_grants_all = 0).
$fnCan = function ($arrKeys) use ($intComId, $intEveId, $blnSuper, $blnLegacyMenus) {
if ($blnSuper || $blnLegacyMenus) {
return true;
}
return fxEveAccesHasAnyPermission($intComId, $intEveId, $arrKeys);
@ -812,8 +812,9 @@ function fxPromoteurHubRenderLinkList($arrLinks, $strDefaultIcon)
$strIcon = !empty($arrLink['icon']) ? $arrLink['icon'] : $strDefaultIcon;
$strTarget = !empty($arrLink['target']) ? ' target="' . fxPromoteurHubEsc($arrLink['target']) . '"' : '';
// MSIN-4473 — coins alignés sur les tuiles outils dossards (border-radius 8px)
$html .= '<li class="mb-2">';
$html .= '<a class="btn btn-outline-primary btn-block text-left rounded-0" href="' . fxPromoteurHubEsc($arrLink['url']) . '"' . $strTarget . '>';
$html .= '<a class="btn btn-outline-primary btn-block text-left promoteur-hub-link" href="' . fxPromoteurHubEsc($arrLink['url']) . '"' . $strTarget . '>';
$html .= '<i class="fa ' . fxPromoteurHubEsc($strIcon) . ' mr-2" aria-hidden="true"></i>' . fxPromoteurHubEsc($arrLink['label']);
$html .= '</a></li>';
}
@ -1586,7 +1587,8 @@ function fxPromoteurHubCanViewDashboard($intComId, $intEveId)
require_once __DIR__ . '/inc_fonctions.php';
}
if (fxEveAccesComEventHasGrantsAllKit($intComId, $intEveId)) {
// MSIN-4484 — kit total respecté via HasPermission (perm_grants_all).
if (fxEveAccesHasPermission($intComId, $intEveId, 'hub.dashboard')) {
return true;
}
@ -1595,7 +1597,7 @@ function fxPromoteurHubCanViewDashboard($intComId, $intEveId)
return true;
}
return fxEveAccesHasPermission($intComId, $intEveId, 'hub.dashboard');
return false;
}
function fxPromoteurHubRenderEventCard($arrItem, $strLangue, $blnArchived = false)

View File

@ -433,13 +433,16 @@ function fxShowFormParticipant($tabEvenement, $strAction, $strLangue, $intEpreuv
</button>
<?php
} elseif ($strAction == 'mod') {
// MSIN-4474 — conserver la page d'origine (liste promoteur / gestion inscriptions)
$strReturnUrl = fxModInscriptionReturnUrl(fxGetReferer());
?>
<input type="hidden" id="promoteur_eve_id" name="promoteur_eve_id" value="<?php echo $tabEvenement['general']['eve_id']; ?>">
<input type="hidden" id="return_url" name="return_url" value="<?php echo htmlspecialchars($strReturnUrl, ENT_QUOTES, 'UTF-8'); ?>">
<button class="btn btn-primary rounded-0" id="btn_enregistrer_modif" name="btn_enregistrer_modif" type="submit">
<i class="fa fa-check mr-2" aria-hidden="true"></i>
<?php echo afficheTexte('btn_enregistrer', 0, 1, 1) ?>
</button>
<button class="btn btn-secondary rounded-0 btn_page_prec" data-code="<?php echo fxGetReferer(); ?>" type="button">
<button class="btn btn-secondary rounded-0 btn_page_prec" data-code="<?php echo htmlspecialchars($strReturnUrl, ENT_QUOTES, 'UTF-8'); ?>" type="button">
<i class="fa fa-cart-plus mr-2" aria-hidden="true"></i>
<?php afficheTexte('btn_annulerinscriptions'); ?>
</button>
@ -946,11 +949,32 @@ function fxCheckAccesEpreuve($strAction, $intEpreuve, $intEvenement, $intpec_id,
$blnReturn = true;
}
} elseif (intval($intPromoteur) == 1 && isset($_SESSION['com_info']) && intval($intpec_id) != 0) { // si modif par promoteur
$arrEvenements = explode(',', $_SESSION['com_info']['com_eve_promoteur']);
// MSIN-4474 — legacy CSV + super-admin + accès v2 (sinon Suivant changement d'épreuve absent)
$strCsvPromoteur = isset($_SESSION['com_info']['com_eve_promoteur'])
? (string)$_SESSION['com_info']['com_eve_promoteur']
: '';
$arrEvenements = array();
foreach (array_map('trim', explode(',', $strCsvPromoteur)) as $strEveId) {
if ($strEveId !== '') {
$arrEvenements[] = intval($strEveId);
}
}
if (in_array($intEvenement, $arrEvenements)) {
if (in_array(intval($intEvenement), $arrEvenements, true)) {
$blnReturn = true;
}
if (!$blnReturn) {
if (!function_exists('fxEveAccesIsSuperAdminSession')) {
require_once dirname(__FILE__) . '/inc_fx_eve_acces.php';
}
if (fxEveAccesIsSuperAdminSession()) {
$blnReturn = true;
} elseif (!empty($_SESSION['com_info']['com_id'])
&& fxEveAccesHasInscrGestionWebAccess(intval($_SESSION['com_info']['com_id']), intval($intEvenement))) {
$blnReturn = true;
}
}
} elseif (intval($intPromoteur) == 0 && isset($_SESSION['com_info']) && !isset($_SESSION['no_panier']) && intval($intpec_id) != 0) { // si modif d'une épreuve déjà commandée
$sqlEpreuve = "SELECT COUNT(*) FROM resultats_epreuves_commandees WHERE pec_id_original = " . intval($intpec_id) . " AND com_id = " . intval($_SESSION['com_info']['com_id']) . $strWhere;
$nbEpreuve = $objDatabase->fxGetVar($sqlEpreuve);

File diff suppressed because it is too large Load Diff

View File

@ -43,18 +43,24 @@ function fxStaticSyncTableDefinitions() {
'label' => 'Accès v2 — permissions',
'keys' => array('perm_key'),
'ignore_cols' => array(),
// CON-333 — catalogue : les en trop peuvent être retirés pour coller à la préprod.
'allow_remove_extras' => true,
),
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'),
'allow_remove_extras' => true,
),
array(
'table' => 'inscriptions_eve_role_permissions',
'label' => 'Accès v2 — matrice rôles',
'keys' => array('role_id', 'perm_key'),
'ignore_cols' => array(),
// CON-333 — role_id change entre BD ; comparer / sync via role_code.
'keys' => array('role_code', 'perm_key'),
'ignore_cols' => array('role_id'),
'join_role_code' => true,
'allow_remove_extras' => true,
),
array(
'table' => 'inscriptions_pages',
@ -128,6 +134,18 @@ function fxStaticSyncTableDefinitions() {
'keys' => array('reg_id'),
'ignore_cols' => array(),
),
// CON-333 — seeds traçabilité fiche (MSIN-4464) ; log runtime exclu.
array(
'table' => 'inscriptions_fiche_audit_settings',
'label' => 'Audit fiche — réglages',
'keys' => array('setting_id'),
'ignore_cols' => array('setting_updated_at'),
),
array(
'table' => 'inscriptions_fiche_audit_fields',
'label' => 'Audit fiche — champs',
'keys' => array('field_key'),
'ignore_cols' => array('field_updated_at'),
),
);
}

View File

@ -7,8 +7,8 @@
* Constantes *
*
**************/
define('_VERSION_CODE', '4.72.843');
define('_DATE_CODE', '2026-07-21');
define('_VERSION_CODE', '4.72.869'); // MSIN-4479 — cache-bust bib-assignments (alert qte)
define('_DATE_CODE', '2026-07-24');
//MSIN-4290
define('QR_SECRET_KEY', 'ms1_qr_2026_cle_secrete_longue_et_fixe');
@ -30,7 +30,7 @@ define('KEYCHAIN_HTTP_EDIT_ALLOWED_COM_IDS', '');
// 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',
'stephan@progiweb.ca','desjardins.lp@ms1timing.com',
);
/**************************************

View File

@ -1,8 +1,8 @@
<?php
/**
* Tableau de bord sync tables statiques — préprod dev, superadm seulement.
* Source : devinscription_preprod
* CON-333 — Tableau de bord sync tables statiques (soft).
* Source : devinscription_preprod. Aucun DELETE — insert manquants + réparation poubelles info.
*/
require_once('inc_start_time.php');
@ -26,6 +26,24 @@ function ssEsc($str) {
return htmlspecialchars((string)$str, ENT_QUOTES, 'UTF-8');
}
function ssSoftActionLabel($strType) {
if ($strType === 'extra') {
return 'en trop (cible seulement — ne pas effacer auto)';
}
if ($strType === 'missing') {
return 'manque en cible';
}
if ($strType === 'poubelle') {
return 'poubelle réparable';
}
if ($strType === 'diff') {
return 'contenu différent — Appliquer corrections';
}
return 'contenu différent';
}
$strCsrf = fxStaticSyncEnsureCsrfToken();
$strFlash = '';
$strFlashType = 'info';
@ -36,16 +54,24 @@ if (is_array($tabPopFlash)) {
$strFlashType = $tabPopFlash['type'];
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'sync') {
// CON-333 — actions soft : apply_missing | apply_content (préprod = vérité textes).
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
$strAction = trim($_POST['action'] ?? '');
if (!fxStaticSyncValidateCsrf($_POST['csrf'] ?? '')) {
fxStaticSyncSetFlash('Jeton CSRF invalide.', 'bad');
} elseif (!in_array($strAction, array('apply_missing', 'apply_content', 'apply_all', 'remove_extras', 'remove_info_poubelle_extras'), true)) {
fxStaticSyncSetFlash(
'Action refusée. Sync destructif désactivé (CON-333). '
. 'Utiliser Appliquer manquants / corrections / Retirer en trop / Retirer poubelles info.',
'bad'
);
} else {
$strTable = trim($_POST['table'] ?? '');
$strEnvId = trim($_POST['env'] ?? '');
$tabDef = fxStaticSyncFindTableDef($strTable);
$tabEnv = fxStaticSyncFindEnvDef($strEnvId);
if (!$tabDef || !$tabEnv || !empty($tabEnv['is_source'])) {
if (!$tabEnv || !empty($tabEnv['is_source'])) {
fxStaticSyncSetFlash('Paramètres invalides.', 'bad');
} else {
$tabConnections = fxStaticSyncOpenConnections();
@ -57,24 +83,54 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['
'Connexion BD impossible — ' . fxStaticSyncFormatConnectionError($tabEnv),
'bad'
);
} elseif ($strAction === 'apply_all') {
$tabResult = fxStaticSyncApplyAllSoft($objSource, $objTarget, $strEnvId);
$strMsg = $tabEnv['label'] . ' — ' . $tabResult['message'];
if (!empty($tabResult['lines'])) {
$strMsg .= "\n" . implode("\n", $tabResult['lines']);
}
fxStaticSyncSetFlash($strMsg, !empty($tabResult['ok']) ? 'ok' : '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');
}
$tabDef = fxStaticSyncFindTableDef($strTable);
if (!$tabDef) {
fxStaticSyncSetFlash('Paramètres invalides.', 'bad');
} else {
fxStaticSyncSetFlash(
$tabDef['label'] . ' → ' . $tabEnv['label'] . ' — échec : ' . $tabResult['message'],
'bad'
);
if ($strAction === 'apply_missing') {
$tabResult = fxStaticSyncApplyMissingRows($objSource, $objTarget, $strTable, $tabDef);
} elseif ($strAction === 'remove_extras') {
$tabResult = fxStaticSyncRemoveExtras($objSource, $objTarget, $strTable, $tabDef);
} elseif ($strAction === 'remove_info_poubelle_extras') {
if ($tabDef['table'] !== 'info') {
$tabResult = array('ok' => false, 'message' => 'Réservé à info.');
} else {
$tabResult = fxStaticSyncRemoveInfoExtraPoubelle($objSource, $objTarget, $tabDef);
}
} else {
$tabResult = fxStaticSyncApplyContentUpdates($objSource, $objTarget, $strTable, $tabDef);
}
if ($tabResult['ok']) {
$tabVerify = fxStaticSyncCompareEnvPair($objSource, $objTarget, $tabDef);
$strMsg = $tabDef['label'] . ' → ' . $tabEnv['label'] . ' : ' . $tabResult['message'];
$intExtra = count($tabVerify['extra_keys']);
$intMiss = count($tabVerify['missing_keys']);
$intDiff = count($tabVerify['diff_keys']);
$strMsg .= ' Après : manque ' . $intMiss
. ', contenu diff. ' . $intDiff
. ', en trop (conservés) ' . $intExtra . '.';
if ($intMiss === 0 && $intDiff === 0 && $tabVerify['status'] === 'ok') {
fxStaticSyncSetFlash($strMsg, 'ok');
} elseif ($intMiss === 0 && $intDiff === 0) {
fxStaticSyncSetFlash($strMsg . ' Écarts restants = revue manuelle (pas de DELETE).', 'ok');
} else {
fxStaticSyncSetFlash($strMsg, 'ok');
}
} else {
fxStaticSyncSetFlash(
$tabDef['label'] . ' → ' . $tabEnv['label'] . ' — échec : ' . $tabResult['message'],
'bad'
);
}
}
}
}
@ -100,7 +156,7 @@ if ($strDetailTable !== '' && $strDetailEnv !== '') {
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Sync tables statiques</title>
<title>Sync tables statiques (soft) — CON-333</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; background: #f4f6f8; color: #222; }
h1 { font-size: 1.4rem; margin: 0 0 8px; }
@ -109,8 +165,11 @@ if ($strDetailTable !== '' && $strDetailEnv !== '') {
.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; }
.warn-box { background: #e8f5e9; border: 1px solid #a5d6a7; padding: 12px 14px; border-radius: 6px; margin-bottom: 16px; font-size: 0.9rem; }
.warn-box strong { color: #1b5e20; }
.proc { background: #fff; border: 1px solid #ccc; padding: 12px 14px; border-radius: 6px; margin-bottom: 16px; font-size: 0.9rem; }
.proc ol { margin: 8px 0 0; padding-left: 22px; }
.proc li { margin-bottom: 4px; }
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; }
@ -122,9 +181,12 @@ if ($strDetailTable !== '' && $strDetailEnv !== '') {
.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 { display: inline-block; margin-top: 6px; margin-right: 4px; 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; }
.btn-soft { border-color: #2e7d32; color: #1b5e20; }
.btn-content { border-color: #1565c0; color: #0d47a1; }
.btn-remove { border-color: #c62828; color: #b71c1c; }
.btn-repair { border-color: #ef6c00; color: #e65100; }
.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; }
@ -136,11 +198,31 @@ if ($strDetailTable !== '' && $strDetailEnv !== '') {
</head>
<body>
<h1>Sync tables statiques</h1>
<h1>Sync tables statiques — mode soft (CON-333)</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.
<strong>Aucun DELETE.</strong>
On nefface jamais un <strong>vrai</strong> libellé <code>info</code> seulement en cible.
Les <strong>poubelles</strong> info en trop (texte = clé) : bouton dédié pour les retirer.
Catalogues accès v2 : Retirer en trop (sauf rôle encore utilisé).
</div>
<div class="proc">
<strong>Test complet (un environnement à la fois)</strong>
<ol>
<li>Rafraîchir cette page (nouveau code déployé).</li>
<li>Cliquer <em>TOUT APPLIQUER</em> sous la colonne voulue (commencer par <strong>Dev prod</strong>).</li>
<li>Lire le message vert/rouge en haut : succès ou échec + détail table par table.</li>
<li>Recharger : Manque=0 et Diff=0. Sur accès v2, En trop aussi → 0 (retirés auto). Sur <code>info</code>, En trop = à revoir via Détails (jamais DELETE auto).</li>
<li>Lignes SAUTÉES (structure / table absente) = à corriger en Navicat, puis re-cliquer.</li>
</ol>
<form method="post" style="margin-top:12px;" onsubmit="return confirm('CON-333 — TOUT APPLIQUER sur Dev prod ?\nToutes les tables : manquants + corrections.\nAucun DELETE.');">
<input type="hidden" name="action" value="apply_all">
<input type="hidden" name="csrf" value="<?php echo ssEsc($strCsrf); ?>">
<input type="hidden" name="env" value="dev_prod">
<button type="submit" class="btn btn-content" style="font-weight:700;padding:8px 14px;">TOUT APPLIQUER → Dev prod</button>
</form>
</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.
@ -197,12 +279,30 @@ if ($strDetailTable !== '' && $strDetailEnv !== '') {
}
$strClass = fxStaticSyncStatusClass($tabCmp['status']);
$strLabel = fxStaticSyncStatusLabel($tabCmp['status'], $tabCmp);
$intMissing = isset($tabCmp['missing_keys']) ? count($tabCmp['missing_keys']) : 0;
$intExtra = isset($tabCmp['extra_keys']) ? count($tabCmp['extra_keys']) : 0;
$intDiff = isset($tabCmp['diff_keys']) ? count($tabCmp['diff_keys']) : 0;
$intPoubExtra = isset($tabCmp['poubelle_extra_keys']) ? count($tabCmp['poubelle_extra_keys']) : 0;
$intRealExtra = isset($tabCmp['real_extra_keys']) ? count($tabCmp['real_extra_keys']) : 0;
$blnCanRemoveExtras = !empty($tabDef['allow_remove_extras']);
$blnCanWrite = ($tabCmp['status'] !== 'no_connection' && $tabCmp['status'] !== 'schema_mismatch'
&& $tabCmp['status'] !== 'missing_table' && $tabCmp['status'] !== 'error');
?>
<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 ($blnCanWrite) { ?>
<div class="count-hint">
Manque <?php echo (int)$intMissing; ?>
· Diff <?php echo (int)$intDiff; ?>
· En trop <?php echo (int)$intExtra; ?>
<?php if ($tabDef['table'] === 'info' && $intExtra > 0) { ?>
<br>dont poubelles <?php echo (int)$intPoubExtra; ?> · vrais textes <?php echo (int)$intRealExtra; ?>
<?php } ?>
</div>
<?php } ?>
<?php if ($tabCmp['status'] === 'schema_mismatch') {
$tabSchemaDiff = array(
'missing_in_target' => isset($tabCmp['schema_missing_cols']) ? $tabCmp['schema_missing_cols'] : array(),
@ -220,13 +320,40 @@ if ($strDetailTable !== '' && $strDetailEnv !== '') {
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">
<?php if ($blnCanWrite && $intMissing > 0) { ?>
<form method="post" style="display:inline;" onsubmit="return confirm('CON-333 — Ajouter <?php echo (int)$intMissing; ?> ligne(s) manquante(s) dans <?php echo ssEsc($tabEnv['label']); ?> ?\nAucun DELETE.');">
<input type="hidden" name="action" value="apply_missing">
<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>
<button type="submit" class="btn btn-soft">Appliquer manquants (<?php echo (int)$intMissing; ?>)</button>
</form>
<?php } ?>
<?php if ($blnCanWrite && $intDiff > 0) { ?>
<form method="post" style="display:inline;" onsubmit="return confirm('CON-333 — Écraser <?php echo (int)$intDiff; ?> ligne(s) en cible avec le contenu préprod ?\nLes clés en trop en cible ne sont pas touchées. Aucun DELETE.');">
<input type="hidden" name="action" value="apply_content">
<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-content">Appliquer corrections (<?php echo (int)$intDiff; ?>)</button>
</form>
<?php } ?>
<?php if ($blnCanWrite && $blnCanRemoveExtras && $intExtra > 0) { ?>
<form method="post" style="display:inline;" onsubmit="return confirm('CON-333 — Retirer <?php echo (int)$intExtra; ?> en trop dans <?php echo ssEsc($tabEnv['label']); ?> ?\nCatalogue accès v2 seulement.\nLes rôles encore utilisés (inscriptions_eve_acces) ne seront PAS effacés.');">
<input type="hidden" name="action" value="remove_extras">
<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-remove">Retirer en trop (<?php echo (int)$intExtra; ?>)</button>
</form>
<?php } ?>
<?php if ($blnCanWrite && $tabDef['table'] === 'info' && $intPoubExtra > 0) { ?>
<form method="post" style="display:inline;" onsubmit="return confirm('CON-333 — Retirer <?php echo (int)$intPoubExtra; ?> poubelle(s) info en trop ?\nSeules les lignes texte=clé absentes de la préprod.\nLes <?php echo (int)$intRealExtra; ?> vrais libellés seulement en cible restent.');">
<input type="hidden" name="action" value="remove_info_poubelle_extras">
<input type="hidden" name="csrf" value="<?php echo ssEsc($strCsrf); ?>">
<input type="hidden" name="table" value="info">
<input type="hidden" name="env" value="<?php echo ssEsc($strEnvId); ?>">
<button type="submit" class="btn btn-remove">Retirer poubelles en trop (<?php echo (int)$intPoubExtra; ?>)</button>
</form>
<?php } ?>
</td>
@ -240,51 +367,75 @@ if ($strDetailTable !== '' && $strDetailEnv !== '') {
$tabCmp = $tabDetail['compare'];
$tabDef = fxStaticSyncFindTableDef($strDetailTable);
$tabEnv = fxStaticSyncFindEnvDef($strDetailEnv);
$intMissing = count($tabCmp['missing_keys']);
$intExtra = count($tabCmp['extra_keys']);
$intDiff = count($tabCmp['diff_keys']);
$tabMissingSamples = isset($tabDetail['samples_missing']) ? $tabDetail['samples_missing'] : array();
$tabDiffSamples = isset($tabDetail['samples_diff']) ? $tabDetail['samples_diff'] : array();
$tabExtraSamples = isset($tabDetail['samples_extra']) ? $tabDetail['samples_extra'] : array();
?>
<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 } } ?>
Manque <strong><?php echo (int)$intMissing; ?></strong>
· Contenu différent <strong><?php echo (int)$intDiff; ?></strong>
· En trop en cible <strong><?php echo (int)$intExtra; ?></strong>
<a class="btn" href="sync_static_db.php" style="margin-left:12px;">Fermer</a>
</p>
<?php if (!empty($tabDetail['samples'])) { ?>
<?php if (!empty($tabDetail['note'])) { ?>
<p class="count-hint"><?php echo ssEsc($tabDetail['note']); ?></p>
<?php } ?>
<?php if (!empty($tabDetail['extra_context'])) { ?>
<p class="warn-box" style="margin-top:10px;"><?php echo ssEsc($tabDetail['extra_context']); ?></p>
<?php } ?>
<?php if (count($tabExtraSamples) > 0) { ?>
<h3 style="margin-top:18px;font-size:1rem;">En trop en cible (<?php echo (int)$intExtra; ?>) — explications</h3>
<ul class="detail-list">
<?php foreach ($tabDetail['samples'] as $tabSample) { ?>
<?php foreach ($tabExtraSamples 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>
<br><?php echo ssEsc(isset($tabSample['explain']) ? $tabSample['explain'] : ssSoftActionLabel('extra')); ?>
<?php if ($tabDef['table'] === 'info' && !empty($tabSample['target_texte'])) { ?>
<br>Texte cible : <em><?php echo ssEsc($tabSample['target_texte']); ?></em>
<?php } ?>
</li>
<?php } ?>
</ul>
<?php } else { ?>
<?php if ($intExtra > count($tabExtraSamples)) { ?>
<p class="count-hint">Affichage limité à <?php echo count($tabExtraSamples); ?> / <?php echo (int)$intExtra; ?>.</p>
<?php } ?>
<?php } ?>
<?php if (count($tabMissingSamples) > 0) { ?>
<h3 style="margin-top:18px;font-size:1rem;">Manquants (<?php echo (int)$intMissing; ?>)</h3>
<ul class="detail-list">
<?php foreach ($tabMissingSamples as $tabSample) { ?>
<li>
<strong><?php echo ssEsc(fxStaticSyncFormatKeyPreview($tabSample['key'])); ?></strong>
— <?php echo ssEsc(isset($tabSample['explain']) ? $tabSample['explain'] : 'manque'); ?>
</li>
<?php } ?>
</ul>
<?php } ?>
<?php if (count($tabDiffSamples) > 0 && $intDiff > 0) { ?>
<h3 style="margin-top:18px;font-size:1rem;">Contenu différent (<?php echo (int)$intDiff; ?>)</h3>
<ul class="detail-list">
<?php foreach ($tabDiffSamples as $tabSample) { ?>
<li>
<strong><?php echo ssEsc(fxStaticSyncFormatKeyPreview($tabSample['key'])); ?></strong>
— <?php echo ssEsc(isset($tabSample['explain']) ? $tabSample['explain'] : 'diff'); ?>
<?php if ($tabDef['table'] === 'info') { ?>
<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 } ?>
<?php if ($intMissing === 0 && $intDiff === 0 && $intExtra === 0) { ?>
<p>Aucun écart à afficher.</p>
<?php } ?>
</div>

Binary file not shown.

View File

@ -0,0 +1,24 @@
-- MSIN-3487 — Table supplémentaire par événement (lien résultats, etc.)
-- Prérequis : aucun
-- Notes prod : idempotent (CREATE IF NOT EXISTS) ; pas de FK (pattern projet)
--
-- IMPORTANT — sélectionner la base AVANT d'exécuter (sinon erreur 1046).
-- Décommenter UNE ligne USE ci-dessous selon l'environnement :
--
-- USE devinscription_preprod;
-- USE devinscription_prod;
-- USE ms1inscription_preprod;
-- USE ms1inscription_prod;
SET NAMES utf8mb4;
CREATE TABLE IF NOT EXISTS `inscriptions_evenements_extra` (
`eve_id` int(10) unsigned NOT NULL
COMMENT 'MSIN-3487 — PK = inscriptions_evenements.eve_id',
`eve_resultats_url` varchar(500) DEFAULT NULL
COMMENT 'MSIN-3487 — URL publique des résultats de course',
`eve_extra_maj` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP
COMMENT 'MSIN-3487 — dernière mise à jour de la ligne',
PRIMARY KEY (`eve_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
COMMENT='MSIN-3487 — métadonnées supplémentaires d''événement (hors table principale)';

View File

@ -200,8 +200,8 @@ INSERT INTO info (info_clef, info_langue, info_texte, info_aide, info_prg, info_
('inscr_gestion_err_refund', 'en', 'Refund error.', '', 'compte.php', '', 0, 1, '', '', '', NOW()),
('inscr_gestion_refund_ok', 'fr', 'Remboursement effectué.', '', 'compte.php', '', 0, 1, '', '', '', NOW()),
('inscr_gestion_refund_ok', 'en', 'Refund completed.', '', 'compte.php', '', 0, 1, '', '', '', NOW()),
('inscr_gestion_refund_cancel_hint', 'fr', 'Votre transaction a été remboursée. Si vous voulez aussi l''annuler dans le système d''inscription :', '', 'compte.php', '', 0, 1, '', '', '', NOW()),
('inscr_gestion_refund_cancel_hint', 'en', 'Your transaction has been refunded. If you also want to cancel it in the registration system:', '', 'compte.php', '', 0, 1, '', '', '', NOW()),
('inscr_gestion_refund_cancel_hint', 'fr', 'Votre transaction a été remboursée. Si vous voulez aussi annuler le participant cliquez sur annuler', '', 'compte.php', '', 0, 1, '', '', '', NOW()),
('inscr_gestion_refund_cancel_hint', 'en', 'Your transaction has been refunded. If you also want to cancel the participant, click cancel', '', 'compte.php', '', 0, 1, '', '', '', NOW()),
('promoteur_hub_load_menu', 'fr', 'Chargement du menu…', '', 'compte.php', '', 0, 1, '', '', '', NOW()),
('promoteur_hub_load_menu', 'en', 'Loading menu…', '', 'compte.php', '', 0, 1, '', '', '', NOW()),
('promoteur_hub_load_dashboard', 'fr', 'Chargement du tableau de bord…', '', 'compte.php', '', 0, 1, '', '', '', NOW()),

View File

@ -0,0 +1,15 @@
-- MSIN-4419 — Hint post-remboursement (annuler le participant)
-- Prérequis : clés déjà créées (MSIN-4401-v2-info-complet.sql)
-- Idempotent : UPDATE ciblé FR/EN sur compte.php
UPDATE info
SET info_texte = 'Votre transaction a été remboursée. Si vous voulez aussi annuler le participant cliquez sur annuler'
WHERE info_clef = 'inscr_gestion_refund_cancel_hint'
AND info_langue = 'fr'
AND info_prg = 'compte.php';
UPDATE info
SET info_texte = 'Your transaction has been refunded. If you also want to cancel the participant, click cancel'
WHERE info_clef = 'inscr_gestion_refund_cancel_hint'
AND info_langue = 'en'
AND info_prg = 'compte.php';

View File

@ -0,0 +1,15 @@
-- MSIN-4445 — Assignation libre : toutes les épreuves solo (avec ou sans séquence)
-- Idempotent. Met à jour le hint Info du bloc libre.
-- Prérequis : sql/MSIN-4445-bib-global-free.sql déjà exécuté.
UPDATE info
SET info_texte = 'Toutes les épreuves solo : choisissez Début et Fin (ou Qté). Les dossards sont assignés sans créer de séquence, même si une séquence existe déjà. Le Go reste disponible même si la quantité n''est pas juste.'
WHERE info_clef = 'bib_v4_global_batch_free_hint'
AND info_langue = 'fr'
AND info_prg = 'compte.php';
UPDATE info
SET info_texte = 'All solo races: choose Start and End (or Qty). Bibs are assigned without creating a sequence, even if a sequence already exists. Go stays available even if the quantity is not exact.'
WHERE info_clef = 'bib_v4_global_batch_free_hint'
AND info_langue = 'en'
AND info_prg = 'compte.php';

View File

@ -11,12 +11,17 @@ WHERE info_prg = 'compte.php'
'bib_v5_workspace_operations',
'bib_v5_workspace_settings',
'bib_v5_preparation_intro',
'bib_v5_group_quantities',
'bib_v5_group_bibs',
'bib_v5_modes_intro',
'bib_v5_stat_races',
'bib_v5_stat_registered',
'bib_v5_stat_assigned',
'bib_v5_stat_pending',
'bib_v5_race_ready',
'bib_v5_race_incomplete',
'bib_v5_quantities_title',
'bib_v5_bib_assignment_title',
'bib_v5_ranges_title',
'bib_v5_ranges_hint',
'bib_v5_method_title',

View File

@ -42,6 +42,12 @@ FROM (
UNION ALL SELECT 'bib_v5_workspace_settings', 'en', 'Settings', 'Rules that apply to the entire event.'
UNION ALL SELECT 'bib_v5_preparation_intro', 'fr', 'Préparez les épreuves et attribuez les dossards aux inscriptions.', ''
UNION ALL SELECT 'bib_v5_preparation_intro', 'en', 'Prepare races and assign bibs to registrations.', ''
UNION ALL SELECT 'bib_v5_group_quantities', 'fr', 'Quantités', ''
UNION ALL SELECT 'bib_v5_group_quantities', 'en', 'Quantities', ''
UNION ALL SELECT 'bib_v5_group_bibs', 'fr', 'Dossards', ''
UNION ALL SELECT 'bib_v5_group_bibs', 'en', 'Bibs', ''
UNION ALL SELECT 'bib_v5_modes_intro', 'fr', 'Utilisez les modes dassignation qui traitent plusieurs épreuves ou des besoins particuliers.', ''
UNION ALL SELECT 'bib_v5_modes_intro', 'en', 'Use assignment modes that process multiple races or special requirements.', ''
UNION ALL SELECT 'bib_v5_stat_races', 'fr', 'Épreuves actives', ''
UNION ALL SELECT 'bib_v5_stat_races', 'en', 'Active races', ''
UNION ALL SELECT 'bib_v5_stat_registered', 'fr', 'Inscriptions', ''
@ -54,6 +60,10 @@ FROM (
UNION ALL SELECT 'bib_v5_race_ready', 'en', 'Ready', ''
UNION ALL SELECT 'bib_v5_race_incomplete', 'fr', 'À compléter', ''
UNION ALL SELECT 'bib_v5_race_incomplete', 'en', 'To complete', ''
UNION ALL SELECT 'bib_v5_quantities_title', 'fr', 'Quantités dinscription', ''
UNION ALL SELECT 'bib_v5_quantities_title', 'en', 'Registration quantities', ''
UNION ALL SELECT 'bib_v5_bib_assignment_title', 'fr', 'Attribution des dossards', ''
UNION ALL SELECT 'bib_v5_bib_assignment_title', 'en', 'Bib assignment', ''
UNION ALL SELECT 'bib_v5_ranges_title', 'fr', 'Plages de numéros', ''
UNION ALL SELECT 'bib_v5_ranges_title', 'en', 'Number ranges', ''
UNION ALL SELECT 'bib_v5_ranges_hint', 'fr', 'Définissez les numéros disponibles pour cette épreuve.', ''

View File

@ -0,0 +1,49 @@
-- MSIN-4468 — Message de validation de la quantité totale
-- À exécuter avec le déploiement de l'édition de la quantité totale dans la gestion des dossards.
-- Idempotent; ajoute uniquement les libellés FR/EN absents pour compte.php.
INSERT INTO info (
info_clef,
info_langue,
info_texte,
info_aide,
info_prg,
info_description,
info_trie,
info_actif,
info_option1,
info_option2,
info_option3,
info_creation
)
SELECT
src.info_clef,
src.info_langue,
src.info_texte,
'',
'compte.php',
'MSIN-4468',
0,
1,
'',
'',
'',
NOW()
FROM (
SELECT
'bib_v5_qte_total_min' AS info_clef,
'fr' AS info_langue,
'Quantité refusée : le total doit être d''au moins %d, soit le nombre d''inscriptions déjà vendues.' AS info_texte
UNION ALL
SELECT
'bib_v5_qte_total_min',
'en',
'Quantity rejected: the total must be at least %d, which is the number of registrations already sold.'
) src
WHERE NOT EXISTS (
SELECT 1
FROM info current_info
WHERE current_info.info_clef = src.info_clef
AND current_info.info_langue = src.info_langue
AND current_info.info_prg = 'compte.php'
);

View File

@ -0,0 +1,46 @@
-- MSIN-4469 — Colonne validation « Lignes Imprimeur » (Commande MS1)
-- Prérequis : MSIN-4469-production-excel-dossards.sql
-- Notes : exécution UNIQUEMENT sur dev préprod ; autres env = Navicat structure + sync_static_db
INSERT INTO info (
info_clef,
info_langue,
info_texte,
info_aide,
info_prg,
info_description,
info_trie,
info_actif,
info_option1,
info_option2,
info_option3,
info_creation
)
SELECT
src.info_clef,
src.info_langue,
src.info_texte,
src.info_aide,
'compte.php',
'MSIN-4469',
0,
1,
'',
'',
'',
NOW()
FROM (
SELECT 'bib_v5_production_col_printer_lines' info_clef, 'fr' info_langue,
'Lignes Imprimeur' info_texte,
'Nombre de lignes de longlet Imprimeur (hors lignes vides). Doit égaler la quantité de dossards.' info_aide
UNION ALL SELECT 'bib_v5_production_col_printer_lines', 'en',
'Printer lines',
'Number of rows on the Printer sheet (excluding blank rows). Must match the bib quantity.'
) src
WHERE NOT EXISTS (
SELECT 1
FROM info current_info
WHERE current_info.info_clef = src.info_clef
AND current_info.info_langue = src.info_langue
AND current_info.info_prg = 'compte.php'
);

View File

@ -0,0 +1,124 @@
-- MSIN-4469 — Production Excel des dossards
-- Prérequis : MSIN-4467-refonte-visuelle-dossards.sql.
-- Idempotent. Exécuter avant le déploiement PHP et avant le premier test.
CREATE TABLE IF NOT EXISTS `inscriptions_bib_production_orders` (
`bpo_id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`eve_id` INT NOT NULL,
`bpo_com_id` INT NOT NULL DEFAULT 0,
`bpo_usa_id` INT NOT NULL DEFAULT 0,
`bpo_status` VARCHAR(24) NOT NULL DEFAULT 'generating',
`bpo_active_key` TINYINT UNSIGNED NULL DEFAULT NULL
COMMENT '1 = commande active; NULL = historique (unicité MySQL par événement)',
`bpo_version` INT UNSIGNED NOT NULL DEFAULT 1,
`bpo_file_name` VARCHAR(255) NOT NULL DEFAULT '',
`bpo_file_path` VARCHAR(500) NOT NULL DEFAULT '',
`bpo_sha256` CHAR(64) NOT NULL DEFAULT '',
`bpo_snapshot` LONGTEXT NULL,
`bpo_created_at` DATETIME NOT NULL,
`bpo_created_by` VARCHAR(128) NOT NULL DEFAULT '',
`bpo_superseded_at` DATETIME NULL DEFAULT NULL,
`bpo_superseded_by` VARCHAR(128) NOT NULL DEFAULT '',
`bpo_superseded_reason` VARCHAR(500) NOT NULL DEFAULT '',
`bpo_error` VARCHAR(500) NOT NULL DEFAULT '',
PRIMARY KEY (`bpo_id`),
UNIQUE KEY `uq_bpo_active_event` (`eve_id`, `bpo_active_key`),
KEY `idx_bpo_event_history` (`eve_id`, `bpo_version`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
COMMENT='MSIN-4469 — fichiers officiels de production des dossards';
CREATE TABLE IF NOT EXISTS `inscriptions_bib_production_ranges` (
`bpr_id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`bpo_id` INT UNSIGNED NOT NULL,
`eve_id` INT NOT NULL,
`epr_id` INT NOT NULL,
`epr_bib_id` INT NOT NULL,
`bpr_epr_label` VARCHAR(255) NOT NULL DEFAULT '',
`bpr_start` INT UNSIGNED NOT NULL,
`bpr_finish` INT UNSIGNED NOT NULL,
`bpr_quantity` INT UNSIGNED NOT NULL,
PRIMARY KEY (`bpr_id`),
UNIQUE KEY `uq_bpr_order_range` (`bpo_id`, `epr_bib_id`),
KEY `idx_bpr_event` (`eve_id`),
KEY `idx_bpr_epr` (`epr_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
COMMENT='MSIN-4469 — instantané des séquences incluses dans une commande';
INSERT INTO info (
info_clef,
info_langue,
info_texte,
info_aide,
info_prg,
info_description,
info_trie,
info_actif,
info_option1,
info_option2,
info_option3,
info_creation
)
SELECT
src.info_clef,
src.info_langue,
src.info_texte,
src.info_aide,
'compte.php',
'MSIN-4469',
0,
1,
'',
'',
'',
NOW()
FROM (
SELECT 'bib_v5_production_excel_title' info_clef, 'fr' info_langue, 'Fichier Excel pour limprimeur' info_texte, 'Crée le fichier officiel associé à lévénement et verrouille les séquences utilisées.' info_aide
UNION ALL SELECT 'bib_v5_production_excel_title', 'en', 'Printer Excel file', 'Creates the official event file and locks the ranges used.'
UNION ALL SELECT 'bib_v5_production_excel_intro', 'fr', 'Le fichier contient un onglet Imprimeur et un onglet Commande MS1.', ''
UNION ALL SELECT 'bib_v5_production_excel_intro', 'en', 'The file contains a Printer sheet and an MS1 Order sheet.', ''
UNION ALL SELECT 'bib_v5_production_excel_generate', 'fr', 'Générer et verrouiller', ''
UNION ALL SELECT 'bib_v5_production_excel_generate', 'en', 'Generate and lock', ''
UNION ALL SELECT 'bib_v5_production_excel_generate_help', 'fr', 'Cette action crée le fichier officiel et verrouille les séquences pour limpression.', ''
UNION ALL SELECT 'bib_v5_production_excel_generate_help', 'en', 'This creates the official file and locks the ranges for printing.', ''
UNION ALL SELECT 'bib_v5_production_excel_download', 'fr', 'Télécharger de nouveau', ''
UNION ALL SELECT 'bib_v5_production_excel_download', 'en', 'Download again', ''
UNION ALL SELECT 'bib_v5_production_excel_locked', 'fr', 'En impression — séquences verrouillées', ''
UNION ALL SELECT 'bib_v5_production_excel_locked', 'en', 'In print — ranges locked', ''
UNION ALL SELECT 'bib_v5_production_excel_meta', 'fr', 'Version %d créée le %s par %s', ''
UNION ALL SELECT 'bib_v5_production_excel_meta', 'en', 'Version %d created on %s by %s', ''
UNION ALL SELECT 'bib_v5_production_excel_generating', 'fr', 'Génération en cours — rechargez la page dans quelques instants.', ''
UNION ALL SELECT 'bib_v5_production_excel_generating', 'en', 'Generation in progress — reload the page shortly.', ''
UNION ALL SELECT 'bib_v5_production_no_ranges', 'fr', 'Aucune séquence de dossards à produire.', ''
UNION ALL SELECT 'bib_v5_production_no_ranges', 'en', 'No bib ranges to produce.', ''
UNION ALL SELECT 'bib_v5_production_event_invalid', 'fr', 'Événement introuvable.', ''
UNION ALL SELECT 'bib_v5_production_event_invalid', 'en', 'Event not found.', ''
UNION ALL SELECT 'bib_v5_production_range_invalid', 'fr', 'Une séquence de dossards est invalide.', ''
UNION ALL SELECT 'bib_v5_production_range_invalid', 'en', 'A bib range is invalid.', ''
UNION ALL SELECT 'bib_v5_production_generation_error', 'fr', 'Impossible de générer le fichier Excel.', ''
UNION ALL SELECT 'bib_v5_production_generation_error', 'en', 'Unable to generate the Excel file.', ''
UNION ALL SELECT 'bib_v5_production_file_missing', 'fr', 'Le fichier Excel enregistré est introuvable.', ''
UNION ALL SELECT 'bib_v5_production_file_missing', 'en', 'The saved Excel file could not be found.', ''
UNION ALL SELECT 'bib_v5_production_col_bib', 'fr', 'Numéro de dossard', ''
UNION ALL SELECT 'bib_v5_production_col_bib', 'en', 'Bib number', ''
UNION ALL SELECT 'bib_v5_production_col_race', 'fr', 'Épreuve', ''
UNION ALL SELECT 'bib_v5_production_col_race', 'en', 'Race', ''
UNION ALL SELECT 'bib_v5_production_total', 'fr', 'TOTAL', ''
UNION ALL SELECT 'bib_v5_production_total', 'en', 'TOTAL', ''
UNION ALL SELECT 'bib_v5_production_locked_error', 'fr', 'Les séquences sont verrouillées parce que le fichier est en impression.', ''
UNION ALL SELECT 'bib_v5_production_locked_error', 'en', 'The ranges are locked because the file is in print.', ''
UNION ALL SELECT 'bib_v5_production_print_lock_col', 'fr', 'Impression', 'Indique que la séquence est figée dans le fichier officiel transmis à limprimeur.'
UNION ALL SELECT 'bib_v5_production_print_lock_col', 'en', 'Printing', 'Indicates that the range is frozen in the official printer file.'
UNION ALL SELECT 'bib_v5_production_print_lock_tip', 'fr', 'Verrouillé pour limpression : cette séquence ne peut plus être modifiée.', ''
UNION ALL SELECT 'bib_v5_production_print_lock_tip', 'en', 'Locked for printing: this range can no longer be modified.', ''
UNION ALL SELECT 'bib_v5_production_dev_unlock', 'fr', 'Déverrouiller pour le développement', ''
UNION ALL SELECT 'bib_v5_production_dev_unlock', 'en', 'Unlock for development', ''
UNION ALL SELECT 'bib_v5_production_dev_unlock_help', 'fr', 'Réactive temporairement la modification de toutes les séquences de cet événement.', ''
UNION ALL SELECT 'bib_v5_production_dev_unlock_help', 'en', 'Temporarily enables editing for all ranges in this event.', ''
) src
WHERE NOT EXISTS (
SELECT 1
FROM info current_info
WHERE current_info.info_clef = src.info_clef
AND current_info.info_langue = src.info_langue
AND current_info.info_prg = 'compte.php'
);

View File

@ -0,0 +1,109 @@
-- MSIN-4470 — Corrections de l'interface des dossards
-- Prérequis : MSIN-4424-bib-allow-inter-epr.sql et MSIN-4467-refonte-visuelle-dossards.sql.
-- Idempotent. Exécuter avant le déploiement PHP/CSS/JS.
UPDATE info
SET info_texte = 'Autoriser les mêmes numéros de dossard entre épreuves (doublons)',
info_description = 'MSIN-4470'
WHERE info_clef = 'bib_v4_allow_inter_epr'
AND info_langue = 'fr'
AND info_prg = 'compte.php';
UPDATE info
SET info_texte = 'Allow the same bib numbers across races (duplicates)',
info_description = 'MSIN-4470'
WHERE info_clef = 'bib_v4_allow_inter_epr'
AND info_langue = 'en'
AND info_prg = 'compte.php';
INSERT INTO info (
info_clef,
info_langue,
info_texte,
info_aide,
info_prg,
info_description,
info_trie,
info_actif,
info_option1,
info_option2,
info_option3,
info_creation
)
SELECT
src.info_clef,
src.info_langue,
src.info_texte,
'',
'compte.php',
'MSIN-4470',
0,
1,
'',
'',
'',
NOW()
FROM (
SELECT
'bib_v4_allow_inter_epr' AS info_clef,
'fr' AS info_langue,
'Autoriser les mêmes numéros de dossard entre épreuves (doublons)' AS info_texte
UNION ALL
SELECT
'bib_v4_allow_inter_epr',
'en',
'Allow the same bib numbers across races (duplicates)'
) src
WHERE NOT EXISTS (
SELECT 1
FROM info current_info
WHERE current_info.info_clef = src.info_clef
AND current_info.info_langue = src.info_langue
AND current_info.info_prg = 'compte.php'
);
INSERT INTO info (
info_clef,
info_langue,
info_texte,
info_aide,
info_prg,
info_description,
info_trie,
info_actif,
info_option1,
info_option2,
info_option3,
info_creation
)
SELECT
src.info_clef,
src.info_langue,
src.info_texte,
'',
'compte.php',
'MSIN-4470',
0,
1,
'',
'',
'',
NOW()
FROM (
SELECT
'bib_v4_dist_print_page' AS info_clef,
'fr' AS info_langue,
'Page %s / %s' AS info_texte
UNION ALL
SELECT
'bib_v4_dist_print_page',
'en',
'Page %s / %s'
) src
WHERE NOT EXISTS (
SELECT 1
FROM info current_info
WHERE current_info.info_clef = src.info_clef
AND current_info.info_langue = src.info_langue
AND current_info.info_prg = 'compte.php'
);

View File

@ -0,0 +1,44 @@
-- MSIN-4471 — Pages intro PDF distribution des dossards
-- Prérequis : tables doc_mod / doc_page / doc_anchor (MSIN-doc-module.sql)
-- Idempotent : DELETE puis INSERT du module bib_dist_print
-- Images : images/doc/dossardposition.png (page table participants)
SET NAMES utf8mb4;
DELETE FROM doc_page WHERE doc_mod_clef = 'bib_dist_print';
DELETE FROM doc_anchor WHERE doc_anchor_clef = 'bib_v4_dist_print_doc';
DELETE FROM doc_mod WHERE doc_mod_clef = 'bib_dist_print';
INSERT INTO `doc_mod` (`doc_mod_clef`, `doc_mod_prg`, `doc_mod_titre`, `doc_mod_actif`, `doc_mod_tri`, `doc_mod_creation`)
VALUES ('bib_dist_print', 'compte.php', 'PDF distribution des dossards', 1, 15, NOW());
INSERT INTO `doc_anchor` (`doc_anchor_clef`, `doc_mod_clef`, `doc_prg`, `doc_actif`, `doc_creation`)
VALUES ('bib_v4_dist_print_doc', 'bib_dist_print', 'compte.php', 1, NOW());
-- ---------------------------------------------------------------------------
-- FR
-- ---------------------------------------------------------------------------
INSERT INTO `doc_page` (`doc_mod_clef`, `doc_page_clef`, `doc_langue`, `doc_titre`, `doc_sous_titre`, `doc_html`, `doc_prg`, `doc_tri`, `doc_actif`, `doc_creation`) VALUES
('bib_dist_print', 'bib_dist_print_reco', 'fr', 'Conseils à la distribution', 'Pour une remise de dossards sans erreur — Responsable de la distribution des dossards',
'<article class="ms1-doc-article"><p><em>À lire avant de commencer.</em> Quelques secondes à chaque remise évitent les échanges, les erreurs de chronométrage et les résultats incorrects.</p><h3>1. Familles et groupes — plusieurs dossards</h3><p>Lorsque vous remettez plusieurs dossards à une même famille ou à un groupe, assurez-vous que le prénom figure sur chaque dossard. S''il n''y est pas déjà, <strong>écrivez-le au verso</strong>. Ainsi, chacun part avec le bon numéro : un échange entre proches fausse le chronométrage et les résultats.</p><h3>2. Ne pas céder son dossard</h3><p>Un dossard inscrit à un nom <strong>ne doit pas</strong> être remis à une autre personne. Si un échange a déjà eu lieu, <strong>avisez immédiatement les organisateurs</strong> afin qu''ils corrigent la base de données avant la course.</p><h3>3. Dossard bien visible à l''avant</h3><p>Rappelez au participant d''épingler son dossard <strong>bien visible à l''avant du torse</strong>. C''est essentiel pour un chronométrage fiable et un bon temps officiel.</p><h3>4. Vérifier ses infos avant la course (code QR)</h3><p>Invitez les participants à scanner le code QR de l''événement pour consulter les résultats en ligne <strong>avant le départ</strong>. Ils pourront confirmer que leur nom, leur âge et leur numéro de dossard sont correctement associés à leur inscription.</p></article>',
'compte.php', 10, 1, NOW()),
('bib_dist_print', 'bib_dist_print_table', 'fr', 'Votre dossard', 'À placer sur la table — pour les participants',
'<article class="ms1-doc-article"><p><em>Vous venez de recevoir votre dossard.</em> Prenez un moment pour vérifier ces points avant le départ.</p><h3>1. Vérifiez vos informations (code QR)</h3><p>Scannez le code QR de l''événement pour consulter les résultats en ligne. Vérifiez que <strong>votre nom est bien orthographié</strong>, que vous êtes dans la <strong>bonne catégorie</strong> et la <strong>bonne distance</strong>. En cas d''erreur, revenez voir l''organisation pour corriger <strong>avant la course</strong>.</p><h3>2. Portez votre dossard correctement</h3><p>Épinglez votre dossard <strong>bien visible à l''avant du torse</strong>, aux quatre coins. Un dossard mal placé ou caché peut compromettre votre chronométrage.</p><img src="/images/doc/dossardposition.png" alt="Position correcte du dossard à l''avant"><h3>3. Courez avec le bon dossard</h3><p>Assurez-vous de courir avec le dossard qui vous a été remis — celui inscrit à votre nom. Un mauvais numéro fausse les résultats.</p><h3>4. Ne cédez pas votre dossard</h3><p>Ne remettez pas votre dossard à une autre personne. Si un échange a déjà eu lieu, <strong>avisez immédiatement les organisateurs</strong> pour qu''ils corrigent la base de données.</p></article>',
'compte.php', 20, 1, NOW()),
('bib_dist_print', 'bib_dist_print_qr', 'fr', 'Code QR de l''événement', 'À afficher / découper pour l''événement',
'<p>Utilisez ce code pour consulter vos résultats en ligne pendant la course.</p>',
'compte.php', 30, 1, NOW());
-- ---------------------------------------------------------------------------
-- EN
-- ---------------------------------------------------------------------------
INSERT INTO `doc_page` (`doc_mod_clef`, `doc_page_clef`, `doc_langue`, `doc_titre`, `doc_sous_titre`, `doc_html`, `doc_prg`, `doc_tri`, `doc_actif`, `doc_creation`) VALUES
('bib_dist_print', 'bib_dist_print_reco', 'en', 'Distribution tips', 'For a smooth, error-free bib handout — Bib distribution lead',
'<article class="ms1-doc-article"><p><em>Read before you start.</em> A few seconds at each handout prevent swaps, timing errors, and incorrect results.</p><h3>1. Families and groups — multiple bibs</h3><p>When handing out several bibs to the same family or group, make sure each bib shows a first name. If it is not already there, <strong>write it on the back</strong>. This way everyone leaves with the right number: a swap between relatives will skew timing and results.</p><h3>2. Do not give away your bib</h3><p>A bib registered under one name <strong>must not</strong> be given to someone else. If a swap has already happened, <strong>notify the organizers immediately</strong> so they can correct the database before the race.</p><h3>3. Bib clearly visible on the front</h3><p>Remind the participant to pin the bib <strong>clearly visible on the front of the torso</strong>. This is essential for reliable timing and an accurate official time.</p><h3>4. Check details before the race (QR code)</h3><p>Invite participants to scan the event QR code and view results online <strong>before the start</strong>. They can confirm that their name, age, and bib number are correctly linked to their registration.</p></article>',
'compte.php', 10, 1, NOW()),
('bib_dist_print', 'bib_dist_print_table', 'en', 'Your bib', 'Place on the table — for participants',
'<article class="ms1-doc-article"><p><em>You just received your bib.</em> Take a moment to check these points before the start.</p><h3>1. Check your details (QR code)</h3><p>Scan the event QR code to view results online. Confirm that <strong>your name is spelled correctly</strong>, and that you are in the <strong>right category</strong> and <strong>right distance</strong>. If anything is wrong, see the organizers to correct it <strong>before the race</strong>.</p><h3>2. Wear your bib correctly</h3><p>Pin your bib <strong>clearly visible on the front of your torso</strong>, at all four corners. A hidden or poorly placed bib can compromise your timing.</p><img src="/images/doc/dossardposition.png" alt="Correct bib placement on the front"><h3>3. Run with the correct bib</h3><p>Make sure you run with the bib that was handed to you — the one registered under your name. The wrong number will skew results.</p><h3>4. Do not give away your bib</h3><p>Do not give your bib to someone else. If a swap has already happened, <strong>notify the organizers immediately</strong> so they can correct the database.</p></article>',
'compte.php', 20, 1, NOW()),
('bib_dist_print', 'bib_dist_print_qr', 'en', 'Event QR code', 'Display / cut out for the event',
'<p>Use this code to view your results online during the race.</p>',
'compte.php', 30, 1, NOW());

View File

@ -0,0 +1,32 @@
-- MSIN-4471 — Libellés feuille bénévole PDF distribution (nom+prénom, case remise)
-- Prérequis : clés bib_v4_dist_print_* (MSIN-4433)
-- Idempotent : INSERT si absent ; UPDATE si déjà présent avec texte vide
SET NAMES utf8mb4;
INSERT INTO info (info_clef, info_langue, info_texte, info_aide, info_prg, info_description, info_trie, info_actif, info_option1, info_option2, info_option3, info_creation)
SELECT 'bib_v4_dist_print_col_nom_prenom', 'fr', 'Nom, Prénom', '', 'compte.php', 'MSIN-4471', 0, 1, '', '', '', NOW()
FROM DUAL WHERE NOT EXISTS (
SELECT 1 FROM info WHERE info_clef = 'bib_v4_dist_print_col_nom_prenom' AND info_langue = 'fr' AND info_prg = 'compte.php'
);
INSERT INTO info (info_clef, info_langue, info_texte, info_aide, info_prg, info_description, info_trie, info_actif, info_option1, info_option2, info_option3, info_creation)
SELECT 'bib_v4_dist_print_col_nom_prenom', 'en', 'Last name, First name', '', 'compte.php', 'MSIN-4471', 0, 1, '', '', '', NOW()
FROM DUAL WHERE NOT EXISTS (
SELECT 1 FROM info WHERE info_clef = 'bib_v4_dist_print_col_nom_prenom' AND info_langue = 'en' AND info_prg = 'compte.php'
);
INSERT INTO info (info_clef, info_langue, info_texte, info_aide, info_prg, info_description, info_trie, info_actif, info_option1, info_option2, info_option3, info_creation)
SELECT 'bib_v4_dist_print_col_check', 'fr', 'OK', '', 'compte.php', 'MSIN-4471', 0, 1, '', '', '', NOW()
FROM DUAL WHERE NOT EXISTS (
SELECT 1 FROM info WHERE info_clef = 'bib_v4_dist_print_col_check' AND info_langue = 'fr' AND info_prg = 'compte.php'
);
INSERT INTO info (info_clef, info_langue, info_texte, info_aide, info_prg, info_description, info_trie, info_actif, info_option1, info_option2, info_option3, info_creation)
SELECT 'bib_v4_dist_print_col_check', 'en', 'OK', '', 'compte.php', 'MSIN-4471', 0, 1, '', '', '', NOW()
FROM DUAL WHERE NOT EXISTS (
SELECT 1 FROM info WHERE info_clef = 'bib_v4_dist_print_col_check' AND info_langue = 'en' AND info_prg = 'compte.php'
);
UPDATE info SET info_texte = 'OK', info_description = 'MSIN-4471'
WHERE info_clef = 'bib_v4_dist_print_col_check' AND info_prg = 'compte.php' AND (info_texte = '' OR info_texte IS NULL);

View File

@ -0,0 +1,12 @@
-- MSIN-4478 — Réinvitation stub non activé (libellé Info)
-- Contexte : invite / réinvite promoteur — mail « créez votre mot de passe » si compte jamais activé
-- Notes : exécution UNIQUEMENT sur dev préprod ; autres env = Navicat structure + sync_static_db
-- Idempotent : DELETE + INSERT de la clé ajoutée
DELETE FROM info
WHERE info_clef = 'eve_acces_promoteur_ok_pending'
AND info_prg = 'compte.php';
INSERT INTO info (info_clef, info_langue, info_texte, info_aide, info_prg, info_description, info_trie, info_actif, info_option1, info_option2, info_option3, info_creation) VALUES
('eve_acces_promoteur_ok_pending', 'fr', 'Invitation mise à jour. Un courriel avec le lien de mot de passe a été envoyé.', '', 'compte.php', '', 0, 1, '', '', '', NOW()),
('eve_acces_promoteur_ok_pending', 'en', 'Invitation updated. An email with the password link was sent.', '', 'compte.php', '', 0, 1, '', '', '', NOW());

View File

@ -0,0 +1,9 @@
-- MSIN-4478 — Sécurité : kit total jamais délégable
-- Contexte : un kit role_grants_all=1 + role_delegable=1 donnait tous les droits (rapports, argent…) à un invité
-- Notes : exécution UNIQUEMENT sur dev préprod ; autres env = Navicat structure + sync_static_db
-- Idempotent
UPDATE inscriptions_eve_roles
SET role_delegable = 0
WHERE role_grants_all = 1
AND role_delegable = 1;

View File

@ -0,0 +1,108 @@
-- MSIN-4482 — Réparer liste inscriptions pour imports hors transaction
-- Contexte : données en BD mais liste / « rapport » trop bas
-- Liste = p.pec_id = e.pec_id_original AND pec_actif = 1
-- Notes : exécution UNIQUEMENT sur dev préprod ; autres env = Navicat structure + sync_static_db
--
-- AVANT : remplacer 1284 par l'eve_id concerné.
-- IMPORTANT : ne PAS forcer pec_id_original = pec_id en masse (UNIQUE pec_id_original → 1062).
SET @eve := 1284;
-- ========== DIAGNOSTIC ==========
SELECT COUNT(*) AS n_participants_import
FROM resultats_participants
WHERE eve_id = @eve AND par_origine = 'import_fichier';
SELECT COUNT(*) AS n_join_ok
FROM resultats_epreuves_commandees e, resultats_participants p
WHERE e.eve_id = @eve
AND p.pec_id = e.pec_id_original
AND p.par_origine = 'import_fichier'
AND p.rol_id NOT IN (3, 4)
AND e.is_cancelled = 0;
SELECT COUNT(*) AS n_visibles_liste
FROM resultats_epreuves_commandees e, resultats_participants p
WHERE e.eve_id = @eve
AND p.pec_id = e.pec_id_original
AND p.par_origine = 'import_fichier'
AND p.rol_id NOT IN (3, 4)
AND e.is_cancelled = 0
AND e.pec_actif = 1;
-- Accroche technique import (p.pec_id = e.pec_id) avant réparation
SELECT COUNT(*) AS n_lie_via_pec_id
FROM resultats_epreuves_commandees e
INNER JOIN resultats_participants p
ON p.eve_id = e.eve_id AND p.pec_id = e.pec_id
WHERE e.eve_id = @eve
AND p.par_origine = 'import_fichier';
-- ========== FIX A — pec_actif = 1 (AVANT de bouger p.pec_id) ==========
UPDATE resultats_epreuves_commandees e
INNER JOIN resultats_participants p
ON p.eve_id = e.eve_id
AND p.pec_id = e.pec_id
AND p.par_origine = 'import_fichier'
SET e.pec_actif = 1
WHERE e.eve_id = @eve
AND e.is_cancelled = 0
AND IFNULL(e.pec_actif, 0) <> 1;
UPDATE resultats_epreuves_commandees e
SET e.pec_actif = 1
WHERE e.eve_id = @eve
AND e.no_panier LIKE CONCAT('I', @eve, 'X%')
AND e.is_cancelled = 0
AND IFNULL(e.pec_actif, 0) <> 1;
-- ========== FIX B — join sans toucher UNIQUE pec_id_original ==========
-- L'import a souvent p.pec_id = e.pec_id alors que la liste veut p.pec_id = e.pec_id_original.
-- On aligne le participant sur pec_id_original (valeur déjà unique).
UPDATE resultats_participants p
INNER JOIN resultats_epreuves_commandees e
ON e.eve_id = p.eve_id
AND e.pec_id = p.pec_id
SET p.pec_id = e.pec_id_original
WHERE p.eve_id = @eve
AND p.par_origine = 'import_fichier'
AND e.pec_id_original > 0
AND p.pec_id <> e.pec_id_original;
-- Optionnel / sûr : pec_id_original := pec_id SEULEMENT s'il n'y a PAS de collision UNIQUE
UPDATE resultats_epreuves_commandees e
INNER JOIN resultats_participants p
ON p.eve_id = e.eve_id
AND (p.pec_id = e.pec_id OR p.pec_id = e.pec_id_original)
AND p.par_origine = 'import_fichier'
LEFT JOIN resultats_epreuves_commandees conflict
ON conflict.pec_id_original = e.pec_id
AND conflict.pec_id <> e.pec_id
SET e.pec_id_original = e.pec_id
WHERE e.eve_id = @eve
AND e.pec_id_original <> e.pec_id
AND conflict.pec_id IS NULL;
-- Si l'optionnel a bougé pec_id_original, ré-aligner p.pec_id
UPDATE resultats_participants p
INNER JOIN resultats_epreuves_commandees e
ON e.eve_id = p.eve_id
AND e.no_panier LIKE CONCAT('I', @eve, 'X%')
AND e.no_panier = p.no_panier
SET p.pec_id = e.pec_id_original
WHERE p.eve_id = @eve
AND p.par_origine = 'import_fichier'
AND e.pec_id_original > 0
AND p.pec_id <> e.pec_id_original;
-- ========== CONTRÔLE ==========
SELECT COUNT(*) AS n_visibles_liste_apres
FROM resultats_epreuves_commandees e, resultats_participants p
WHERE e.eve_id = @eve
AND p.pec_id = e.pec_id_original
AND p.par_origine = 'import_fichier'
AND p.rol_id NOT IN (3, 4)
AND e.is_cancelled = 0
AND e.pec_actif = 1;
-- Attendu : proche de n_participants_import.

View File

@ -0,0 +1,56 @@
-- MSIN-4482 — Réparer questions import pour affichage fiche (eve hors inscription en ligne)
-- Contexte : réponses en BD mais fiche vide (pec_id / que_actif / que_cart_show)
-- Notes : exécution UNIQUEMENT sur dev préprod ; autres env = Navicat structure + sync_static_db
-- Cible : eve_id 1284 + par_origine = import_fichier seulement (ne touche pas au payant)
SET @eve := 1284;
-- Diagnostic : combien de réponses import vs ce que la fiche classique verrait
SELECT COUNT(*) AS n_reponses_via_par
FROM resultats_questions rq
INNER JOIN resultats_participants p
ON (rq.par_id = p.par_id OR rq.par_id = p.par_id_original)
WHERE p.eve_id = @eve AND p.par_origine = 'import_fichier';
-- 1) Aligner rq.pec_id sur e.pec_id_original (clé utilisée par la fiche)
UPDATE resultats_questions rq
INNER JOIN resultats_participants p
ON p.eve_id = @eve
AND p.par_origine = 'import_fichier'
AND (rq.par_id = p.par_id OR rq.par_id = p.par_id_original)
INNER JOIN resultats_epreuves_commandees e
ON e.eve_id = @eve
AND (e.pec_id_original = p.pec_id OR e.pec_id = p.pec_id OR e.pec_id = rq.pec_id)
SET rq.pec_id = e.pec_id_original
WHERE e.pec_id_original > 0
AND rq.pec_id <> e.pec_id_original;
-- 2) Activer les réponses import
UPDATE resultats_questions rq
INNER JOIN resultats_participants p
ON p.eve_id = @eve
AND p.par_origine = 'import_fichier'
AND (rq.par_id = p.par_id OR rq.par_id = p.par_id_original)
SET rq.que_actif = 1
WHERE IFNULL(rq.que_actif, 0) <> 1;
-- 3) Afficher en fiche (que_cart_show) les questions du catalogue liées à cet import
UPDATE inscriptions_questions iq
INNER JOIN resultats_questions rq ON rq.que_id = iq.que_id
INNER JOIN resultats_participants p
ON p.eve_id = @eve
AND p.par_origine = 'import_fichier'
AND (rq.par_id = p.par_id OR rq.par_id = p.par_id_original)
SET iq.que_cart_show = 1,
iq.que_cart_label_fr = IF(
TRIM(IFNULL(iq.que_cart_label_fr, '')) = '',
iq.que_question_fr,
iq.que_cart_label_fr
),
iq.que_cart_label_en = IF(
TRIM(IFNULL(iq.que_cart_label_en, '')) = '',
IF(TRIM(IFNULL(iq.que_question_en, '')) = '', iq.que_question_fr, iq.que_question_en),
iq.que_cart_label_en
)
WHERE iq.eve_id = @eve
AND IFNULL(iq.que_cart_show, 0) <> 1;

View File

@ -0,0 +1,63 @@
-- MSIN-4482 — Templates mapping import participants (hors transaction)
-- Contexte : UI import fiche événement superadm ; upsert sur clé fichier
-- Notes : exécution UNIQUEMENT sur dev préprod ; autres env = Navicat structure + sync_static_db
SET @db := DATABASE();
-- Templates de mapping colonnes (réutilisables par empreinte d'en-têtes)
CREATE TABLE IF NOT EXISTS `inscriptions_import_templates` (
`tpl_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`eve_id` int(10) unsigned NOT NULL DEFAULT 0,
`com_id` int(10) unsigned NOT NULL DEFAULT 0 COMMENT 'MSIN-4482 — org_id evenement (partage templates même org) ; pas com_id compte',
`tpl_name` varchar(120) NOT NULL DEFAULT '',
`tpl_header_sig` char(40) NOT NULL DEFAULT '' COMMENT 'sha1 en-têtes normalisés',
`tpl_headers_json` mediumtext NOT NULL,
`tpl_mapping_json` mediumtext NOT NULL COMMENT 'colonnes + valeurs épreuve + questions',
`tpl_actif` tinyint(1) unsigned NOT NULL DEFAULT 1,
`tpl_usa_id` int(10) unsigned NOT NULL DEFAULT 0,
`tpl_creation` datetime NOT NULL,
`tpl_maj` datetime NOT NULL,
PRIMARY KEY (`tpl_id`),
KEY `idx_import_tpl_eve_sig` (`eve_id`, `tpl_header_sig`),
KEY `idx_import_tpl_com_sig` (`com_id`, `tpl_header_sig`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Clé unique fichier (= external_id ChronoTrack) + origine d'entrée
SET @col_ext := (
SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'resultats_participants' AND COLUMN_NAME = 'par_external_id'
);
SET @sql_ext := IF(
@col_ext = 0,
'ALTER TABLE resultats_participants ADD COLUMN par_external_id VARCHAR(64) NULL DEFAULT NULL COMMENT ''MSIN-4482 — clé unique import / CT external_id'' AFTER par_id_original',
'SELECT ''par_external_id already exists'' AS note'
);
PREPARE stmt_ext FROM @sql_ext;
EXECUTE stmt_ext;
DEALLOCATE PREPARE stmt_ext;
SET @col_ori := (
SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'resultats_participants' AND COLUMN_NAME = 'par_origine'
);
SET @sql_ori := IF(
@col_ori = 0,
'ALTER TABLE resultats_participants ADD COLUMN par_origine VARCHAR(32) NULL DEFAULT NULL COMMENT ''MSIN-4482 — import_fichier|saisie_rapide|inscription_en_ligne'' AFTER par_external_id',
'SELECT ''par_origine already exists'' AS note'
);
PREPARE stmt_ori FROM @sql_ori;
EXECUTE stmt_ori;
DEALLOCATE PREPARE stmt_ori;
SET @idx_ext := (
SELECT COUNT(*) FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'resultats_participants' AND INDEX_NAME = 'idx_par_external_eve'
);
SET @sql_idx := IF(
@idx_ext = 0,
'ALTER TABLE resultats_participants ADD KEY idx_par_external_eve (eve_id, par_external_id)',
'SELECT ''idx_par_external_eve already exists'' AS note'
);
PREPARE stmt_idx FROM @sql_idx;
EXECUTE stmt_idx;
DEALLOCATE PREPARE stmt_idx;

View File

@ -0,0 +1,45 @@
-- MSIN-4482 — Type d'événement « Sans inscription en ligne »
-- Contexte : marqueur isolé pour eve hors tunnel inscription / paiement MS1 (import resultats_*)
-- Notes : exécution UNIQUEMENT sur dev préprod ; autres env = Navicat structure + sync_static_db
--
-- Table : inscriptions_types_evenements
-- Lien : inscriptions_evenements.te_id
-- Icône : images/te_sans_inscription_en_ligne.png (pas le coureur — écran + interdit)
-- 1) Créer le type (idempotent)
INSERT INTO inscriptions_types_evenements (te_nom_fr, te_nom_en, te_icone, te_actif)
SELECT
'Sans inscription en ligne',
'No online registration',
'te_sans_inscription_en_ligne.png',
1
FROM (SELECT 1 AS _x) AS _seed
WHERE NOT EXISTS (
SELECT 1
FROM inscriptions_types_evenements
WHERE te_nom_fr = 'Sans inscription en ligne'
);
-- 1b) Si le type existe déjà sans icône (ou à corriger) — petit UPDATE, safe
UPDATE inscriptions_types_evenements
SET te_icone = 'te_sans_inscription_en_ligne.png'
WHERE te_nom_fr = 'Sans inscription en ligne'
AND IFNULL(te_icone, '') <> 'te_sans_inscription_en_ligne.png';
-- 2) Vérification
SELECT te_id, te_nom_fr, te_nom_en, te_icone, te_actif
FROM inscriptions_types_evenements
WHERE te_nom_fr = 'Sans inscription en ligne';
-- 3) Rattacher l'événement de test (adapter eve_id si besoin)
UPDATE inscriptions_evenements
SET te_id = (
SELECT te_id FROM inscriptions_types_evenements
WHERE te_nom_fr = 'Sans inscription en ligne'
LIMIT 1
)
WHERE eve_id = 1284;
SELECT eve_id, te_id, eve_nom_fr
FROM inscriptions_evenements
WHERE eve_id = 1284;

View File

@ -0,0 +1,15 @@
-- MSIN-4482 — Urgence : débloquer / rollback type eve 1284 si besoin
-- Notes : exécution UNIQUEMENT sur dev préprod
--
-- 1) Si MySQL est saturé par un gros UPDATE/DELETE import :
-- SHOW FULL PROCESSLIST;
-- puis KILL <id>; sur les requêtes longues sur resultats_* / inscriptions_questions
--
-- 2) Remettre l'événement 1284 en type « Course » (te_id typique = 1 — vérifier d'abord) :
SELECT te_id, te_nom_fr FROM inscriptions_types_evenements WHERE te_actif = 1 ORDER BY te_nom_fr;
-- Exemple si Course = te_id 1 :
-- UPDATE inscriptions_evenements SET te_id = 1 WHERE eve_id = 1284;
SELECT eve_id, te_id, eve_nom_fr FROM inscriptions_evenements WHERE eve_id = 1284;

View File

@ -0,0 +1,31 @@
-- MSIN-4484 — Permission catalogue : hors propagation kit total
-- Prérequis : inscriptions_eve_permissions (accès v2)
-- Notes : exécution UNIQUEMENT sur dev préprod ; autres env = Navicat structure + sync_static_db
-- Idempotent
--
-- perm_grants_all = 1 (défaut) : incluse dans role_grants_all (comportement actuel)
-- perm_grants_all = 0 : nest PAS accordée automatiquement par un kit total ;
-- seulement via matrice / extra explicite (ou gate hors catalogue)
SET NAMES utf8mb4;
SET @col_exists := (
SELECT COUNT(*)
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'inscriptions_eve_permissions'
AND COLUMN_NAME = 'perm_grants_all'
);
SET @sql := IF(
@col_exists = 0,
'ALTER TABLE `inscriptions_eve_permissions`
ADD COLUMN `perm_grants_all` tinyint(1) unsigned NOT NULL DEFAULT 1
COMMENT ''MSIN-4484 — 1 = incluse dans kit total (grants_all) ; 0 = non propagée''
AFTER `perm_actif`',
'SELECT ''perm_grants_all already exists'' AS info'
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

View File

@ -0,0 +1,72 @@
-- MSIN-4485 — Impression dossard PDF (fiche gestion) + droit hors kit total
-- Prérequis : inscriptions_eve_permissions (accès v2)
-- Inclut la colonne MSIN-4484 (perm_grants_all) si absente — safe si 4484 déjà joué
-- Notes : exécution UNIQUEMENT sur dev préprod ; autres env = Navicat structure + sync_static_db
-- Idempotent
--
-- Droit non propagé (perm_grants_all = 0) : activer via extra / matrice explicite
-- sur le compte voulu — pas via kit owner / total.
SET NAMES utf8mb4;
/* MSIN-4484 — colonne hors propagation kit total (si pas déjà exécuté) */
SET @col_exists := (
SELECT COUNT(*)
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'inscriptions_eve_permissions'
AND COLUMN_NAME = 'perm_grants_all'
);
SET @sql := IF(
@col_exists = 0,
'ALTER TABLE `inscriptions_eve_permissions`
ADD COLUMN `perm_grants_all` tinyint(1) unsigned NOT NULL DEFAULT 1
COMMENT ''MSIN-4484 — 1 = incluse dans kit total (grants_all) ; 0 = non propagée''
AFTER `perm_actif`',
'SELECT ''perm_grants_all already exists'' AS info'
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
INSERT INTO `inscriptions_eve_permissions`
(`perm_key`, `perm_group`, `perm_scope`, `perm_label_fr`, `perm_label_en`,
`perm_description_fr`, `perm_description_en`, `perm_phase`, `perm_actif`,
`perm_grants_all`, `perm_tri`)
VALUES
('inscriptions_gestion.dossard_print', 'inscriptions', 'action',
'Gestion inscription : imprimer le dossard (PDF)',
'Registration mgmt: print bib (PDF)',
'Bouton Imprimer le dossard sur la fiche participant (PDF demi-lettre)',
'Print bib button on participant sheet (half-letter PDF)',
2, 1, 0, 119)
ON DUPLICATE KEY UPDATE
`perm_group` = VALUES(`perm_group`),
`perm_scope` = VALUES(`perm_scope`),
`perm_label_fr` = VALUES(`perm_label_fr`),
`perm_label_en` = VALUES(`perm_label_en`),
`perm_description_fr` = VALUES(`perm_description_fr`),
`perm_description_en` = VALUES(`perm_description_en`),
`perm_phase` = VALUES(`perm_phase`),
`perm_actif` = 1,
`perm_grants_all` = 0,
`perm_tri` = VALUES(`perm_tri`);
/* Ne pas laisser ce droit dans un kit par accident (hors propagation). */
DELETE FROM `inscriptions_eve_role_permissions`
WHERE `perm_key` = 'inscriptions_gestion.dossard_print';
DELETE FROM info
WHERE info_prg = 'compte.php'
AND info_clef IN (
'inscr_gestion_btn_dossard_print',
'inscr_gestion_btn_dossard_print_title'
);
INSERT INTO info (info_clef, info_langue, info_texte, info_aide, info_prg, info_description, info_trie, info_actif, info_option1, info_option2, info_option3, info_creation) VALUES
('inscr_gestion_btn_dossard_print', 'fr', 'Imprimer le dossard', '', 'compte.php', 'MSIN-4485', 0, 1, '', '', '', NOW()),
('inscr_gestion_btn_dossard_print', 'en', 'Print bib', '', 'compte.php', 'MSIN-4485', 0, 1, '', '', '', NOW()),
('inscr_gestion_btn_dossard_print_title', 'fr', 'Générer le PDF du dossard', '', 'compte.php', 'MSIN-4485', 0, 1, '', '', '', NOW()),
('inscr_gestion_btn_dossard_print_title', 'en', 'Generate bib PDF', '', 'compte.php', 'MSIN-4485', 0, 1, '', '', '', NOW());

View File

@ -6,6 +6,10 @@ require_once dirname(__FILE__) . '/php/inc_functions.php';
require_once dirname(__FILE__) . '/../php/chronotrack_api/inc_bootstrap.php';
$intUsaId = fxSuperadmAjaxAuthOrJsonExit();
// MSIN-4328 — libérer le verrou session : sinon un push long bloque Actualiser / preview (Erreur réseau).
if (session_status() === PHP_SESSION_ACTIVE) {
session_write_close();
}
$strAction = trim((string)($_REQUEST['a'] ?? ''));
$intEveId = intval($_REQUEST['eve_id'] ?? 0);
@ -91,7 +95,7 @@ switch ($strAction) {
$tabRetour = array('state' => 'error', 'message' => 'eve_id manquant');
break;
}
set_time_limit(60);
set_time_limit(120);
$tabRetour = fxChronotrackApiSyncBuildPreview($intEveId);
break;
@ -171,7 +175,7 @@ switch ($strAction) {
break;
case 'sync_probe_custom':
// MSIN-4328 — sonde création champ custom (comme import Custom)
// MSIN-4461 — sonde entry_note_test (notes custom CT)
if ($intEveId <= 0) {
$tabRetour = array('state' => 'error', 'message' => 'eve_id manquant');
break;
@ -186,7 +190,12 @@ switch ($strAction) {
break;
}
set_time_limit(180);
$tabRetour = fxChronotrackApiSyncPushPrepare($intEveId);
$blnForceFull = (intval($_REQUEST['force_full'] ?? 0) === 1);
$blnVerboseLogs = (intval($_REQUEST['verbose_logs'] ?? 0) === 1);
$tabRetour = fxChronotrackApiSyncPushPrepare($intEveId, array(
'force_full' => $blnForceFull,
'verbose_logs' => $blnVerboseLogs,
));
break;
case 'sync_push':
@ -200,10 +209,17 @@ switch ($strAction) {
if ($intLimit <= 0) {
$intLimit = MSIN_API_CHRONOTRACK_SYNC_CHUNK_SIZE;
}
$tabRetour = fxChronotrackApiSyncPushEvent($intEveId, array(
'offset' => $intOffset,
'limit' => $intLimit,
));
try {
$tabRetour = fxChronotrackApiSyncPushEvent($intEveId, array(
'offset' => $intOffset,
'limit' => $intLimit,
));
} catch (Throwable $e) {
$tabRetour = array(
'state' => 'error',
'message' => 'Exception push : ' . $e->getMessage(),
);
}
break;
case 'sync_orphans':
@ -219,4 +235,11 @@ switch ($strAction) {
break;
}
echo json_encode($tabRetour);
if (!headers_sent()) {
header('Content-Type: application/json; charset=utf-8');
}
$strJsonOut = json_encode($tabRetour, JSON_UNESCAPED_UNICODE);
if ($strJsonOut === false) {
$strJsonOut = '{"state":"error","message":"json_encode failed"}';
}
echo $strJsonOut;

View File

@ -0,0 +1,134 @@
<?php
/**
* MSIN-4482 — AJAX import participants (hors transaction) — superadm.
* Buffer strict : toute sortie parasite (erreur SQL, notices Excel) casse dataType:json.
*/
ob_start();
error_reporting(E_ALL & ~E_DEPRECATED & ~E_NOTICE & ~E_WARNING);
ini_set('display_errors', '0');
require_once dirname(__FILE__) . '/php/inc_functions.php';
require_once dirname(__FILE__) . '/../php/inc_fx_import_resultats.php';
$intUsaId = fxSuperadmAjaxAuthOrJsonExit();
if (session_status() === PHP_SESSION_ACTIVE) {
session_write_close();
}
$strAction = trim((string)($_REQUEST['a'] ?? ''));
$intEveId = intval($_REQUEST['eve_id'] ?? 0);
$tabRetour = array('state' => 'error', 'message' => 'action_invalide');
// MSIN-4482 — toutes les actions mutantes exigent le type « Sans inscription en ligne »
$tabActionsGuard = array('upload', 'save_template', 'validate', 'commit', 'reset_preview', 'reset');
if (in_array($strAction, $tabActionsGuard, true)) {
$tabGuard = fxImportResultatsGuardEveSansInscription($intEveId);
if (is_array($tabGuard)) {
$tabRetour = $tabGuard;
while (ob_get_level() > 0) {
ob_end_clean();
}
if (!headers_sent()) {
header('Content-Type: application/json; charset=utf-8');
}
echo json_encode($tabRetour, JSON_UNESCAPED_UNICODE);
exit;
}
}
switch ($strAction) {
case 'upload':
if ($intEveId <= 0) {
$tabRetour = array('state' => 'error', 'message' => 'eve_id manquant');
break;
}
if (empty($_FILES['file'])) {
$tabRetour = array('state' => 'error', 'message' => 'Fichier manquant');
break;
}
$tabRetour = fxImportResultatsSaveUpload($intEveId, $_FILES['file']);
break;
case 'save_template':
if ($intEveId <= 0) {
$tabRetour = array('state' => 'error', 'message' => 'eve_id manquant');
break;
}
$strToken = trim((string)($_REQUEST['token'] ?? ''));
$strName = trim((string)($_REQUEST['tpl_name'] ?? ''));
$arrMapping = $_REQUEST['mapping'] ?? array();
if (is_string($arrMapping)) {
$arrDecoded = json_decode($arrMapping, true);
$arrMapping = is_array($arrDecoded) ? $arrDecoded : array();
}
if (!is_array($arrMapping)) {
$arrMapping = array();
}
$tabRetour = fxImportResultatsSaveTemplate($intEveId, $strToken, $strName, $arrMapping, $intUsaId, true);
break;
case 'validate':
if ($intEveId <= 0) {
$tabRetour = array('state' => 'error', 'message' => 'eve_id manquant');
break;
}
$strToken = trim((string)($_REQUEST['token'] ?? ''));
$arrMapping = $_REQUEST['mapping'] ?? array();
if (is_string($arrMapping)) {
$arrDecoded = json_decode($arrMapping, true);
$arrMapping = is_array($arrDecoded) ? $arrDecoded : array();
}
if (!is_array($arrMapping)) {
$arrMapping = array();
}
$tabRetour = fxImportResultatsValidateMapping($intEveId, $strToken, $arrMapping);
break;
case 'commit':
if ($intEveId <= 0) {
$tabRetour = array('state' => 'error', 'message' => 'eve_id manquant');
break;
}
$strToken = trim((string)($_REQUEST['token'] ?? ''));
$arrMapping = $_REQUEST['mapping'] ?? array();
if (is_string($arrMapping)) {
$arrDecoded = json_decode($arrMapping, true);
$arrMapping = is_array($arrDecoded) ? $arrDecoded : array();
}
if (!is_array($arrMapping)) {
$arrMapping = array();
}
$tabRetour = fxImportResultatsCommit($intEveId, $strToken, $arrMapping);
break;
case 'reset_preview':
if ($intEveId <= 0) {
$tabRetour = array('state' => 'error', 'message' => 'eve_id manquant');
break;
}
$tabRetour = fxImportResultatsResetPreview($intEveId);
break;
case 'reset':
if ($intEveId <= 0) {
$tabRetour = array('state' => 'error', 'message' => 'eve_id manquant');
break;
}
$strConfirm = trim((string)($_REQUEST['confirm'] ?? ''));
$tabRetour = fxImportResultatsReset($intEveId, $strConfirm);
break;
default:
$tabRetour = array('state' => 'error', 'message' => 'action_invalide');
break;
}
// Jeter tout HTML parasite (erreur SQL legacy, notices PHPExcel, etc.)
while (ob_get_level() > 0) {
ob_end_clean();
}
if (!headers_sent()) {
header('Content-Type: application/json; charset=utf-8');
}
echo json_encode($tabRetour, JSON_UNESCAPED_UNICODE);
exit;

View File

@ -1,6 +1,7 @@
<?php
/**
* MSIN API ChronoTrack — page superadmin (config par eve_id + dashboard).
* MSIN API ChronoTrack — page superadmin (liste + opération course).
* MSIN-4328 — focus=1 : fenêtre opération plein écran (sans menu gauche).
*/
$strLangue = 'fr';
$strPage = 'chronotrack_api.php';
@ -16,12 +17,13 @@ if (empty($_SESSION['usa_id']) || empty($_SESSION['usa_info'])) {
$intEveId = isset($_GET['eve_id']) ? intval($_GET['eve_id']) : 0;
$blnDashboard = ($intEveId <= 0);
$blnFocus = (!$blnDashboard && !empty($_GET['focus']));
include('inc_header.php');
?>
<div class="container-fluid" id="main">
<div class="container-fluid<?php echo $blnFocus ? ' ct-api-focus-page' : ''; ?>" id="main">
<div class="row">
<div class="col-md-9 col-lg-10 order-first order-md-last py-3">
<div class="<?php echo $blnFocus ? 'col-12' : 'col-md-9 col-lg-10 order-first order-md-last'; ?> py-3">
<?php
require_once dirname(__FILE__) . '/../php/inc_fx_superadm_v2_ui.php';
if ($blnDashboard) {
@ -30,16 +32,20 @@ include('inc_header.php');
));
fxChronotrackApiAdminRenderDashboard($strLangue, true);
} else {
fxSuperadmV2PanelOpen('ChronoTrack — Configuration événement', array(
fxSuperadmV2PanelOpen($blnFocus ? 'ChronoTrack — Tableau de bord course' : 'ChronoTrack — Course', array(
'zone_class' => 'superadm-content-v2-zone superadm-content-v2-zone--flush',
));
fxChronotrackApiAdminRenderEventPage($intEveId, $strLangue, false, true);
fxChronotrackApiAdminRenderEventPage($intEveId, $strLangue, false, true, $blnFocus);
}
fxSuperadmV2PanelClose();
?>
<p>&nbsp;</p>
</div>
<?php include('inc_droite.php'); ?>
<?php
if (!$blnFocus) {
include('inc_droite.php');
}
?>
</div>
</div>
<?php include('inc_footer.php'); ?>

View File

@ -396,4 +396,76 @@ table.rapport tr {background-color: #190d7394;}*/
.superadm-v2-panel__body .custom-control-label {
color: #1e293b;
font-weight: 500;
}
/* MSIN-4482 — Import participants (hors transaction) */
.ms1-import__drop {
align-items: center;
background: linear-gradient(160deg, #f0f9fc 0%, #e4f3f8 100%);
border: 2px dashed #045a6e;
border-radius: 12px;
color: #045a6e;
cursor: pointer;
display: flex;
flex-direction: column;
gap: 0.35rem;
justify-content: center;
min-height: 9.5rem;
padding: 1.25rem 1rem;
text-align: center;
transition: background 0.15s ease, border-color 0.15s ease, transform 0.15s ease, box-shadow 0.15s ease;
}
.ms1-import__drop:hover,
.ms1-import__drop:focus {
background: #e8f6fb;
box-shadow: 0 4px 14px rgba(4, 90, 110, 0.12);
outline: none;
}
.ms1-import__drop.is-drag {
background: #dff3f9;
border-color: #0284a8;
border-style: solid;
box-shadow: 0 0 0 3px rgba(2, 132, 168, 0.2);
transform: scale(1.01);
}
.ms1-import__drop.is-busy {
cursor: wait;
opacity: 0.72;
pointer-events: none;
}
.ms1-import__drop.is-ready {
border-style: solid;
min-height: 4.5rem;
padding: 0.65rem;
}
.ms1-import__drop.is-ready .ms1-import__drop-sub {
display: none;
}
.ms1-import__drop-icon {
font-size: 2rem;
line-height: 1;
opacity: 0.9;
}
.ms1-import__drop-title {
font-size: 1.05rem;
font-weight: 700;
}
.ms1-import__drop-sub {
color: #64748b;
font-size: 0.875rem;
}
.ms1-import__h {
color: #045a6e;
font-weight: 700;
margin-top: 0.35rem;
}
.ms1-import__status.alert {
margin-bottom: 0.75rem;
}
.ms1-import__epr,
.ms1-import__que {
background: #f8fafc;
border: 1px solid #cbd5e1;
border-radius: 8px;
padding: 0.75rem 0.85rem;
}

View File

@ -0,0 +1,44 @@
<?php
/**
* MSIN-3487 — PNG QR résultats événement (logo MS1).
* Endpoint léger (sans inc_manager) pour téléchargement direct.
*
* Affichage / DL : eve_resultats_qr.php?eve_id=760[&dl=1]
*/
session_start();
if (empty($_SESSION['usa_id'])) {
http_response_code(403);
header('Content-Type: text/plain; charset=utf-8');
echo 'Accès refusé.';
exit;
}
// Settings publics pour $vDomaine uniquement (pas tout le stack superadm).
require_once dirname(__FILE__) . '/../php/inc_settings.php';
require_once dirname(__FILE__) . '/../php/inc_fx_eve_extra.php';
$intEveId = isset($_GET['eve_id']) ? intval($_GET['eve_id']) : 0;
$blnDownload = !empty($_GET['dl']);
$binPng = fxKcEveResultatsQrPngBinary($intEveId, null, 10, true);
if ($binPng === false) {
http_response_code(400);
header('Content-Type: text/plain; charset=utf-8');
echo 'QR impossible à générer.';
exit;
}
$strFilename = 'qr_resultats_eve_' . $intEveId . '.png';
header('Content-Type: image/png');
header('Content-Length: ' . strlen($binPng));
header('Cache-Control: private, max-age=60');
if ($blnDownload) {
header('Content-Disposition: attachment; filename="' . $strFilename . '"');
} else {
header('Content-Disposition: inline; filename="' . $strFilename . '"');
}
echo $binPng;
exit;

View File

@ -90,7 +90,9 @@ if (isset($_SESSION['com_info']['com_id'])) {
strpath = '/book/';
}
document.location.href = '<?php echo $vDomaine; ?>' + strpath + code + '/' + intEprId + '?promoteur=1&action=mod&pec_id=' + intPecId;
document.location.href = '<?php echo $vDomaine; ?>' + strpath + code + '/' + intEprId
+ '?promoteur=1&action=mod&pec_id=' + intPecId
+ '&return_url=' + encodeURIComponent(window.location.href);
});
$('.btn_update_panier').click(function () {

View File

@ -1,15 +1,16 @@
<?php
session_start();
// Require avant redirect : fxSuperadmConsumeRedirectAfterLogin est dans inc_functions.php.
require_once('php/inc_functions.php');
require_once('php/inc_fx_messages.php');
global $db, $blnDev, $vDomaine;
if (!empty($_SESSION['usa_id'])) {
header('Location: ' . fxSuperadmConsumeRedirectAfterLogin('index.php'));
exit;
}
require_once('php/inc_functions.php');
require_once('php/inc_fx_messages.php');
global $db, $blnDev, $vDomaine;
if (isset($_POST['usa_login'])) {
$sqlLogin = "SELECT * FROM inscriptions_comptes WHERE com_login = '" . $db->fxEscape($_POST['usa_login']) . "' AND com_actif = 1 AND com_superadm = 1";
$recLogin = $db->fxGetRow($sqlLogin);

View File

@ -128,6 +128,10 @@ if ($strAction == 'mod') {
));
fxChronotrackApiAdminRenderEventPage($intEveIdForm, $strLangue, true, true);
fxSuperadmV2PanelClose(array('collapse_id' => 'chronotrackApiCollapse'));
// MSIN-4482 — Import participants hors transaction (UI + templates)
require_once(dirname(__FILE__) . '/../../php/inc_fx_import_resultats.php');
fxImportResultatsRenderEventPanel($intEveIdForm);
}
}
@ -153,6 +157,13 @@ if ($strAction == 'mod') {
onclick="if (window.jQuery) { jQuery('#chronotrackApiCollapse').collapse('show'); }">
ChronoTrack
</a>
<a class="btn btn-secondary" href="#import_resultats"
onclick="if (window.jQuery) { jQuery('#importResultatsCollapse').collapse('show'); }">
Import
</a>
<a class="btn btn-secondary" href="#list_eve_extra">
Champs supplémentaires
</a>
<a class="btn btn-secondary" href="#list_rapports">
Rapports
</a>
@ -1121,7 +1132,84 @@ if ($strAction == 'mod') {
}
if ($blnIsEventModForm) {
// MSIN-3487 — UI temporaire : lien résultats (table inscriptions_evenements_extra)
require_once dirname(__FILE__) . '/../../php/inc_fx_eve_extra.php';
$strEveExtraFlash = '';
if (isset($_POST['btn_eve_extra_save'])) {
$strUrlPosted = isset($_POST['eve_resultats_url']) ? trim((string) $_POST['eve_resultats_url']) : '';
if (fxEveExtraSetResultatsUrl($intEveIdForm, $strUrlPosted)) {
$strEveExtraFlash = ($strUrlPosted === '')
? 'Lien résultats effacé (fallback ms1.ca).'
: 'Lien résultats enregistré.';
} else {
$strEveExtraFlash = 'Erreur à l\'enregistrement du lien résultats.';
}
}
$strEveResultatsUrl = (string) (fxEveExtraGetResultatsUrl($intEveIdForm) ?? '');
$strEveQrPreview = fxKcEveResultatsQrUrl($intEveIdForm);
// $intMenuActuel : figé en tête de fiche (évite le $tabResults écrasé par les sous-listes)
$strEveExtraFormAction = $strhomeclient
. '?t=' . urlencode(base64_encode($intMenuActuel))
. '&a=mod&id=' . intval($intEveIdForm);
?>
<h1 id="list_eve_extra">Champs supplémentaires</h1>
<p class="text-muted small">MSIN-3487 — temporaire (superadm). Lien public des résultats de course.</p>
<?php if ($strEveExtraFlash !== '') { ?>
<div class="alert alert-info py-2"><?php echo htmlspecialchars($strEveExtraFlash, ENT_QUOTES, 'UTF-8'); ?></div>
<?php } ?>
<form action="<?php echo htmlspecialchars($strEveExtraFormAction, ENT_QUOTES, 'UTF-8'); ?>"
method="post" class="mb-4" id="frm_eve_extra" name="frm_eve_extra">
<div class="form-group row">
<label class="col-form-label col-sm-3" for="eve_resultats_url">Lien résultats</label>
<div class="col-sm-9 col-lg-7">
<input type="text" class="form-control rounded-0" id="eve_resultats_url" name="eve_resultats_url"
value="<?php echo htmlspecialchars($strEveResultatsUrl, ENT_QUOTES, 'UTF-8'); ?>"
placeholder="https://…" autocomplete="off">
<small class="form-text text-muted">
QR court :
<code><?php echo htmlspecialchars($strEveQrPreview, ENT_QUOTES, 'UTF-8'); ?></code>
— si vide → <code>https://ms1.ca</code>
</small>
<?php if ($intEveIdForm > 0) {
// MSIN-3487 — génération inline (évite 403/404 sur l'endpoint séparé)
$binEveQrPng = fxKcEveResultatsQrPngBinary($intEveIdForm, null, 8, true);
if ($binEveQrPng !== false) {
$strEveQrDataUri = 'data:image/png;base64,' . base64_encode($binEveQrPng);
$strEveQrFilename = 'qr_resultats_eve_' . intval($intEveIdForm) . '.png';
?>
<div class="mt-3">
<div class="mb-2">
<img src="<?php echo htmlspecialchars($strEveQrDataUri, ENT_QUOTES, 'UTF-8'); ?>"
alt="QR résultats événement <?php echo intval($intEveIdForm); ?>"
width="220" height="220"
class="border bg-white p-1">
</div>
<a class="btn btn-outline-secondary btn-sm rounded-0"
href="<?php echo htmlspecialchars($strEveQrDataUri, ENT_QUOTES, 'UTF-8'); ?>"
download="<?php echo htmlspecialchars($strEveQrFilename, ENT_QUOTES, 'UTF-8'); ?>">
<i class="fa fa-download" aria-hidden="true"></i>&nbsp;Télécharger le QR (PNG)
</a>
</div>
<?php
} else {
?>
<div class="mt-3 text-danger small">Impossible de générer le QR (GD / phpqrcode).</div>
<?php
}
} ?>
</div>
</div>
<div class="form-group row">
<div class="col-sm-9 col-lg-7 offset-sm-3">
<button type="submit" class="btn btn-success rounded-0" name="btn_eve_extra_save" value="1">
<i class="fa fa-check" aria-hidden="true"></i>&nbsp;Enregistrer le lien résultats
</button>
</div>
</div>
</form>
<h1 id="list_rapports">Rapport de l'événement</h1>
<ul class="nav nav-pills flex-column">
<?php

View File

@ -510,9 +510,9 @@ function fxEveAccesAdminSaveRole($arrData, $arrPermKeys)
if ($blnCodeLocked) {
$blnGrantsAll = 1;
}
// MSIN-4460Kit total n'interdit plus la delegation : c'est le super admin qui decide.
// Kits systeme uniquement : jamais redistribuables par un promoteur.
if ($blnCodeLocked || in_array($strCode, array('owner', 'qr_debug'), true)) {
// MSIN-4478kit total + kits systeme : jamais redistribuables par un promoteur
// (évite qu'une invitation donne rapports / argent / tout).
if ($blnCodeLocked || $blnGrantsAll || in_array($strCode, array('owner', 'qr_debug'), true)) {
$blnDelegable = 0;
}
@ -961,7 +961,7 @@ function fxEveAccesAdminRenderKitForm($intRoleId, $strError = '')
Délégable par le promoteur (invitation équipe)
</label>
</div>
<small class="text-muted">Le promoteur pourra assigner ce kit à un membre invité. Indépendant de « Kit total » — cest vous qui choisissez.</small>
<small class="text-muted">Le promoteur pourra assigner ce kit à un membre invité. Impossible si « Kit total » est coché (MSIN-4478).</small>
</div>
<?php } ?>
<?php if (fxEveAccesAdminHasGrantsAllColumn()) { ?>
@ -978,7 +978,8 @@ function fxEveAccesAdminRenderKitForm($intRoleId, $strError = '')
</label>
</div>
<small class="text-muted">
Si coché : aucune matrice à maintenir. Sinon : choisissez les droits un par un ci-dessous.
Si coché : aucune matrice à maintenir (sauf clés marquées
<em>hors kit total</em>, MSIN-4484). Sinon : choisissez les droits un par un ci-dessous.
</small>
</div>
<?php } ?>
@ -1014,6 +1015,8 @@ function fxEveAccesAdminRenderKitForm($intRoleId, $strError = '')
<strong>Niveau 1</strong> = accès aux menus du hub.
<strong>Niveau 2</strong> = actions dans la page (ex. Inscriptions V2).
La page <code>/mobile</code> est la même gestion inscriptions en affichage mobile — pas une API.
Badge <span class="badge badge-warning">hors kit total</span> = non inclus dans un kit total
(MSIN-4484) ; cocher ici seulement pour une attribution explicite.
</p>
<?php
foreach ($arrOrderedGroups as $strGroup => $arrPerms) {
@ -1033,16 +1036,23 @@ function fxEveAccesAdminRenderKitForm($intRoleId, $strError = '')
$strKey = $rowPerm['perm_key'];
$strId = 'perm_' . preg_replace('/[^a-z0-9_]/', '_', $strKey);
$strScope = (string)($rowPerm['perm_scope'] ?? '');
$blnPropagates = !isset($rowPerm['perm_grants_all'])
|| intval($rowPerm['perm_grants_all']) === 1;
$strBadge = ($strScope === 'page')
? '<span class="badge badge-primary ml-1">page</span>'
: (($strScope === 'action')
? '<span class="badge badge-secondary ml-1">détail</span>'
: '');
if (!$blnPropagates) {
$strBadge .= ' <span class="badge badge-warning ml-1">hors kit total</span>';
}
$strPad = ($strScope === 'action') ? ' pl-3' : '';
$strChkClass = 'custom-control-input perm-checkbox'
. ($blnPropagates ? '' : ' perm-checkbox-no-grants-all');
?>
<div class="col-md-6 col-lg-4 mb-2<?= $strPad ?>">
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input perm-checkbox"
<input type="checkbox" class="<?= htmlspecialchars($strChkClass, ENT_QUOTES, 'UTF-8') ?>"
id="<?= htmlspecialchars($strId, ENT_QUOTES, 'UTF-8') ?>"
name="perm_keys[]"
value="<?= htmlspecialchars($strKey, ENT_QUOTES, 'UTF-8') ?>"
@ -1101,14 +1111,23 @@ function fxEveAccesAdminRenderKitForm($intRoleId, $strError = '')
if (blnAll) {
$('.perm-checkbox').prop('checked', false);
}
// MSIN-4460Ne plus forcer / desactiver « delegable » quand Kit total est coche.
// MSIN-4478Kit total = jamais délégable (invitation ne doit pas donner tout).
var $deleg = $('#role_delegable');
if ($deleg.length && !$deleg.prop('disabled')) {
if (blnAll) {
$deleg.prop('checked', false).prop('disabled', true);
} else {
$deleg.prop('disabled', false);
}
}
}
$('#role_grants_all').on('change', syncGrantsAllUi);
syncGrantsAllUi();
$('#btn-perm-all').on('click', function () {
$('.perm-checkbox').prop('checked', true);
// MSIN-4484 — ne pas cocher les clés hors kit total (évite assignation accidentelle).
$('.perm-checkbox').not('.perm-checkbox-no-grants-all').prop('checked', true);
});
$('#btn-perm-none').on('click', function () {
$('.perm-checkbox').prop('checked', false);
@ -1116,7 +1135,7 @@ function fxEveAccesAdminRenderKitForm($intRoleId, $strError = '')
$('.perm-group-block').each(function () {
var $block = $(this);
$block.find('.btn-group-all').on('click', function () {
$block.find('.perm-checkbox').prop('checked', true);
$block.find('.perm-checkbox').not('.perm-checkbox-no-grants-all').prop('checked', true);
});
$block.find('.btn-group-none').on('click', function () {
$block.find('.perm-checkbox').prop('checked', false);