@@ -336,43 +336,109 @@ function confirmation() {
336336 echo '<td width="30%"> ' .get_lang ('Date ' ).' : ' .api_convert_and_format_date ($ valueCertificate ['created_at ' ]).'</td> ' ;
337337 echo '<td width="20%"> ' ;
338338
339- // Normalize and availability checks
340- $ pathRaw = isset ( $ valueCertificate [ ' path_certificate ' ]) ? ( string ) $ valueCertificate [ ' path_certificate ' ] : '' ;
341- $ path = ltrim ( $ pathRaw , ' / ' ); // ensure no leading slash
342- $ hasPath = $ path !== '' ;
343- $ hash = $ hasPath ? pathinfo ( $ path , PATHINFO_FILENAME ) : '' ;
344-
345- // Admin can bypass publish flag for visibility
339+ /**
340+ * Resource-first per-course certificate resolution.
341+ * - First: try to resolve a Resource certificate for (cat_id, user_id).
342+ * - Never fallback to the user's general/custom certificate here.
343+ * - If no Resource exists, use legacy path_certificate (HTML/PDF in /certificates/).
344+ * - Keep publish flag behavior; platform admins bypass publish.
345+ */
346346 $ isPublished = !empty ($ valueCertificate ['publish ' ]) || api_is_platform_admin ();
347- $ isAvailable = $ hasPath && $ isPublished ;
348347
349- // Build URLs only if available
350- $ htmlUrl = $ isAvailable ? api_get_path (WEB_PATH ).'certificates/ ' .$ hash .'.html ' : '' ;
351- $ pdfUrl = $ isAvailable ? api_get_path (WEB_PATH ).'certificates/ ' .$ hash .'.pdf ' : '' ;
348+ $ certRepo = Container::getGradeBookCertificateRepository ();
349+ $ router = null ;
350+ try {
351+ $ router = Container::getRouter (); // Might not exist in some installs; guarded below.
352+ } catch (\Throwable $ e ) {
353+ // Non-fatal. We'll just skip route generation if router is not available.
354+ }
355+
356+ $ htmlUrl = '' ;
357+ $ pdfUrl = '' ;
358+ $ isAvailable = false ;
359+
360+ // Try to resolve the per-category (course/session) resource
361+ try {
362+ $ entity = $ certRepo ->getCertificateByUserId (
363+ $ categoryId === 0 ? null : (int ) $ categoryId ,
364+ (int ) $ value ['user_id ' ]
365+ );
366+
367+ if ($ entity && $ entity ->hasResourceNode ()) {
368+ // HTML is served through the Resource layer (secured, hashed filename)
369+ $ htmlUrl = (string ) $ certRepo ->getResourceFileUrl ($ entity );
370+ $ isAvailable = $ isPublished && $ htmlUrl !== '' ;
371+
372+ // PDF is served by your Symfony controller (update the route name/params if needed)
373+ if ($ router && $ isAvailable ) {
374+ // Attempt 1: route by certificateId (common signature)
375+ try {
376+ $ pdfUrl = $ router ->generate ('gradebook_certificate_pdf ' , [
377+ 'certificateId ' => (int ) $ valueCertificate ['id ' ],
378+ ]);
379+ } catch (\Throwable $ e1 ) {
380+ // Attempt 2: route by userId+catId (alternative signature)
381+ try {
382+ $ pdfUrl = $ router ->generate ('gradebook_certificate_pdf ' , [
383+ 'userId ' => (int ) $ value ['user_id ' ],
384+ 'catId ' => (int ) $ categoryId ,
385+ ]);
386+ } catch (\Throwable $ e2 ) {
387+ // Route not found or wrong signature: leave $pdfUrl empty.
388+ error_log ('[gradebook_display_certificate] PDF route resolution failed: ' .$ e2 ->getMessage ());
389+ }
390+ }
391+ }
392+ }
393+ } catch (\Throwable $ e ) {
394+ error_log ('[gradebook_display_certificate] resource resolve error: ' .$ e ->getMessage ());
395+ }
396+
397+ // Legacy per-course fallback ONLY if no resource is available.
398+ // IMPORTANT: do NOT fallback to general/custom certificate on this screen.
399+ if (!$ isAvailable ) {
400+ $ pathRaw = isset ($ valueCertificate ['path_certificate ' ]) ? (string ) $ valueCertificate ['path_certificate ' ] : '' ;
401+ $ path = ltrim ($ pathRaw , '/ ' ); // normalize: remove leading slash if present
402+ $ hasPath = $ path !== '' ;
403+ $ hash = $ hasPath ? pathinfo ($ path , PATHINFO_FILENAME ) : '' ;
404+
405+ $ isAvailable = $ hasPath && $ isPublished ;
406+
407+ if ($ isAvailable ) {
408+ $ htmlUrl = api_get_path (WEB_PATH ).'certificates/ ' .$ hash .'.html ' ;
409+ $ pdfUrl = api_get_path (WEB_PATH ).'certificates/ ' .$ hash .'.pdf ' ;
410+ }
411+ }
352412
353- // HTML certificate button/link
413+ // Render buttons (enabled/disabled) preserving existing UI
354414 if ($ isAvailable ) {
415+ // HTML certificate button/link
355416 echo Display::url (
356417 get_lang ('Certificate ' ),
357418 $ htmlUrl ,
358419 ['target ' => '_blank ' , 'class ' => 'btn btn--plain ' ]
359420 );
421+
422+ // PDF download icon/link (only if we have a URL)
423+ if (!empty ($ pdfUrl )) {
424+ echo Display::url (
425+ Display::getMdiIcon (ActionIcon::EXPORT_PDF , 'ch-tool-icon ' , null , ICON_SIZE_SMALL , get_lang ('Download ' )),
426+ $ pdfUrl ,
427+ ['target ' => '_blank ' , 'title ' => 'Download PDF certificate ' ]
428+ );
429+ } else {
430+ // Route not available: show disabled icon with a clear tooltip
431+ echo '<button type="button" class="btn btn-link disabled" disabled '
432+ .'title="PDF route unavailable"> '
433+ .Display::getMdiIcon (ActionIcon::EXPORT_PDF , 'ch-tool-icon text-muted ' , null , ICON_SIZE_SMALL , get_lang ('PDF route unavailable ' ))
434+ .'</button> ' ;
435+ }
360436 } else {
361- // Disabled button with clear message
437+ // Disabled HTML button
362438 echo '<button type="button" class="btn btn--plain disabled" disabled '
363439 .'title="Certificate not available"> ' .get_lang ('Certificate ' ).' </button> ' ;
364- }
365- echo PHP_EOL ;
366440
367- // PDF download button/link (mdi icon)
368- if ($ isAvailable ) {
369- echo Display::url (
370- Display::getMdiIcon (ActionIcon::EXPORT_PDF , 'ch-tool-icon ' , null , ICON_SIZE_SMALL , get_lang ('Download ' )),
371- $ pdfUrl ,
372- ['target ' => '_blank ' , 'title ' => 'Download PDF certificate ' ]
373- );
374- } else {
375- // Disabled icon with tooltip
441+ // Disabled PDF icon
376442 echo '<button type="button" class="btn btn-link disabled" disabled '
377443 .'title="PDF download unavailable"> '
378444 .Display::getMdiIcon (ActionIcon::EXPORT_PDF , 'ch-tool-icon text-muted ' , null , ICON_SIZE_SMALL , get_lang ('PDF download unavailable ' ))
0 commit comments