feat: sitio CV completo v1.0.0

- 4 temas WordPress: persona-cv (activo), lumina, apex-cv, cvolio
- Plugin pflores-cv-fields con CMB2 (100% gratuito) — admin editable
- Generador PDF dinámico con Chromium headless (?cv_pdf=1)
- Datos del CV de Patricia Flores cargados en CMB2
- URL dinámica con soporte proxy SSL (NPM/X-Forwarded-Proto)
- WP Mail SMTP instalado (pendiente config SMTP)
- Documentación completa: PROMPT_IA.md, DEPLOY_WEB.md, PRODUCCION.md, VERSIONADO.md
This commit is contained in:
2026-06-05 11:21:30 -05:00
parent cc6545604c
commit 5846c515f3
77 changed files with 9567 additions and 220 deletions
@@ -0,0 +1,499 @@
<?php
/**
* Generador de PDF del CV — replica el diseño del DOCX original.
* Endpoint: /?cv_pdf=1 → descarga CV_Patricia_Flores_Sanchez.pdf
*
* Diseño fiel al DOCX:
* - Columna única, A4
* - Colores: #1B3A5C (navy), #3A3A3A (cuerpo), #666666 (secundario)
* - Fuentes: nombre 19pt, cargos 10pt, cuerpo 9pt, secundario 8pt
* - Secciones: MAYÚSCULAS + borde inferior navy
* - Cargo + período en la misma línea (izq / der)
* - Certificaciones: Título — Emisor Año
* - Competencias: Grupo: habilidad · habilidad · ...
*/
declare(strict_types=1);
add_filter('query_vars', function (array $vars): array {
$vars[] = 'cv_pdf';
return $vars;
});
add_action('template_redirect', 'pflores_handle_pdf_download');
function pflores_handle_pdf_download(): void
{
if ((string) get_query_var('cv_pdf') !== '1') {
return;
}
$chromium = pflores_find_chromium();
if ($chromium === '') {
wp_die('Chromium no disponible en el servidor.', 'Error PDF', ['response' => 500]);
}
$cv = function_exists('pflores_get_cv_data') ? pflores_get_cv_data() : [];
$html = pflores_build_cv_html($cv);
$ts = time();
$tmpDir = sys_get_temp_dir();
$htmlFile = $tmpDir . '/cv_pf_' . $ts . '.html';
$pdfFile = $tmpDir . '/cv_pf_' . $ts . '.pdf';
file_put_contents($htmlFile, $html);
$cmd = escapeshellcmd($chromium)
. ' --headless --no-sandbox --disable-gpu'
. ' --run-all-compositor-stages-before-draw'
. ' --print-to-pdf=' . escapeshellarg($pdfFile)
. ' --print-to-pdf-no-header'
. ' "file://' . $htmlFile . '"'
. ' 2>/dev/null';
exec($cmd);
@unlink($htmlFile);
if (! file_exists($pdfFile) || filesize($pdfFile) < 1024) {
@unlink($pdfFile);
wp_die('No se pudo generar el PDF.', 'Error PDF', ['response' => 500]);
}
header('Content-Type: application/pdf');
header('Content-Disposition: attachment; filename="CV_Patricia_Flores_Sanchez.pdf"');
header('Content-Length: ' . filesize($pdfFile));
header('Cache-Control: no-cache, no-store, must-revalidate');
readfile($pdfFile);
@unlink($pdfFile);
exit;
}
function pflores_find_chromium(): string
{
foreach (['chromium', 'chromium-browser', 'google-chrome', 'google-chrome-stable'] as $bin) {
$path = trim((string) shell_exec('which ' . escapeshellarg($bin) . ' 2>/dev/null'));
if ($path !== '' && file_exists($path)) {
return $path;
}
}
return '';
}
/* ═══════════════════════════════════════════════════════════
CONSTRUCTOR HTML — réplica fiel del DOCX
═══════════════════════════════════════════════════════════ */
function pflores_build_cv_html(array $cv): string
{
/* ── datos ── */
$hero = $cv['hero'] ?? [];
$metrics = $cv['metrics'] ?? [];
$highlights = $cv['highlights'] ?? [];
$summary = $cv['summary'] ?? [];
$impact = $cv['impact'] ?? [];
$experience = $cv['experience'] ?? [];
$education = $cv['education'] ?? [];
$certifications = $cv['certifications'] ?? [];
$competencies = $cv['competencies'] ?? [];
$volunteer = $cv['volunteer'] ?? [];
$additional = $cv['additional'] ?? [];
$name = $hero['name'] ?? '';
$role = $hero['role'] ?? '';
$role2 = $hero['role2'] ?? '';
$location = $hero['location'] ?? '';
$email = $hero['email'] ?? '';
$phone = $hero['phone'] ?? '';
$linkedin = $hero['linkedin'] ?? '';
$avail = $hero['availability'] ?? '';
$focus = $hero['focus'] ?? '';
$langs = $hero['languages'] ?? '';
$web = $hero['web'] ?? '';
$intro = $hero['intro'] ?? '';
/* ── helpers ── */
$h = fn(string $s): string => htmlspecialchars($s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
$nl = "\n";
/* ── estilos (replica el DOCX) ── */
$css = '
* { box-sizing: border-box; margin: 0; padding: 0; }
@page {
size: A4;
margin: 16mm 18mm 16mm 18mm;
}
body {
font-family: Arial, Helvetica, sans-serif;
font-size: 9pt;
color: #3A3A3A;
background: #ffffff;
line-height: 1.45;
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
/* ── CABECERA ── */
.cv-name {
font-size: 19pt;
font-weight: 700;
color: #1B3A5C;
line-height: 1.1;
margin-bottom: 3px;
}
.cv-role {
font-size: 10pt;
font-weight: 700;
color: #1B3A5C;
margin-bottom: 2px;
}
.cv-contact {
font-size: 8pt;
color: #666666;
margin-bottom: 10px;
}
.cv-contact span { margin: 0 4px; }
/* ── SECCIONES ── */
.sec {
margin-bottom: 10px;
}
.sec-title {
font-size: 10pt;
font-weight: 700;
color: #1B3A5C;
text-transform: uppercase;
letter-spacing: .04em;
border-bottom: 1.5px solid #1B3A5C;
padding-bottom: 2px;
margin-bottom: 6px;
}
/* ── PERFIL ── */
.profile-text {
font-size: 9pt;
color: #3A3A3A;
line-height: 1.55;
text-align: justify;
}
/* ── EXPERIENCIA ── */
.exp-item { margin-bottom: 8px; }
.exp-header {
display: flex;
justify-content: space-between;
align-items: baseline;
}
.exp-title {
font-size: 10pt;
font-weight: 700;
color: #1B3A5C;
}
.exp-period {
font-size: 9pt;
color: #3A3A3A;
font-style: italic;
white-space: nowrap;
margin-left: 8px;
}
.exp-company {
font-size: 9pt;
color: #666666;
margin-bottom: 3px;
}
.exp-desc {
font-size: 9pt;
color: #3A3A3A;
margin-bottom: 3px;
font-style: italic;
}
.exp-bullets {
margin: 0;
padding-left: 0;
list-style: none;
}
.exp-bullets li {
font-size: 9pt;
color: #3A3A3A;
padding-left: 12px;
position: relative;
margin-bottom: 2px;
line-height: 1.45;
text-align: justify;
}
.exp-bullets li::before {
content: "•";
position: absolute;
left: 0;
color: #1B3A5C;
font-weight: 700;
}
/* ── FORMACIÓN ── */
.edu-item {
margin-bottom: 6px;
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.edu-left { flex: 1; }
.edu-degree {
font-size: 9pt;
font-weight: 700;
color: #1B3A5C;
}
.edu-institution {
font-size: 8pt;
color: #666666;
margin-top: 1px;
}
.edu-detail {
font-size: 8pt;
color: #666666;
font-style: italic;
margin-top: 1px;
}
.edu-year {
font-size: 9pt;
font-weight: 700;
color: #1B3A5C;
white-space: nowrap;
margin-left: 8px;
}
/* ── CERTIFICACIONES ── */
.cert-item {
display: flex;
justify-content: space-between;
font-size: 8pt;
color: #3A3A3A;
margin-bottom: 2.5px;
line-height: 1.4;
}
.cert-title { flex: 1; padding-right: 8px; }
.cert-year { color: #1B3A5C; font-weight: 700; white-space: nowrap; }
/* ── COMPETENCIAS ── */
.comp-line {
font-size: 9pt;
color: #3A3A3A;
margin-bottom: 3px;
line-height: 1.45;
}
.comp-line strong { color: #1B3A5C; }
/* ── VOLUNTARIADO / ADICIONAL ── */
.vol-header {
display: flex;
justify-content: space-between;
align-items: baseline;
}
.vol-title { font-size: 9pt; font-weight: 700; color: #1B3A5C; }
.vol-year { font-size: 9pt; font-weight: 700; color: #1B3A5C; }
.vol-body { font-size: 9pt; color: #3A3A3A; line-height: 1.45; margin-top: 1px; }
.add-line {
font-size: 9pt;
color: #3A3A3A;
margin-bottom: 2px;
}
.add-line strong { color: #1B3A5C; }
.cv-footer-line {
font-size: 8pt;
color: #999999;
text-align: right;
margin-top: 8px;
border-top: .5px solid #dddddd;
padding-top: 4px;
}
';
/* ═══════════ SECCIÓN: PERFIL PROFESIONAL ═══════════ */
// Combina intro + summary (como el DOCX)
$summaryBody = $summary['body'] ?? '';
$profileFull = trim($intro);
if ($summaryBody && $summaryBody !== $intro) {
// Evitar duplicar si intro ya contiene el resumen
if (strpos($profileFull, substr($summaryBody, 0, 30)) === false) {
$profileFull .= ' ' . $summaryBody;
}
}
/* ═══════════ SECCIÓN: EXPERIENCIA ═══════════ */
$expHtml = '';
foreach ($experience as $job) {
$expHtml .= '<div class="exp-item">';
$expHtml .= '<div class="exp-header">';
$expHtml .= '<span class="exp-title">' . $h($job['title'] ?? '') . '</span>';
$expHtml .= '<span class="exp-period">' . $h($job['period'] ?? '') . '</span>';
$expHtml .= '</div>';
$company = $h($job['company'] ?? '');
$loc = $h($job['location'] ?? '');
if ($company || $loc) {
$expHtml .= '<p class="exp-company">' . $company . ($loc ? ' &nbsp;|&nbsp; ' . $loc : '') . '</p>';
}
if (! empty($job['lead'])) {
$expHtml .= '<p class="exp-desc">' . $h($job['lead']) . '</p>';
}
if (! empty($job['bullets'])) {
$expHtml .= '<ul class="exp-bullets">';
foreach ($job['bullets'] as $bullet) {
$expHtml .= '<li>' . $h($bullet) . '</li>';
}
$expHtml .= '</ul>';
}
$expHtml .= '</div>';
}
/* ═══════════ SECCIÓN: FORMACIÓN ═══════════ */
$eduHtml = '';
foreach ($education as $edu) {
$eduHtml .= '<div class="edu-item">';
$eduHtml .= '<div class="edu-left">';
$eduHtml .= '<p class="edu-degree">' . $h($edu['title'] ?? '') . '</p>';
$eduHtml .= '<p class="edu-institution">' . $h($edu['institution'] ?? '') . '</p>';
if (! empty($edu['detail'])) {
$eduHtml .= '<p class="edu-detail">' . $h($edu['detail']) . '</p>';
}
$eduHtml .= '</div>';
$eduHtml .= '<span class="edu-year">' . $h($edu['year'] ?? '') . '</span>';
$eduHtml .= '</div>';
}
/* ═══════════ SECCIÓN: CERTIFICACIONES ═══════════ */
$certHtml = '';
foreach ($certifications as $cert) {
$title = $h($cert['title'] ?? '');
$issuer = $h($cert['issuer'] ?? '');
$year = $h($cert['year'] ?? '');
$certHtml .= '<div class="cert-item">';
$certHtml .= '<span class="cert-title">' . $title . ($issuer ? ' — ' . $issuer : '') . '</span>';
$certHtml .= '<span class="cert-year">' . $year . '</span>';
$certHtml .= '</div>';
}
/* ═══════════ SECCIÓN: COMPETENCIAS ═══════════ */
$compHtml = '';
foreach ($competencies as $groupName => $items) {
$skills = [];
foreach ($items as $item) {
$skills[] = is_array($item) ? ($item['name'] ?? '') : (string) $item;
}
$skillsStr = implode(' · ', array_filter($skills));
$compHtml .= '<p class="comp-line"><strong>' . $h($groupName) . ':</strong>&nbsp; ' . $h($skillsStr) . '</p>';
}
/* ═══════════ SECCIÓN: VOLUNTARIADO ═══════════ */
$volHtml = '';
if (! empty($volunteer['title'])) {
$volHtml .= '<div class="vol-header">';
$volHtml .= '<span class="vol-title">' . $h($volunteer['title']) . '</span>';
$volHtml .= '<span class="vol-year">' . $h($volunteer['year'] ?? '') . '</span>';
$volHtml .= '</div>';
if (! empty($volunteer['body'])) {
$volHtml .= '<p class="vol-body">' . $h($volunteer['body']) . '</p>';
}
}
/* ═══════════ SECCIÓN: INFORMACIÓN ADICIONAL ═══════════ */
$addHtml = '';
$addLines = [
'Idiomas' => $langs,
'Disponibilidad' => $avail,
'Enfoque' => $focus,
];
// Añadir los del array additional filtrando redundancias
foreach ($additional as $k => $v) {
if ($v !== '' && ! in_array($k, ['Disponibilidad', 'Idiomas'], true)) {
$addLines[$k] = $v;
}
}
foreach ($addLines as $k => $v) {
if ($v === '') continue;
$addHtml .= '<p class="add-line"><strong>' . $h($k) . ':</strong>&nbsp;' . $h($v) . '</p>';
}
if ($web) {
$addHtml .= '<p class="add-line"><strong>Web:</strong>&nbsp;' . $h($web) . '</p>';
}
/* ═══════════ LÍNEA DE CONTACTO (cabecera) ═══════════ */
$contactParts = [];
if ($email) $contactParts[] = 'Correo: ' . $email;
if ($phone) $contactParts[] = 'Tel.: ' . $phone;
if ($linkedin) {
// Mostrar URL limpia sin https://www.
$lnDisplay = preg_replace('#^https?://(www\.)?#', '', urldecode($linkedin));
$contactParts[] = 'LinkedIn: ' . $lnDisplay;
}
if ($location) $contactParts[] = $location;
$contactLine = implode(' | ', $contactParts);
/* ═══════════ TÍTULO del documento ═══════════ */
$fullRole = $role . ($role2 ? ' | ' . $role2 : '');
$year = gmdate('Y');
/* ═══════════ HTML COMPLETO ═══════════ */
return '<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<title>CV — ' . $h($name) . '</title>
<style>' . $css . '</style>
</head>
<body>
<!-- CABECERA -->
<p class="cv-name">' . $h($name) . '</p>
<p class="cv-role">' . $h($fullRole) . '</p>
<p class="cv-contact">' . $h($contactLine) . '</p>
<!-- PERFIL PROFESIONAL -->
<div class="sec">
<p class="sec-title">Perfil profesional</p>
<p class="profile-text">' . $h($profileFull) . '</p>
</div>
<!-- EXPERIENCIA PROFESIONAL -->
<div class="sec">
<p class="sec-title">Experiencia profesional</p>
' . $expHtml . '
</div>
<!-- FORMACIÓN ACADÉMICA -->
<div class="sec">
<p class="sec-title">Formación académica</p>
' . $eduHtml . '
</div>
<!-- CERTIFICACIONES RELEVANTES -->
<div class="sec">
<p class="sec-title">Certificaciones relevantes</p>
' . $certHtml . '
</div>
<!-- COMPETENCIAS CLAVE -->
' . ($compHtml ? '<div class="sec">
<p class="sec-title">Competencias clave</p>
' . $compHtml . '
</div>' : '') . '
<!-- VOLUNTARIADO -->
' . ($volHtml ? '<div class="sec">
<p class="sec-title">Voluntariado</p>
' . $volHtml . '
</div>' : '') . '
<!-- INFORMACIÓN ADICIONAL -->
' . ($addHtml ? '<div class="sec">
<p class="sec-title">Información adicional</p>
' . $addHtml . '
</div>' : '') . '
<p class="cv-footer-line">' . $h($location) . ' — ' . $year . '</p>
</body>
</html>';
}
@@ -0,0 +1,350 @@
<?php
/**
* Plugin Name: Pflores CV Fields
* Description: Página de opciones y campos para el CV de Patricia Flores. Usa CMB2 (100 % gratuito, WordPress.org).
* Version: 2.0.0
* Author: mgonzalez
* Text Domain: pflores-cv
*/
declare(strict_types=1);
if (! defined('ABSPATH')) {
exit;
}
require_once __DIR__ . '/pdf-generator.php';
/* ─────────────────────────────────────────────
1. PÁGINAS DE OPCIONES (menú WP-Admin)
────────────────────────────────────────────── */
add_action('cmb2_admin_init', 'pflores_register_pages');
function pflores_register_pages(): void
{
/* ── PERFIL Y CONTACTO ── */
$perfil = new_cmb2_box([
'id' => 'pflores_perfil',
'title' => 'Perfil y contacto',
'object_types' => ['options-page'],
'option_key' => 'pflores_perfil',
'menu_title' => 'CV Patricia Flores',
'icon_url' => 'dashicons-id-alt',
'position' => 3,
'save_button' => 'Guardar cambios',
'capability' => 'manage_options',
]);
$perfil->add_field(['id' => 'pf_name', 'name' => 'Nombre completo', 'type' => 'text']);
$perfil->add_field(['id' => 'pf_role', 'name' => 'Cargo principal', 'type' => 'text']);
$perfil->add_field(['id' => 'pf_role2', 'name' => 'Sector / subtítulo', 'type' => 'text']);
$perfil->add_field(['id' => 'pf_intro', 'name' => 'Introducción', 'type' => 'textarea', 'attributes' => ['rows' => 4]]);
$perfil->add_field(['id' => 'pf_summary_title', 'name' => 'Título del perfil', 'type' => 'text']);
$perfil->add_field(['id' => 'pf_summary_body', 'name' => 'Resumen profesional', 'type' => 'textarea', 'attributes' => ['rows' => 5]]);
$perfil->add_field(['id' => 'pf_location', 'name' => 'Ubicación', 'type' => 'text']);
$perfil->add_field(['id' => 'pf_email', 'name' => 'Correo electrónico', 'type' => 'text_email']);
$perfil->add_field(['id' => 'pf_phone', 'name' => 'Teléfono', 'type' => 'text']);
$perfil->add_field(['id' => 'pf_linkedin', 'name' => 'LinkedIn URL', 'type' => 'text_url']);
$perfil->add_field(['id' => 'pf_availability', 'name' => 'Disponibilidad', 'type' => 'text']);
$perfil->add_field(['id' => 'pf_focus', 'name' => 'Enfoque profesional', 'type' => 'text']);
$perfil->add_field(['id' => 'pf_languages', 'name' => 'Idiomas', 'type' => 'text']);
$perfil->add_field(['id' => 'pf_photo', 'name' => 'Foto de perfil', 'type' => 'file', 'options' => ['url' => false], 'text' => ['add_upload_file_text' => 'Seleccionar foto']]);
$perfil->add_field(['id' => 'pf_born', 'name' => 'Fecha de nacimiento', 'type' => 'text']);
$perfil->add_field(['id' => 'pf_ci', 'name' => 'Cédula de identidad', 'type' => 'text']);
$perfil->add_field(['id' => 'pf_web', 'name' => 'Web profesional', 'type' => 'text']);
/* ── EXPERIENCIA ── */
$exp = new_cmb2_box([
'id' => 'pflores_experiencia',
'title' => 'Experiencia profesional',
'object_types' => ['options-page'],
'option_key' => 'pflores_experiencia',
'parent_slug' => 'pflores_perfil',
'menu_title' => 'Experiencia',
'save_button' => 'Guardar cambios',
'capability' => 'manage_options',
]);
$exp_group = $exp->add_field([
'id' => 'pf_experience',
'type' => 'group',
'description' => 'Cargos del más reciente al más antiguo',
'options' => [
'group_title' => 'Cargo {#}',
'add_button' => 'Añadir cargo',
'remove_button' => 'Eliminar cargo',
'sortable' => true,
'closed' => true,
],
]);
$exp->add_group_field($exp_group, ['id' => 'period', 'name' => 'Período (ej: Abr. 2022 – Presente)', 'type' => 'text']);
$exp->add_group_field($exp_group, ['id' => 'title', 'name' => 'Cargo', 'type' => 'text']);
$exp->add_group_field($exp_group, ['id' => 'company', 'name' => 'Institución / Empresa', 'type' => 'text']);
$exp->add_group_field($exp_group, ['id' => 'location', 'name' => 'Lugar', 'type' => 'text']);
$exp->add_group_field($exp_group, ['id' => 'lead', 'name' => 'Descripción breve', 'type' => 'textarea_small']);
$exp->add_group_field($exp_group, [
'id' => 'bullets_text',
'name' => 'Logros y actividades',
'type' => 'textarea',
'description' => 'Un punto por línea. Sin guiones ni asteriscos.',
'attributes' => ['rows' => 8],
]);
/* ── FORMACIÓN Y CERTIFICACIONES ── */
$form = new_cmb2_box([
'id' => 'pflores_formacion',
'title' => 'Formación y certificaciones',
'object_types' => ['options-page'],
'option_key' => 'pflores_formacion',
'parent_slug' => 'pflores_perfil',
'menu_title' => 'Formación',
'save_button' => 'Guardar cambios',
'capability' => 'manage_options',
]);
$edu_group = $form->add_field([
'id' => 'pf_education',
'type' => 'group',
'options' => ['group_title' => 'Título {#}', 'add_button' => 'Añadir título', 'remove_button' => 'Eliminar', 'sortable' => true, 'closed' => true],
]);
$form->add_group_field($edu_group, ['id' => 'year', 'name' => 'Año', 'type' => 'text', 'attributes' => ['size' => 6]]);
$form->add_group_field($edu_group, ['id' => 'title', 'name' => 'Título', 'type' => 'text']);
$form->add_group_field($edu_group, ['id' => 'institution', 'name' => 'Institución','type' => 'text']);
$form->add_group_field($edu_group, ['id' => 'detail', 'name' => 'Detalle / Nota de reconocimiento', 'type' => 'textarea_small']);
$cert_group = $form->add_field([
'id' => 'pf_certifications',
'type' => 'group',
'description' => 'Certificaciones relevantes',
'options' => ['group_title' => 'Certificación {#}', 'add_button' => 'Añadir certificación', 'remove_button' => 'Eliminar', 'sortable' => true, 'closed' => true],
]);
$form->add_group_field($cert_group, ['id' => 'year', 'name' => 'Año', 'type' => 'text', 'attributes' => ['size' => 6]]);
$form->add_group_field($cert_group, ['id' => 'title', 'name' => 'Título', 'type' => 'text']);
$form->add_group_field($cert_group, ['id' => 'issuer', 'name' => 'Emisor', 'type' => 'text']);
/* ── COMPETENCIAS ── */
$comp = new_cmb2_box([
'id' => 'pflores_competencias',
'title' => 'Competencias y habilidades',
'object_types' => ['options-page'],
'option_key' => 'pflores_competencias',
'parent_slug' => 'pflores_perfil',
'menu_title' => 'Competencias',
'save_button' => 'Guardar cambios',
'capability' => 'manage_options',
]);
$comp_group = $comp->add_field([
'id' => 'pf_competencies',
'type' => 'group',
'options' => ['group_title' => 'Grupo {#}', 'add_button' => 'Añadir grupo', 'remove_button' => 'Eliminar', 'sortable' => true, 'closed' => true],
]);
$comp->add_group_field($comp_group, ['id' => 'group_name', 'name' => 'Nombre del grupo (ej: Finanzas y Operaciones)', 'type' => 'text']);
$comp->add_group_field($comp_group, [
'id' => 'skills_text',
'name' => 'Habilidades',
'type' => 'textarea',
'description' => 'Una por línea. Formato: Nombre de la habilidad|85 (el número es el porcentaje 0-100).',
'attributes' => ['rows' => 7],
]);
/* ── MÉTRICAS, IMPACTO Y EXTRAS ── */
$extra = new_cmb2_box([
'id' => 'pflores_extras',
'title' => 'Métricas, impacto y datos adicionales',
'object_types' => ['options-page'],
'option_key' => 'pflores_extras',
'parent_slug' => 'pflores_perfil',
'menu_title' => 'Métricas / Impacto',
'save_button' => 'Guardar cambios',
'capability' => 'manage_options',
]);
$metrics_group = $extra->add_field([
'id' => 'pf_metrics',
'type' => 'group',
'description' => 'Contadores principales del héroe',
'options' => ['group_title' => 'Métrica {#}', 'add_button' => 'Añadir métrica', 'remove_button' => 'Eliminar', 'sortable' => true, 'closed' => false],
]);
$extra->add_group_field($metrics_group, ['id' => 'value', 'name' => 'Valor (ej: 22+, USD 5 M)', 'type' => 'text']);
$extra->add_group_field($metrics_group, ['id' => 'suffix', 'name' => 'Sufijo (opcional)', 'type' => 'text', 'attributes' => ['size' => 8]]);
$extra->add_group_field($metrics_group, ['id' => 'label', 'name' => 'Etiqueta descriptiva', 'type' => 'text']);
$hl_group = $extra->add_field([
'id' => 'pf_highlights',
'type' => 'group',
'description' => 'Logros destacados con número grande',
'options' => ['group_title' => 'Logro {#}', 'add_button' => 'Añadir logro', 'remove_button' => 'Eliminar', 'sortable' => true, 'closed' => false],
]);
$extra->add_group_field($hl_group, ['id' => 'value', 'name' => 'Valor (ej: 74 %, 9/10)', 'type' => 'text']);
$extra->add_group_field($hl_group, ['id' => 'suffix', 'name' => 'Sufijo (opcional)', 'type' => 'text', 'attributes' => ['size' => 8]]);
$extra->add_group_field($hl_group, ['id' => 'label', 'name' => 'Descripción', 'type' => 'text']);
$impact_group = $extra->add_field([
'id' => 'pf_impact',
'type' => 'group',
'description' => 'Áreas de impacto ejecutivo',
'options' => ['group_title' => 'Impacto {#}', 'add_button' => 'Añadir área', 'remove_button' => 'Eliminar', 'sortable' => true, 'closed' => false],
]);
$extra->add_group_field($impact_group, ['id' => 'title', 'name' => 'Título', 'type' => 'text']);
$extra->add_group_field($impact_group, ['id' => 'body', 'name' => 'Descripción', 'type' => 'textarea_small']);
$extra->add_field(['id' => 'pf_volunteer_title', 'name' => 'Voluntariado — Título', 'type' => 'text']);
$extra->add_field(['id' => 'pf_volunteer_year', 'name' => 'Voluntariado — Año', 'type' => 'text', 'attributes' => ['size' => 8]]);
$extra->add_field(['id' => 'pf_volunteer_body', 'name' => 'Voluntariado — Descripción', 'type' => 'textarea_small']);
}
/* ─────────────────────────────────────────────
2. FUNCIÓN CENTRAL — lee de CMB2
────────────────────────────────────────────── */
function pflores_get_cv_data(): array
{
if (! function_exists('cmb2_get_option')) {
return pflores_cv_fallback_data();
}
$perfil = get_option('pflores_perfil', []);
$experiencia = get_option('pflores_experiencia', []);
$formacion = get_option('pflores_formacion', []);
$competencias = get_option('pflores_competencias', []);
$extras = get_option('pflores_extras', []);
// ── Experiencia ──
$experience = [];
foreach ((array) ($experiencia['pf_experience'] ?? []) as $job) {
$bullets = array_values(array_filter(
array_map('trim', explode("\n", (string) ($job['bullets_text'] ?? '')))
));
$experience[] = [
'period' => $job['period'] ?? '',
'title' => $job['title'] ?? '',
'company' => $job['company'] ?? '',
'location' => $job['location'] ?? '',
'lead' => $job['lead'] ?? '',
'bullets' => $bullets,
];
}
// ── Educación ──
$education = [];
foreach ((array) ($formacion['pf_education'] ?? []) as $edu) {
$education[] = [
'year' => $edu['year'] ?? '',
'title' => $edu['title'] ?? '',
'institution' => $edu['institution'] ?? '',
'detail' => $edu['detail'] ?? '',
];
}
// ── Certificaciones ──
$certifications = [];
foreach ((array) ($formacion['pf_certifications'] ?? []) as $cert) {
$certifications[] = [
'year' => $cert['year'] ?? '',
'title' => $cert['title'] ?? '',
'issuer' => $cert['issuer'] ?? '',
];
}
// ── Competencias (nombre|porcentaje) ──
$competencies = [];
foreach ((array) ($competencias['pf_competencies'] ?? []) as $group) {
$groupName = $group['group_name'] ?? '';
if ($groupName === '') {
continue;
}
$skills = [];
foreach (array_filter(array_map('trim', explode("\n", (string) ($group['skills_text'] ?? '')))) as $line) {
$parts = explode('|', $line, 2);
$skills[] = ['name' => trim($parts[0]), 'pct' => isset($parts[1]) ? (int) trim($parts[1]) : 80];
}
$competencies[$groupName] = $skills;
}
// ── Métricas ──
$metrics = [];
foreach ((array) ($extras['pf_metrics'] ?? []) as $m) {
$metrics[] = ['value' => $m['value'] ?? '', 'suffix' => $m['suffix'] ?? '', 'label' => $m['label'] ?? ''];
}
// ── Highlights ──
$highlights = [];
foreach ((array) ($extras['pf_highlights'] ?? []) as $h) {
$highlights[] = ['value' => $h['value'] ?? '', 'suffix' => $h['suffix'] ?? '', 'label' => $h['label'] ?? ''];
}
// ── Impacto ──
$impact = [];
foreach ((array) ($extras['pf_impact'] ?? []) as $item) {
$impact[] = ['title' => $item['title'] ?? '', 'body' => $item['body'] ?? ''];
}
// ── Foto (CMB2 file field devuelve ID; obtenemos la URL) ──
$photoId = $perfil['pf_photo'] ?? '';
$photoUrl = $photoId ? (string) wp_get_attachment_url((int) $photoId) : '';
return [
'hero' => [
'eyebrow' => 'Currículum ejecutivo',
'name' => $perfil['pf_name'] ?? '',
'role' => $perfil['pf_role'] ?? '',
'role2' => $perfil['pf_role2'] ?? '',
'intro' => $perfil['pf_intro'] ?? '',
'location' => $perfil['pf_location'] ?? '',
'email' => $perfil['pf_email'] ?? '',
'phone' => $perfil['pf_phone'] ?? '',
'linkedin' => $perfil['pf_linkedin'] ?? '',
'availability' => $perfil['pf_availability'] ?? '',
'focus' => $perfil['pf_focus'] ?? '',
'languages' => $perfil['pf_languages'] ?? '',
'born' => $perfil['pf_born'] ?? '',
'id' => $perfil['pf_ci'] ?? '',
'web' => $perfil['pf_web'] ?? '',
],
'photo' => $photoUrl,
'metrics' => $metrics,
'highlights' => $highlights,
'summary' => [
'title' => $perfil['pf_summary_title'] ?? '',
'body' => $perfil['pf_summary_body'] ?? '',
],
'impact' => $impact,
'experience' => $experience,
'education' => $education,
'certifications' => $certifications,
'competencies' => $competencies,
'volunteer' => [
'title' => $extras['pf_volunteer_title'] ?? '',
'year' => $extras['pf_volunteer_year'] ?? '',
'body' => $extras['pf_volunteer_body'] ?? '',
],
'additional' => [
'Idiomas' => $perfil['pf_languages'] ?? '',
'Disponibilidad' => $perfil['pf_availability'] ?? '',
'C.I.' => $perfil['pf_ci'] ?? '',
'Fecha de nacimiento' => $perfil['pf_born'] ?? '',
'Web profesional' => $perfil['pf_web'] ?? '',
],
];
}
/* ─────────────────────────────────────────────
3. FALLBACK si CMB2 no está activo
────────────────────────────────────────────── */
function pflores_cv_fallback_data(): array
{
return [
'hero' => ['eyebrow' => 'Currículum ejecutivo', 'name' => 'Patricia Soledad Flores Sánchez', 'role' => 'Gerente / Jefa de Operaciones Financieras', 'role2' => 'Sector financiero y microfinanzas', 'intro' => 'Ejecutiva financiera con más de 22 años de trayectoria en banca y microfinanzas.', 'location' => 'Loja, Ecuador', 'email' => 'patricflorsanchez@gmail.com', 'phone' => '+593 99 469 8405', 'linkedin' => 'https://www.linkedin.com/in/patricia-soledad-flores-s%C3%A1nchez-43b84418/', 'availability' => 'Inmediata | Presencial / Híbrida / Remota', 'focus' => 'Operaciones financieras, control interno y transformación digital', 'languages' => 'Español nativo | Inglés básico en formación activa', 'born' => '3 de agosto de 1980', 'id' => '1103591648', 'web' => 'pflores.linkedcorp.net'],
'photo' => '',
'metrics' => [],
'highlights' => [],
'summary' => ['title' => '', 'body' => ''],
'impact' => [],
'experience' => [],
'education' => [],
'certifications' => [],
'competencies' => [],
'volunteer' => ['title' => '', 'year' => '', 'body' => ''],
'additional' => [],
];
}
@@ -0,0 +1,187 @@
<?php
/**
* Carga los datos iniciales del CV de Patricia Flores en CMB2.
* URL: /wp-content/plugins/pflores-cv-fields/populate-data.php?key=pflores2024
*/
if (($_GET['key'] ?? '') !== 'pflores2024') {
http_response_code(403); exit('Acceso denegado.');
}
require_once dirname(__DIR__, 3) . '/wp-load.php';
if (! function_exists('cmb2_get_option')) {
exit('CMB2 no está activo.');
}
$ok = [];
/* ════════════════════════════════════════════
PERFIL Y CONTACTO → option: pflores_perfil
════════════════════════════════════════════ */
update_option('pflores_perfil', [
'pf_name' => 'Patricia Soledad Flores Sánchez',
'pf_role' => 'Gerente / Jefa de Operaciones Financieras',
'pf_role2' => 'Sector financiero y microfinanzas',
'pf_intro' => 'Ejecutiva financiera con más de 22 años de trayectoria en banca y microfinanzas. Máster en Dirección y Planificación Financiera, reconocido por la SENECYT en Ecuador, con especialización en gestión operativa, transformación digital de procesos, control interno y liderazgo de equipos multidisciplinarios.',
'pf_summary_title' => 'Perfil estratégico con enfoque en eficiencia, control y ejecución sostenible',
'pf_summary_body' => 'Actualmente lidera la operatividad de 26 agencias a nivel nacional y coordina aproximadamente 1.500 operaciones de microcrédito mensuales, por un monto cercano a USD 5 millones. También articula la recaudación de cerca de 28.000 clientes por canales internos y externos, integrando visión operativa, criterio financiero, disciplina de control y liderazgo sostenido del desempeño.',
'pf_location' => 'Loja, Ecuador',
'pf_email' => 'patricflorsanchez@gmail.com',
'pf_phone' => '+593 99 469 8405',
'pf_linkedin' => 'https://www.linkedin.com/in/patricia-soledad-flores-s%C3%A1nchez-43b84418/',
'pf_availability' => 'Inmediata | Presencial / Híbrida / Remota',
'pf_focus' => 'Operaciones financieras, control interno y transformación digital',
'pf_languages' => 'Español nativo | Inglés básico en formación activa',
'pf_born' => '3 de agosto de 1980',
'pf_ci' => '1103591648',
'pf_web' => 'pflores.linkedcorp.net',
'pf_photo' => '',
]);
$ok[] = '✓ Perfil y contacto (17 campos)';
/* ════════════════════════════════════════════
EXPERIENCIA → option: pflores_experiencia
════════════════════════════════════════════ */
update_option('pflores_experiencia', [
'pf_experience' => [
[
'period' => 'Abr. 2022 – Presente',
'title' => 'Jefa de Operaciones',
'company' => 'Fundación FACES Microfinanzas',
'location' => 'Loja, Ecuador',
'lead' => 'Responsable del desempeño operativo nacional, cumplimiento del POA, control interno, digitalización de procesos y gestión de equipos.',
'bullets_text' =>
"Cumplimiento del POA: 98 % (Q2 2025), 96 % (Q3 2025), 100 % (Q4 2025) y 105 % (Q1 2026).\n" .
"La transformación digital logró que los desembolsos por transferencia representen el 47,80 % del monto total en Q1 2026.\n" .
"Redujo los requerimientos de soporte desde Dirección en un 74 %, de 1.489 a 382, mediante la descentralización de procesos hacia las agencias.\n" .
"Automatizó el proceso del seguro de desgravamen con cálculo de prima, conciliación mensual, liquidación de siniestros y asientos contables automatizados.\n" .
"Elevó el índice de cumplimiento operativo nacional del 65 % al 88 % con la matriz de supervisión y certificación por agencia.\n" .
"Redujo el tiempo de atención del seguro de desgravamen de 17 a 10 días.\n" .
"Redujo los tiempos de desembolso a 1,68 días en Q3 2025, mejor resultado del año frente a la meta institucional máxima de 2 días.\n" .
"La efectividad de recuperación de mora alcanzó 90,60 % en Q1 2026, con USD 3,06 millones recuperados sobre USD 3,37 millones en mora.\n" .
"Redujo el índice de rotación del área del 41,46 % al 10,24 % entre diciembre de 2022 y marzo de 2026 mediante mejoras salariales, remuneración variable, evaluación trimestral y un plan de carrera con certificación de 60 horas para asistentes de caja.\n" .
"Participa en comités estratégicos, auditorías internas y externas, y en representación institucional ante organismos financieros internacionales de desarrollo.",
],
[
'period' => 'Feb. 2021 – Mar. 2022',
'title' => 'Supervisora de Operaciones',
'company' => 'Fundación FACES Microfinanzas',
'location' => 'Loja, Ecuador',
'lead' => 'Supervisó operaciones de 18 agencias a nivel nacional y regularizó procesos críticos inexistentes o incompletos.',
'bullets_text' =>
"Reconstruyó desde cero las bases de cálculo del seguro de desgravamen y regularizó pagos pendientes desde enero de 2021.\n" .
"Elaboró el instructivo de constitución de garantías hipotecarias, proceso que no existía previamente en la institución.\n" .
"Inició el proyecto de automatización de pagos por depósitos y transferencias.",
],
[
'period' => '2019 – 2021',
'title' => 'Actualización y formación profesional',
'company' => 'Formación independiente',
'location' => 'Ecuador',
'lead' => 'Etapa enfocada en fortalecimiento técnico y actualización profesional tras su salida de Mutualista Pichincha.',
'bullets_text' => "SAP FI (30 h).\nExcel Empresarial (24 h).\nPrevención de Lavado de Activos — UAFE (5 h).",
],
[
'period' => 'Ago. 2004 – May. 2019',
'title' => 'Oficial Operativa Regional / Jefa Operativa',
'company' => 'Mutualista Pichincha',
'location' => 'Loja, Ecuador',
'lead' => 'Trayectoria ascendente de 15 años desde Cajera hasta Jefa Operativa Regional.',
'bullets_text' =>
"Reconocida a nivel nacional por excelencia en control operativo.\n" .
"La agencia se mantuvo consistentemente por encima del 80 % en gestión de excepciones de crédito hipotecario.\n" .
"Administró proyecto inmobiliario institucional en Loja.\n" .
"Gestionó alertas ALA/FT con 100 % de cumplimiento en comités trimestrales y auditorías internas sin observaciones significativas de forma sostenida.",
],
[
'period' => 'Ene. 2002 – Nov. 2003',
'title' => 'Cajera',
'company' => 'Cooperativa de Ahorro y Crédito El Porvenir',
'location' => 'Loja, Ecuador',
'lead' => 'Inicio de carrera en el frente operativo financiero con disciplina de caja y apoyo contable.',
'bullets_text' =>
"Gestión de caja sin faltantes ni sobrantes.\n" .
"Contrato renovado de forma anticipada a los tres meses.\n" .
"Apoyo al área de Contabilidad y reemplazo temporal de la contadora durante vacaciones.",
],
],
]);
$ok[] = '✓ Experiencia (5 cargos, 23 bullets)';
/* ════════════════════════════════════════════
FORMACIÓN Y CERTIFICACIONES → pflores_formacion
════════════════════════════════════════════ */
update_option('pflores_formacion', [
'pf_education' => [
['year' => '2021', 'title' => 'Máster Universitario en Dirección y Planificación Financiera', 'institution' => 'Universidad Europea Miguel de Cervantes — Valladolid, España', 'detail' => 'Título reconocido por la SENECYT — Ecuador, febrero de 2023 | Reg. 2021/394270.'],
['year' => '2007', 'title' => 'Ingeniera en Administración en Banca y Finanzas', 'institution' => 'Universidad Técnica Particular de Loja (UTPL)', 'detail' => ''],
],
'pf_certifications' => [
['year' => '2026', 'title' => 'Mujeres que Lideran: Alta Gerencia', 'issuer' => 'Foro UNIR — Universidad Internacional de La Rioja'],
['year' => '2025', 'title' => 'Programa Internacional de Habilidades Gerenciales (48 h)', 'issuer' => 'IDEASGROUP — Ecuador / México'],
['year' => '2025', 'title' => 'Transforma tu Trabajo con Inteligencia Artificial (12 h)', 'issuer' => 'Red de Instituciones Financieras de Desarrollo'],
['year' => '2025', 'title' => 'Excel: Básico a Dashboard (40 h)', 'issuer' => 'DMARURI Training & Consulting'],
['year' => '2024', 'title' => 'Administración por Procesos — BPM (64 h)', 'issuer' => 'Escuela Politécnica Nacional — CEC'],
['year' => '2023', 'title' => 'Formación de Formadores (80 h)', 'issuer' => 'Ministerio del Trabajo del Ecuador | Certificación vigente hasta 2028'],
['year' => '2023', 'title' => 'Power BI (40 h) | Indicadores Clave de Desempeño — KPI (12 h)', 'issuer' => 'Liderazgo Capacitación & Eventos'],
['year' => '2020', 'title' => 'Gestión Financiera ERP SAP FI — Key User (30 h)', 'issuer' => 'Socio SAP certificado'],
['year' => '2020', 'title' => 'Prevención de Lavado de Activos y Financiamiento de Delitos', 'issuer' => 'UAFE — Ecuador'],
['year' => '2013', 'title' => 'Tributación Integral (36 h)', 'issuer' => 'Universidad Técnica Particular de Loja'],
],
]);
$ok[] = '✓ Formación (2 títulos) + Certificaciones (10)';
/* ════════════════════════════════════════════
COMPETENCIAS → pflores_competencias
════════════════════════════════════════════ */
update_option('pflores_competencias', [
'pf_competencies' => [
['group_name' => 'Finanzas y Operaciones', 'skills_text' => "Gestión de operaciones financieras|95\nControl interno y cumplimiento|95\nGestión de cartera y microfinanzas|90\nConciliación bancaria / Tesorería|90\nALA/FT y prevención de riesgos|88"],
['group_name' => 'Tecnología y Procesos', 'skills_text' => "Transformación digital / BPM|85\nSAP FI|78\nExcel avanzado y dashboards|85\nPower BI|75\nIA aplicada al trabajo|70"],
['group_name' => 'Liderazgo', 'skills_text' => "Equipos de hasta 48 personas|95\nGestión del cambio organizacional|90\nFormación de formadores|88\nRemuneración variable y plan de carrera|85\nDiversidad, equidad e inclusión|80"],
],
]);
$ok[] = '✓ Competencias (3 grupos, 15 habilidades)';
/* ════════════════════════════════════════════
MÉTRICAS, IMPACTO Y EXTRAS → pflores_extras
════════════════════════════════════════════ */
update_option('pflores_extras', [
'pf_metrics' => [
['value' => '22+', 'suffix' => '', 'label' => 'años de experiencia en banca y microfinanzas'],
['value' => '26', 'suffix' => '', 'label' => 'agencias bajo liderazgo operativo nacional'],
['value' => '48', 'suffix' => '', 'label' => 'personas a cargo en la estructura operativa'],
['value' => 'USD 5 M','suffix' => '', 'label' => 'en operaciones mensuales de microcrédito'],
['value' => '28.000', 'suffix' => '', 'label' => 'clientes coordinados en recaudación por canales internos y externos'],
],
'pf_highlights' => [
['value' => '9/10', 'suffix' => '', 'label' => 'calificación de gestión con resultado excelente en cuatro trimestres consecutivos'],
['value' => '74 %', 'suffix' => '', 'label' => 'reducción de requerimientos de soporte desde Dirección por descentralización de procesos'],
['value' => '1,68 días','suffix' => '', 'label' => 'tiempo de desembolso logrado en Q3 2025, mejor resultado del año'],
['value' => '75 %', 'suffix' => '', 'label' => 'reducción del índice de rotación del área entre 2022 y 2026'],
],
'pf_impact' => [
['title' => 'Transformación digital', 'body' => 'Lideró desembolsos por transferencia bancaria, integración de APIs bancarias y digitalización de expedientes y pagarés en 13 agencias.'],
['title' => 'Automatización crítica', 'body' => 'Automatizó el seguro de desgravamen, desde el cálculo de prima hasta la liquidación contable, reduciendo tiempos de 17 a 10 días.'],
['title' => 'Supervisión nacional', 'body' => 'Implementó una matriz de supervisión y certificación por agencia, elevando el índice de cumplimiento operativo del 65 % al 88 %.'],
],
'pf_volunteer_title' => 'Voluntaria — Hogar San Camilo de Lellis',
'pf_volunteer_year' => '2025',
'pf_volunteer_body' => 'Hogar de niñas con discapacidad | Loja, Ecuador. Alineado a la visión y misión de responsabilidad social de la Fundación FACES Microfinanzas.',
]);
$ok[] = '✓ Métricas (5) + Highlights (4) + Impacto (3) + Voluntariado';
?>
<!doctype html><html><head><meta charset="utf-8"><title>Datos CV cargados</title>
<style>body{font-family:monospace;background:#0f1117;color:#e0e3ef;padding:2rem;line-height:1.8}h1{color:#22d3ee}li{color:#22c55e}.links a{color:#22d3ee;display:block;margin:.3rem 0}.warn{color:#f59e0b;margin-top:1.5rem}</style>
</head><body>
<h1>✅ CV de Patricia Flores — datos cargados con CMB2</h1>
<ul><?php foreach ($ok as $r) echo "<li>$r</li>"; ?></ul>
<div class="links" style="margin-top:1.5rem">
<a href="/wp-admin">→ Ir al admin de WordPress</a>
<a href="/wp-admin/admin.php?page=pflores_perfil">→ CV Patricia Flores → Perfil</a>
<a href="/">→ Ver el CV en el sitio</a>
</div>
<p class="warn">⚠ Elimina este archivo una vez verificado.</p>
</body></html>