diff --git a/app/CertificateExcellence.php b/app/CertificateExcellence.php index c394bda2a..674b090cf 100644 --- a/app/CertificateExcellence.php +++ b/app/CertificateExcellence.php @@ -79,6 +79,30 @@ public function preflight(): void } } + /** + * Generate the certificate PDF and save a copy to the given path (e.g. for viewing test certs). + * Does not upload to S3. Cleans up LaTeX temp files after copying. + * + * @return string The full path where the PDF was saved + */ + public function generateAndSavePdfTo(string $fullPath): string + { + try { + $this->customize_and_save_latex(); + $this->run_pdf_creation(); + $pdfName = $this->personalized_template_name . '.pdf'; + $dir = dirname($fullPath); + if (! is_dir($dir) && ! @mkdir($dir, 0775, true) && ! is_dir($dir)) { + throw new \RuntimeException("Cannot create directory: {$dir}"); + } + $contents = Storage::disk('latex')->get($pdfName); + file_put_contents($fullPath, $contents); + return $fullPath; + } finally { + $this->clean_temp_files(); + } + } + /** * Clean up LaTeX artifacts for the generated file. */ @@ -99,6 +123,14 @@ public function is_greek() return (count($split) > 1); } + /** + * Check for Cyrillic characters in the name (Russian, Ukrainian, etc.). + */ + public function is_cyrillic(): bool + { + return (bool) preg_match('/[\p{Cyrillic}]/u', $this->name_of_certificate_holder); + } + /** * Escape LaTeX special characters in user data. */ @@ -141,12 +173,12 @@ protected function customize_and_save_latex() Log::info("Using template: {$this->templateName}"); $base_template = Storage::disk('latex')->get($this->templateName); - // Always replace for both excellence & super-organiser - $template = str_replace( - '', - $this->tex_escape($this->name_of_certificate_holder), - $base_template - ); + // Name replacement: for default (non-Greek) template, wrap Cyrillic names in russian block so T2A is used + $nameReplacement = $this->tex_escape($this->name_of_certificate_holder); + if (! $this->is_greek() && $this->is_cyrillic()) { + $nameReplacement = '\\begin{otherlanguage*}{russian}' . $nameReplacement . '\\end{otherlanguage*}'; + } + $template = str_replace('', $nameReplacement, $base_template); // If super-organiser, we also replace these: if ($this->type === 'super-organiser') { diff --git a/app/Console/Commands/CertificateTestTwo.php b/app/Console/Commands/CertificateTestTwo.php index c6a7fe7c9..045c90cf3 100644 --- a/app/Console/Commands/CertificateTestTwo.php +++ b/app/Console/Commands/CertificateTestTwo.php @@ -4,47 +4,104 @@ use App\CertificateExcellence; use Illuminate\Console\Command; -use Illuminate\Support\Facades\Storage; class CertificateTestTwo extends Command { protected $signature = 'certificate:test-two {--edition=2025 : Edition year} - {--no-pdf : Only run preflight (no S3), do not keep PDFs}'; + {--type=all : excellence|super-organiser|all} + {--no-pdf : Preflight only (no S3, no PDFs kept); default} + {--keep-pdf : Generate PDFs and save locally so you can view them (storage/app/test-certificates/)} + {--generate : Generate and upload to S3 (use with care)}'; - protected $description = 'Generate two test certificates locally: one Greek name, one Russian name'; + protected $description = 'Run preflight for a few test names (Greek, Cyrillic, Latin, accented); use --keep-pdf to save viewable PDFs'; + + /** @var array */ + private static array $testNames = [ + ['name' => 'Βασιλική Μπαμπαλή', 'label' => 'Greek'], + ['name' => 'Юлія Колісниченко', 'label' => 'Ukrainian (Cyrillic)'], + ['name' => 'Иван Петров', 'label' => 'Russian (Cyrillic)'], + ['name' => 'John Smith', 'label' => 'English'], + ['name' => 'José García', 'label' => 'Accented Latin'], + ['name' => 'María Fernández', 'label' => 'Accented Latin 2'], + ]; public function handle(): int { $edition = (int) $this->option('edition'); - $preflightOnly = (bool) $this->option('no-pdf'); + $typeOption = strtolower(trim((string) $this->option('type'))); + $keepPdf = (bool) $this->option('keep-pdf'); + $uploadToS3 = (bool) $this->option('generate'); + $preflightOnly = ! $keepPdf && ! $uploadToS3; + + $typeList = $this->resolveTypes($typeOption); + if ($typeList === null) { + $this->error("Invalid --type: {$typeOption}. Use excellence, super-organiser, or all."); + return self::FAILURE; + } - $tests = [ - ['name' => 'Βασιλική Μπαμπαλή', 'label' => 'Greek'], - ['name' => 'Иван Петров', 'label' => 'Russian'], - ]; + $outDir = $keepPdf ? storage_path('app/test-certificates') : null; + if ($outDir !== null && ! is_dir($outDir)) { + mkdir($outDir, 0775, true); + } - foreach ($tests as $test) { - $this->info("Testing {$test['label']}: {$test['name']}"); + $mode = $preflightOnly ? 'Preflight only (no PDFs kept)' : ($keepPdf ? 'Generating PDFs to ' . $outDir : 'Generating and uploading to S3'); + $this->info('Certificate test names. Edition: ' . $edition . ', types: ' . implode(', ', $typeList) . '. ' . $mode); + $this->newLine(); + + $failed = 0; + $index = 0; + foreach (self::$testNames as $test) { + $this->line("--- {$test['label']}: {$test['name']} ---"); $cert = new CertificateExcellence($edition, $test['name'], 'excellence', 0, 999999, 'test@example.com'); - $this->line(' is_greek(): ' . ($cert->is_greek() ? 'true' : 'false')); - - try { - if ($preflightOnly) { - $cert->preflight(); - $this->info(" Preflight OK (no PDF kept)."); - } else { - $url = $cert->generate(); - $this->info(" Generated: {$url}"); + $this->line(' is_greek(): ' . ($cert->is_greek() ? 'true' : 'false') . ', is_cyrillic(): ' . ($cert->is_cyrillic() ? 'true' : 'false')); + + $safeLabel = preg_replace('/[^a-zA-Z0-9_-]/', '_', $test['label']); + foreach ($typeList as $certType) { + $activities = $certType === 'super-organiser' ? 10 : 0; + $certTest = new CertificateExcellence($edition, $test['name'], $certType, $activities, 999999, 'test@example.com'); + $label = $certType === 'super-organiser' ? 'SuperOrganiser' : 'Excellence'; + try { + if ($preflightOnly) { + $certTest->preflight(); + $this->info(" [{$label}] Preflight OK."); + } elseif ($keepPdf) { + $index++; + $filename = sprintf('%02d-%s-%s.pdf', $index, $safeLabel, $label); + $path = $certTest->generateAndSavePdfTo($outDir . DIRECTORY_SEPARATOR . $filename); + $this->info(" [{$label}] Saved: {$path}"); + } else { + $url = $certTest->generate(); + $this->info(" [{$label}] Generated: {$url}"); + } + } catch (\Throwable $e) { + $this->error(" [{$label}] Failed: " . $e->getMessage()); + $failed++; } - } catch (\Throwable $e) { - $this->error(' Failed: ' . $e->getMessage()); - return self::FAILURE; } + $this->newLine(); } - $this->newLine(); - $this->info('Both certificates OK. Greek uses _greek template; Russian uses default template.'); + if ($failed > 0) { + $this->error("{$failed} test(s) failed."); + return self::FAILURE; + } + if ($keepPdf) { + $this->info('All test PDFs saved. Open: ' . $outDir); + } else { + $this->info('All test names passed.'); + } return self::SUCCESS; } + + /** @return array|null */ + private function resolveTypes(string $typeOption): ?array + { + return match ($typeOption) { + 'all' => ['excellence', 'super-organiser'], + 'excellence' => ['excellence'], + 'super-organiser', 'superorganiser' => ['super-organiser'], + default => null, + }; + } } diff --git a/app/DreamJobRoleModel.php b/app/DreamJobRoleModel.php index 7db8ceba4..7a626e399 100644 --- a/app/DreamJobRoleModel.php +++ b/app/DreamJobRoleModel.php @@ -18,6 +18,8 @@ class DreamJobRoleModel extends Model 'link', 'video', 'pathway_map_link', + 'pathway_title', + 'pathway_cta_text', 'position', 'active', ]; diff --git a/app/Nova/DreamJobRoleModel.php b/app/Nova/DreamJobRoleModel.php index a5c857350..71e509fea 100644 --- a/app/Nova/DreamJobRoleModel.php +++ b/app/Nova/DreamJobRoleModel.php @@ -65,6 +65,12 @@ public function fields(Request $request): array ->nullable() ->rules('nullable', 'max:255') ->help('Either a full URL (e.g. S3 link) OR a filename in /public/docs/dream-jobs/, e.g. Career Pathway Map Anny Tubbs.pdf'), + Trix::make('Pathway section title', 'pathway_title') + ->nullable() + ->help('Shown above the pathway map (e.g. Explore Career Pathway). Supports links and formatting.'), + Text::make('Pathway CTA text', 'pathway_cta_text') + ->nullable() + ->help('Link label under the map. Defaults to "Career Pathway Map".'), Number::make('Position', 'position') ->min(0) diff --git a/database/migrations/2026_02_16_142000_add_pathway_text_fields_to_dream_job_role_models_table.php b/database/migrations/2026_02_16_142000_add_pathway_text_fields_to_dream_job_role_models_table.php new file mode 100644 index 000000000..1796cd3c2 --- /dev/null +++ b/database/migrations/2026_02_16_142000_add_pathway_text_fields_to_dream_job_role_models_table.php @@ -0,0 +1,37 @@ +text('pathway_title')->nullable()->after('pathway_map_link'); + }); + } + + if (Schema::hasTable('dream_job_role_models') && !Schema::hasColumn('dream_job_role_models', 'pathway_cta_text')) { + Schema::table('dream_job_role_models', function (Blueprint $table) { + $table->string('pathway_cta_text')->nullable()->after('pathway_title'); + }); + } + } + + public function down(): void + { + if (Schema::hasTable('dream_job_role_models') && Schema::hasColumn('dream_job_role_models', 'pathway_cta_text')) { + Schema::table('dream_job_role_models', function (Blueprint $table) { + $table->dropColumn('pathway_cta_text'); + }); + } + + if (Schema::hasTable('dream_job_role_models') && Schema::hasColumn('dream_job_role_models', 'pathway_title')) { + Schema::table('dream_job_role_models', function (Blueprint $table) { + $table->dropColumn('pathway_title'); + }); + } + } +}; diff --git a/database/seeders/DreamJobRoleModelSeeder.php b/database/seeders/DreamJobRoleModelSeeder.php index 27ea35956..e2dad1222 100644 --- a/database/seeders/DreamJobRoleModelSeeder.php +++ b/database/seeders/DreamJobRoleModelSeeder.php @@ -199,6 +199,8 @@ public function run(): void 'link' => $row['link'], 'video' => $row['video'], 'pathway_map_link' => $row['pathway_map_link'] !== '' ? $row['pathway_map_link'] : null, + 'pathway_title' => $row['pathway_title'] ?? 'Explore Career Pathway', + 'pathway_cta_text' => $row['pathway_cta_text'] ?? 'Career Pathway Map', 'position' => $index, 'active' => true, ] diff --git a/resources/views/static/dream-jobs-in-digital-role.blade.php b/resources/views/static/dream-jobs-in-digital-role.blade.php index c8b2bafa5..028ae0d80 100644 --- a/resources/views/static/dream-jobs-in-digital-role.blade.php +++ b/resources/views/static/dream-jobs-in-digital-role.blade.php @@ -213,6 +213,8 @@ 'link' => (string) ($dbItem->link ?? ''), 'video' => (string) ($dbItem->video ?? ''), 'pathway_map_link' => (string) ($dbItem->pathway_map_link ?? ''), + 'pathway_title' => (string) ($dbItem->pathway_title ?? ''), + 'pathway_cta_text' => (string) ($dbItem->pathway_cta_text ?? ''), 'country' => (string) $dbItem->country, ]; } else { @@ -220,6 +222,9 @@ abort_if(! $item, 404); } + $item['pathway_title'] = (string) ($item['pathway_title'] ?? 'Explore Career Pathway'); + $item['pathway_cta_text'] = (string) ($item['pathway_cta_text'] ?? 'Career Pathway Map'); + $list = [ (object) ['label' => 'Careers in Digital', 'href' => '/dream-jobs-in-digital'], (object) ['label' => $item['first_name'] . ' ' . $item['last_name'], 'href' => ''], @@ -336,11 +341,11 @@ class="animation-element move-background duration-[1.5s] absolute z-0 lg:-bottom ? (string) $item['pathway_map_link'] : '/docs/dream-jobs/' . ltrim((string) $item['pathway_map_link'], '/'); @endphp -

- Explore Career Pathway -

+
+ {!! $item['pathway_title'] !!} +
- Career Pathway Map + {{ $item['pathway_cta_text'] }} @endif diff --git a/storage/app/test-certificates/01-Greek-Excellence.pdf b/storage/app/test-certificates/01-Greek-Excellence.pdf new file mode 100644 index 000000000..ea01935f1 Binary files /dev/null and b/storage/app/test-certificates/01-Greek-Excellence.pdf differ diff --git a/storage/app/test-certificates/02-Greek-SuperOrganiser.pdf b/storage/app/test-certificates/02-Greek-SuperOrganiser.pdf new file mode 100644 index 000000000..717f55c2c Binary files /dev/null and b/storage/app/test-certificates/02-Greek-SuperOrganiser.pdf differ diff --git a/storage/app/test-certificates/03-Ukrainian__Cyrillic_-Excellence.pdf b/storage/app/test-certificates/03-Ukrainian__Cyrillic_-Excellence.pdf new file mode 100644 index 000000000..1490536e8 Binary files /dev/null and b/storage/app/test-certificates/03-Ukrainian__Cyrillic_-Excellence.pdf differ diff --git a/storage/app/test-certificates/04-Ukrainian__Cyrillic_-SuperOrganiser.pdf b/storage/app/test-certificates/04-Ukrainian__Cyrillic_-SuperOrganiser.pdf new file mode 100644 index 000000000..f696bee0e Binary files /dev/null and b/storage/app/test-certificates/04-Ukrainian__Cyrillic_-SuperOrganiser.pdf differ diff --git a/storage/app/test-certificates/05-Russian__Cyrillic_-Excellence.pdf b/storage/app/test-certificates/05-Russian__Cyrillic_-Excellence.pdf new file mode 100644 index 000000000..486e82707 Binary files /dev/null and b/storage/app/test-certificates/05-Russian__Cyrillic_-Excellence.pdf differ diff --git a/storage/app/test-certificates/06-Russian__Cyrillic_-SuperOrganiser.pdf b/storage/app/test-certificates/06-Russian__Cyrillic_-SuperOrganiser.pdf new file mode 100644 index 000000000..aad06890f Binary files /dev/null and b/storage/app/test-certificates/06-Russian__Cyrillic_-SuperOrganiser.pdf differ diff --git a/storage/app/test-certificates/07-English-Excellence.pdf b/storage/app/test-certificates/07-English-Excellence.pdf new file mode 100644 index 000000000..3cf12bbdf Binary files /dev/null and b/storage/app/test-certificates/07-English-Excellence.pdf differ diff --git a/storage/app/test-certificates/07-Latin-Excellence.pdf b/storage/app/test-certificates/07-Latin-Excellence.pdf new file mode 100644 index 000000000..fc1c9f5b3 Binary files /dev/null and b/storage/app/test-certificates/07-Latin-Excellence.pdf differ diff --git a/storage/app/test-certificates/08-English-SuperOrganiser.pdf b/storage/app/test-certificates/08-English-SuperOrganiser.pdf new file mode 100644 index 000000000..24ab30ad7 Binary files /dev/null and b/storage/app/test-certificates/08-English-SuperOrganiser.pdf differ diff --git a/storage/app/test-certificates/08-Latin-SuperOrganiser.pdf b/storage/app/test-certificates/08-Latin-SuperOrganiser.pdf new file mode 100644 index 000000000..515815db0 Binary files /dev/null and b/storage/app/test-certificates/08-Latin-SuperOrganiser.pdf differ diff --git a/storage/app/test-certificates/09-Accented_Latin-Excellence.pdf b/storage/app/test-certificates/09-Accented_Latin-Excellence.pdf new file mode 100644 index 000000000..2fb0267e3 Binary files /dev/null and b/storage/app/test-certificates/09-Accented_Latin-Excellence.pdf differ diff --git a/storage/app/test-certificates/10-Accented_Latin-SuperOrganiser.pdf b/storage/app/test-certificates/10-Accented_Latin-SuperOrganiser.pdf new file mode 100644 index 000000000..74c788ef6 Binary files /dev/null and b/storage/app/test-certificates/10-Accented_Latin-SuperOrganiser.pdf differ diff --git a/storage/app/test-certificates/11-Accented_Latin_2-Excellence.pdf b/storage/app/test-certificates/11-Accented_Latin_2-Excellence.pdf new file mode 100644 index 000000000..5465423c3 Binary files /dev/null and b/storage/app/test-certificates/11-Accented_Latin_2-Excellence.pdf differ diff --git a/storage/app/test-certificates/12-Accented_Latin_2-SuperOrganiser.pdf b/storage/app/test-certificates/12-Accented_Latin_2-SuperOrganiser.pdf new file mode 100644 index 000000000..e70964d14 Binary files /dev/null and b/storage/app/test-certificates/12-Accented_Latin_2-SuperOrganiser.pdf differ