Plugins bloques
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
/**
|
||||
* Plugin Name: Importar Actuaciones desde CSV
|
||||
* Description: Importador idempotente de Actuaciones (Bloques en Transición) desde CSV con mapeo interactivo de categorías.
|
||||
* Version: 1.1.0
|
||||
* Author: Freepress Coop
|
||||
* License: GPL-2.0+
|
||||
* Text Domain: bloques-actuaciones-import
|
||||
* Requires Plugins: bloques-transicion
|
||||
* Requires at least: 6.0
|
||||
* Requires PHP: 8.0
|
||||
*/
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
define('BLOQUES_IMPORT_VERSION', '1.1.0');
|
||||
define('BLOQUES_IMPORT_DIR', plugin_dir_path(__FILE__));
|
||||
define('BLOQUES_IMPORT_URL', plugin_dir_url(__FILE__));
|
||||
|
||||
require_once BLOQUES_IMPORT_DIR . 'includes/class-bloques-actuaciones-importer.php';
|
||||
|
||||
add_action('admin_menu', 'bloques_import_register_menu', 20);
|
||||
|
||||
function bloques_import_register_menu() {
|
||||
if (!defined('BLOQUES_PLUGIN_DIR')) {
|
||||
return;
|
||||
}
|
||||
add_submenu_page(
|
||||
'bloques-dashboard',
|
||||
__('Importar Actuaciones CSV', 'bloques-actuaciones-import'),
|
||||
__('📥 Importar CSV', 'bloques-actuaciones-import'),
|
||||
'manage_options',
|
||||
'bloques-import-actuaciones',
|
||||
'bloques_import_render_page'
|
||||
);
|
||||
}
|
||||
|
||||
function bloques_import_render_page() {
|
||||
$step = 'upload'; // upload | mapping | done
|
||||
$message = '';
|
||||
$message_type = '';
|
||||
$import_errors = [];
|
||||
$analysis = null;
|
||||
|
||||
// ── PASO 2: el usuario ha confirmado mapeos → importar ──
|
||||
if (
|
||||
isset($_POST['bloques_import_step']) &&
|
||||
$_POST['bloques_import_step'] === 'import' &&
|
||||
wp_verify_nonce($_POST['bloques_import_nonce'] ?? '', 'bloques_import_actuaciones')
|
||||
) {
|
||||
$csv_path = sanitize_text_field($_POST['csv_path'] ?? '');
|
||||
$geocode = !empty($_POST['reverse_geocode']);
|
||||
$ini_map = array_map('sanitize_text_field', (array) ($_POST['ini_map'] ?? []));
|
||||
$linea_map = array_map('sanitize_text_field', (array) ($_POST['linea_map'] ?? []));
|
||||
|
||||
$result = Bloques_Actuaciones_Importer::import($csv_path, $ini_map, $linea_map, $geocode);
|
||||
$message = $result['message'];
|
||||
$message_type = $result['success'] ? 'success' : 'error';
|
||||
$import_errors = $result['errors'] ?? [];
|
||||
$step = 'done';
|
||||
}
|
||||
|
||||
// ── PASO 1: subir CSV → analizar y mostrar mapeos ──
|
||||
elseif (
|
||||
isset($_POST['bloques_import_step']) &&
|
||||
$_POST['bloques_import_step'] === 'analyze' &&
|
||||
wp_verify_nonce($_POST['bloques_import_nonce'] ?? '', 'bloques_import_actuaciones')
|
||||
) {
|
||||
$analysis = Bloques_Actuaciones_Importer::analyze($_FILES['csv_file'] ?? null);
|
||||
if (!$analysis['success']) {
|
||||
$message = $analysis['message'];
|
||||
$message_type = 'error';
|
||||
$step = 'upload';
|
||||
} else {
|
||||
$step = 'mapping';
|
||||
}
|
||||
}
|
||||
|
||||
include BLOQUES_IMPORT_DIR . 'templates/admin/import-page.php';
|
||||
}
|
||||
@@ -0,0 +1,457 @@
|
||||
<?php
|
||||
/**
|
||||
* Importador de Actuaciones desde CSV — flujo en dos pasos.
|
||||
*
|
||||
* Paso 1 (analyze): lee el CSV, extrae valores únicos de Iniciativa y Línea de trabajo,
|
||||
* los compara con los términos existentes y devuelve los que necesitan mapeo.
|
||||
* Paso 2 (import): recibe el CSV guardado + los mapeos confirmados por el usuario
|
||||
* e importa/actualiza las actuaciones de forma idempotente.
|
||||
*
|
||||
* @package Bloques_Actuaciones_Import
|
||||
*/
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class Bloques_Actuaciones_Importer {
|
||||
|
||||
private const CSV_DELIMITER = ';';
|
||||
private const NOMINATIM_DELAY_US = 1100000; // 1.1 s
|
||||
private const NOMINATIM_USER_AGENT = 'BloquesTransicion/1.0 (WordPress; tangente.coop)';
|
||||
|
||||
/**
|
||||
* Mapeos por defecto del CSV a los slugs de WP.
|
||||
* clave = nombre tal como aparece en el CSV (mb_strtolower).
|
||||
*/
|
||||
private const DEFAULT_INICIATIVA_MAP = [
|
||||
'bloques' => 'oficina-transicion-justa',
|
||||
'colegios' => 'coles-transicion',
|
||||
];
|
||||
|
||||
/* ─────────────────────────────────────────────
|
||||
* PASO 1 — Analizar CSV
|
||||
* ───────────────────────────────────────────── */
|
||||
|
||||
/**
|
||||
* Analizar el CSV y devolver términos que necesitan mapeo.
|
||||
*
|
||||
* @param array $file $_FILES['csv_file']
|
||||
* @return array{ success:bool, message:string, csv_path:string,
|
||||
* unmatched_iniciativas:array, unmatched_lineas:array,
|
||||
* existing_iniciativas:array, existing_lineas:array,
|
||||
* stats:array }
|
||||
*/
|
||||
public static function analyze($file) {
|
||||
if (empty($file['tmp_name']) || !is_uploaded_file($file['tmp_name'])) {
|
||||
return ['success' => false, 'message' => __('No se ha subido ningún archivo válido.', 'bloques-actuaciones-import')];
|
||||
}
|
||||
|
||||
// Guardar el CSV temporalmente en uploads para el paso 2
|
||||
$upload_dir = wp_upload_dir();
|
||||
$tmp_path = trailingslashit($upload_dir['basedir']) . 'bloques-import-' . wp_generate_password(12, false) . '.csv';
|
||||
if (!move_uploaded_file($file['tmp_name'], $tmp_path)) {
|
||||
return ['success' => false, 'message' => __('No se pudo guardar el archivo temporal.', 'bloques-actuaciones-import')];
|
||||
}
|
||||
|
||||
$errors = [];
|
||||
$rows = self::parse_csv($tmp_path, $errors);
|
||||
if ($rows === null) {
|
||||
@unlink($tmp_path);
|
||||
return ['success' => false, 'message' => __('Error al leer el CSV.', 'bloques-actuaciones-import') . ' ' . implode(' ', $errors)];
|
||||
}
|
||||
|
||||
// Recoger valores únicos del CSV
|
||||
$csv_iniciativas = [];
|
||||
$csv_lineas = [];
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$ini = trim(self::val($row, ['Iniciativa']));
|
||||
if ($ini !== '') {
|
||||
$csv_iniciativas[$ini] = true;
|
||||
}
|
||||
|
||||
$lin = trim(self::val($row, ['Línea de trabajo', 'Líneas de trabajo', 'Linea de trabajo']));
|
||||
if ($lin !== '') {
|
||||
foreach (preg_split('/\s*;\s*/', $lin) as $l) {
|
||||
$l = trim($l);
|
||||
if ($l !== '') {
|
||||
$csv_lineas[$l] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$csv_iniciativas = array_keys($csv_iniciativas);
|
||||
$csv_lineas = array_keys($csv_lineas);
|
||||
|
||||
// Términos existentes en WP
|
||||
$existing_iniciativas = self::get_existing_terms('iniciativa');
|
||||
$existing_lineas = self::get_existing_terms('linea_trabajo');
|
||||
|
||||
// Determinar cuáles necesitan mapeo
|
||||
$unmatched_iniciativas = self::find_unmatched($csv_iniciativas, $existing_iniciativas, self::DEFAULT_INICIATIVA_MAP);
|
||||
$unmatched_lineas = self::find_unmatched($csv_lineas, $existing_lineas, []);
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'csv_path' => $tmp_path,
|
||||
'total_rows' => count($rows),
|
||||
'unmatched_iniciativas' => $unmatched_iniciativas,
|
||||
'unmatched_lineas' => $unmatched_lineas,
|
||||
'existing_iniciativas' => $existing_iniciativas,
|
||||
'existing_lineas' => $existing_lineas,
|
||||
'default_map' => self::DEFAULT_INICIATIVA_MAP,
|
||||
];
|
||||
}
|
||||
|
||||
/* ─────────────────────────────────────────────
|
||||
* PASO 2 — Importar con mapeos confirmados
|
||||
* ───────────────────────────────────────────── */
|
||||
|
||||
/**
|
||||
* @param string $csv_path Ruta al CSV almacenado en paso 1
|
||||
* @param array $iniciativa_map csv_value => slug_existente | '__new__'
|
||||
* @param array $linea_map csv_value => slug_existente | '__new__'
|
||||
* @param bool $do_reverse_geocode
|
||||
* @return array{ success:bool, message:string, created:int, updated:int, errors:string[] }
|
||||
*/
|
||||
public static function import($csv_path, array $iniciativa_map, array $linea_map, $do_reverse_geocode = true) {
|
||||
$created = 0;
|
||||
$updated = 0;
|
||||
$errors = [];
|
||||
|
||||
if (!file_exists($csv_path) || !is_readable($csv_path)) {
|
||||
return self::result(false, __('No se encontró el archivo CSV temporal.', 'bloques-actuaciones-import'), 0, 0, []);
|
||||
}
|
||||
|
||||
$rows = self::parse_csv($csv_path, $errors);
|
||||
if ($rows === null) {
|
||||
return self::result(false, __('Error al leer el CSV.', 'bloques-actuaciones-import'), 0, 0, $errors);
|
||||
}
|
||||
|
||||
$total = count($rows);
|
||||
|
||||
foreach ($rows as $idx => $row) {
|
||||
$title = self::val($row, ['Título/Nombre', 'Título', 'Nombre']);
|
||||
if (trim((string) $title) === '') {
|
||||
$errors[] = sprintf(__('Fila %d: título vacío, omitida.', 'bloques-actuaciones-import'), $idx + 2);
|
||||
continue;
|
||||
}
|
||||
|
||||
$result = self::get_or_create($title);
|
||||
if (!$result) {
|
||||
$errors[] = sprintf(__('Fila %d: no se pudo crear/obtener "%s".', 'bloques-actuaciones-import'), $idx + 2, esc_html($title));
|
||||
continue;
|
||||
}
|
||||
|
||||
$post_id = $result['id'];
|
||||
$result['created'] ? $created++ : $updated++;
|
||||
|
||||
$desc = self::val($row, ['Descripción', 'Descripcion']);
|
||||
|
||||
wp_update_post([
|
||||
'ID' => $post_id,
|
||||
'post_title' => $title,
|
||||
'post_content' => $desc,
|
||||
'post_excerpt' => wp_trim_words(wp_strip_all_tags($desc), 30),
|
||||
'post_status' => 'publish',
|
||||
'post_type' => 'actuacion',
|
||||
]);
|
||||
|
||||
// Iniciativa
|
||||
$csv_ini = trim(self::val($row, ['Iniciativa']));
|
||||
if ($csv_ini !== '') {
|
||||
self::assign_term($post_id, 'iniciativa', $csv_ini, $iniciativa_map);
|
||||
}
|
||||
|
||||
// Líneas de trabajo
|
||||
$csv_lin = trim(self::val($row, ['Línea de trabajo', 'Líneas de trabajo', 'Linea de trabajo']));
|
||||
if ($csv_lin !== '') {
|
||||
$lineas_list = array_filter(array_map('trim', preg_split('/\s*;\s*/', $csv_lin)));
|
||||
$term_ids = [];
|
||||
foreach ($lineas_list as $l) {
|
||||
$tid = self::resolve_term('linea_trabajo', $l, $linea_map);
|
||||
if ($tid) {
|
||||
$term_ids[] = $tid;
|
||||
}
|
||||
}
|
||||
if (!empty($term_ids)) {
|
||||
wp_set_object_terms($post_id, $term_ids, 'linea_trabajo');
|
||||
}
|
||||
}
|
||||
|
||||
// Coordenadas
|
||||
$lat_long = self::val($row, ['Lat/Long', 'Lat', 'Long']);
|
||||
self::set_coords($post_id, $lat_long);
|
||||
|
||||
// Reverse geocode → dirección
|
||||
if ($do_reverse_geocode && $lat_long !== '') {
|
||||
$address = self::reverse_geocode($lat_long);
|
||||
if ($address !== '') {
|
||||
update_post_meta($post_id, 'direccion', $address);
|
||||
if (function_exists('update_field')) {
|
||||
update_field('direccion', $address, $post_id);
|
||||
}
|
||||
}
|
||||
if ($total > 1) {
|
||||
usleep(self::NOMINATIM_DELAY_US);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Limpiar archivo temporal
|
||||
@unlink($csv_path);
|
||||
|
||||
$msg = sprintf(
|
||||
__('Importación completada: %d creadas, %d actualizadas de %d filas.', 'bloques-actuaciones-import'),
|
||||
$created, $updated, $total
|
||||
);
|
||||
if (!empty($errors)) {
|
||||
$msg .= ' ' . sprintf(__('%d advertencias.', 'bloques-actuaciones-import'), count($errors));
|
||||
}
|
||||
|
||||
return self::result(true, $msg, $created, $updated, $errors);
|
||||
}
|
||||
|
||||
/* ─────────────────────────────────────────────
|
||||
* Helpers privados
|
||||
* ───────────────────────────────────────────── */
|
||||
|
||||
private static function result($ok, $msg, $c, $u, $e) {
|
||||
return ['success' => $ok, 'message' => $msg, 'created' => $c, 'updated' => $u, 'errors' => $e];
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtener términos existentes de una taxonomía como [ slug => name ].
|
||||
*/
|
||||
private static function get_existing_terms($taxonomy) {
|
||||
$terms = get_terms(['taxonomy' => $taxonomy, 'hide_empty' => false]);
|
||||
$result = [];
|
||||
if (!is_wp_error($terms)) {
|
||||
foreach ($terms as $t) {
|
||||
$result[$t->slug] = $t->name;
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determinar qué valores del CSV no coinciden con términos existentes.
|
||||
* Devuelve array de [ csv_value => suggested_slug|'' ].
|
||||
*/
|
||||
private static function find_unmatched(array $csv_values, array $existing, array $default_map) {
|
||||
$unmatched = [];
|
||||
$existing_lower = [];
|
||||
foreach ($existing as $slug => $name) {
|
||||
$existing_lower[mb_strtolower($name)] = $slug;
|
||||
}
|
||||
|
||||
foreach ($csv_values as $csv_val) {
|
||||
$key = mb_strtolower(trim($csv_val));
|
||||
|
||||
// Coincidencia exacta por nombre
|
||||
if (isset($existing_lower[$key])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Coincidencia exacta por slug
|
||||
$slug_candidate = sanitize_title($csv_val);
|
||||
if (isset($existing[$slug_candidate])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Mapeo por defecto
|
||||
if (isset($default_map[$key]) && isset($existing[$default_map[$key]])) {
|
||||
// Hay sugerencia pero se mostrará al usuario para que confirme
|
||||
$unmatched[$csv_val] = $default_map[$key];
|
||||
continue;
|
||||
}
|
||||
|
||||
// Sin coincidencia
|
||||
$unmatched[$csv_val] = '';
|
||||
}
|
||||
|
||||
return $unmatched;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asignar un término de taxonomía a un post, usando el mapa de mapeos.
|
||||
*/
|
||||
private static function assign_term($post_id, $taxonomy, $csv_value, array $map) {
|
||||
$tid = self::resolve_term($taxonomy, $csv_value, $map);
|
||||
if ($tid) {
|
||||
wp_set_object_terms($post_id, [$tid], $taxonomy);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolver un valor CSV a un term_id, creando si procede.
|
||||
*/
|
||||
private static function resolve_term($taxonomy, $csv_value, array $map) {
|
||||
$key = trim($csv_value);
|
||||
if ($key === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// ¿Hay mapeo del usuario?
|
||||
if (isset($map[$key])) {
|
||||
$mapped = $map[$key];
|
||||
if ($mapped === '__new__') {
|
||||
// Crear nuevo término
|
||||
return self::ensure_term($taxonomy, $key);
|
||||
}
|
||||
// Usar slug existente
|
||||
$term = get_term_by('slug', $mapped, $taxonomy);
|
||||
return $term ? (int) $term->term_id : self::ensure_term($taxonomy, $key);
|
||||
}
|
||||
|
||||
// Intentar match directo por nombre (case-insensitive)
|
||||
$existing = self::get_existing_terms($taxonomy);
|
||||
foreach ($existing as $slug => $name) {
|
||||
if (mb_strtolower($name) === mb_strtolower($key)) {
|
||||
return (int) get_term_by('slug', $slug, $taxonomy)->term_id;
|
||||
}
|
||||
}
|
||||
|
||||
// Intentar por slug
|
||||
$slug_try = sanitize_title($key);
|
||||
$term = get_term_by('slug', $slug_try, $taxonomy);
|
||||
if ($term) {
|
||||
return (int) $term->term_id;
|
||||
}
|
||||
|
||||
// Sin mapeo explícito → crear
|
||||
return self::ensure_term($taxonomy, $key);
|
||||
}
|
||||
|
||||
private static function ensure_term($taxonomy, $name) {
|
||||
$slug = sanitize_title($name);
|
||||
$term = get_term_by('slug', $slug, $taxonomy);
|
||||
if ($term) {
|
||||
return (int) $term->term_id;
|
||||
}
|
||||
$r = wp_insert_term($name, $taxonomy, ['slug' => $slug]);
|
||||
return is_wp_error($r) ? null : (int) $r['term_id'];
|
||||
}
|
||||
|
||||
/* ── Post idempotente ── */
|
||||
|
||||
private static function get_or_create($title) {
|
||||
global $wpdb;
|
||||
$id = $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT ID FROM {$wpdb->posts} WHERE post_type = 'actuacion' AND post_title = %s AND post_status IN ('publish','draft','pending','private') LIMIT 1",
|
||||
$title
|
||||
));
|
||||
if ($id) {
|
||||
return ['id' => (int) $id, 'created' => false];
|
||||
}
|
||||
$id = wp_insert_post(['post_title' => $title, 'post_type' => 'actuacion', 'post_status' => 'publish']);
|
||||
if (!$id || is_wp_error($id)) {
|
||||
return false;
|
||||
}
|
||||
return ['id' => (int) $id, 'created' => true];
|
||||
}
|
||||
|
||||
/* ── Coordenadas ── */
|
||||
|
||||
private static function set_coords($post_id, $lat_long) {
|
||||
if (!preg_match('/^(-?\d+\.?\d*)\s*,\s*(-?\d+\.?\d*)$/', trim($lat_long), $m)) {
|
||||
return;
|
||||
}
|
||||
$lat = (float) $m[1];
|
||||
$lng = (float) $m[2];
|
||||
update_post_meta($post_id, 'latitud', $lat);
|
||||
update_post_meta($post_id, 'longitud', $lng);
|
||||
if (function_exists('update_field')) {
|
||||
update_field('latitud', $lat, $post_id);
|
||||
update_field('longitud', $lng, $post_id);
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Reverse geocode (Nominatim) ── */
|
||||
|
||||
private static function reverse_geocode($lat_long) {
|
||||
if (!preg_match('/^(-?\d+\.?\d*)\s*,\s*(-?\d+\.?\d*)$/', trim($lat_long), $m)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$url = add_query_arg([
|
||||
'lat' => (float) $m[1],
|
||||
'lon' => (float) $m[2],
|
||||
'format' => 'json',
|
||||
'addressdetails' => 1,
|
||||
], 'https://nominatim.openstreetmap.org/reverse');
|
||||
|
||||
$resp = wp_remote_get($url, [
|
||||
'timeout' => 10,
|
||||
'user-agent' => self::NOMINATIM_USER_AGENT,
|
||||
'headers' => ['Accept-Language' => 'es'],
|
||||
]);
|
||||
|
||||
if (is_wp_error($resp) || wp_remote_retrieve_response_code($resp) !== 200) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$data = json_decode(wp_remote_retrieve_body($resp), true);
|
||||
if (empty($data['address'])) {
|
||||
return $data['display_name'] ?? '';
|
||||
}
|
||||
|
||||
$a = $data['address'];
|
||||
$parts = array_filter([
|
||||
$a['road'] ?? '',
|
||||
$a['house_number'] ?? '',
|
||||
$a['suburb'] ?? $a['neighbourhood'] ?? '',
|
||||
$a['city'] ?? $a['town'] ?? $a['village'] ?? $a['municipality'] ?? '',
|
||||
$a['state'] ?? '',
|
||||
]);
|
||||
return implode(', ', $parts);
|
||||
}
|
||||
|
||||
/* ── CSV parser ── */
|
||||
|
||||
private static function parse_csv($path, &$errors = []) {
|
||||
$handle = fopen($path, 'r');
|
||||
if (!$handle) {
|
||||
$errors[] = __('No se pudo abrir el archivo.', 'bloques-actuaciones-import');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Saltar BOM
|
||||
$bom = fread($handle, 3);
|
||||
if ($bom !== "\xEF\xBB\xBF") {
|
||||
rewind($handle);
|
||||
}
|
||||
|
||||
$header = fgetcsv($handle, 0, self::CSV_DELIMITER);
|
||||
if ($header === false || empty($header)) {
|
||||
fclose($handle);
|
||||
$errors[] = __('Cabecera CSV no válida.', 'bloques-actuaciones-import');
|
||||
return null;
|
||||
}
|
||||
|
||||
$header = array_map('trim', $header);
|
||||
$rows = [];
|
||||
|
||||
while (($raw = fgetcsv($handle, 0, self::CSV_DELIMITER)) !== false) {
|
||||
$padded = array_pad($raw, count($header), '');
|
||||
$row = array_combine($header, array_slice($padded, 0, count($header)));
|
||||
if (is_array($row)) {
|
||||
$rows[] = array_map(fn($v) => is_string($v) ? trim($v) : $v, $row);
|
||||
}
|
||||
}
|
||||
|
||||
fclose($handle);
|
||||
return $rows;
|
||||
}
|
||||
|
||||
private static function val(array $row, array $keys) {
|
||||
foreach ($keys as $k) {
|
||||
if (isset($row[$k]) && $row[$k] !== '') {
|
||||
return $row[$k];
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
<?php
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
$step = $step ?? 'upload';
|
||||
$message = $message ?? '';
|
||||
$message_type = $message_type ?? '';
|
||||
$import_errors = $import_errors ?? [];
|
||||
$analysis = $analysis ?? null;
|
||||
?>
|
||||
<div class="wrap">
|
||||
<h1><?php esc_html_e('Importar Actuaciones desde CSV', 'bloques-actuaciones-import'); ?></h1>
|
||||
|
||||
<?php if ($message) : ?>
|
||||
<div class="notice notice-<?php echo esc_attr($message_type === 'success' ? 'success' : 'error'); ?> is-dismissible">
|
||||
<p><?php echo esc_html($message); ?></p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($import_errors)) : ?>
|
||||
<div class="notice notice-warning">
|
||||
<p><strong><?php esc_html_e('Detalle:', 'bloques-actuaciones-import'); ?></strong></p>
|
||||
<ul style="margin-left:1.5em;list-style:disc;">
|
||||
<?php foreach (array_slice($import_errors, 0, 30) as $err) : ?>
|
||||
<li><?php echo esc_html($err); ?></li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<?php if (count($import_errors) > 30) : ?>
|
||||
<p><?php echo esc_html(sprintf(__('… y %d más.', 'bloques-actuaciones-import'), count($import_errors) - 30)); ?></p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php /* ═══════════════════════════════════════════
|
||||
* PASO 1 — Subir CSV
|
||||
* ═══════════════════════════════════════════ */ ?>
|
||||
<?php if ($step === 'upload' || $step === 'done') : ?>
|
||||
<div class="card" style="max-width:660px;padding:20px;">
|
||||
<h2 style="margin-top:0;"><?php esc_html_e('1. Subir archivo CSV', 'bloques-actuaciones-import'); ?></h2>
|
||||
<p><?php esc_html_e('CSV con delimitador punto y coma (;). Columnas: Título/Nombre, Descripción, Lat/Long, Iniciativa, Línea de trabajo.', 'bloques-actuaciones-import'); ?></p>
|
||||
<p><?php esc_html_e('La importación es idempotente: si la actuación ya existe (mismo título), se actualiza.', 'bloques-actuaciones-import'); ?></p>
|
||||
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
<?php wp_nonce_field('bloques_import_actuaciones', 'bloques_import_nonce'); ?>
|
||||
<input type="hidden" name="bloques_import_step" value="analyze">
|
||||
|
||||
<p>
|
||||
<label for="csv_file"><strong><?php esc_html_e('Archivo CSV', 'bloques-actuaciones-import'); ?></strong></label><br>
|
||||
<input type="file" name="csv_file" id="csv_file" accept=".csv,.txt" required>
|
||||
</p>
|
||||
<p>
|
||||
<label>
|
||||
<input type="checkbox" name="reverse_geocode" value="1" checked>
|
||||
<?php esc_html_e('Obtener dirección desde coordenadas (Nominatim/OpenStreetMap, ~1 petición/s).', 'bloques-actuaciones-import'); ?>
|
||||
</label>
|
||||
</p>
|
||||
<p>
|
||||
<button type="submit" class="button button-primary"><?php esc_html_e('Analizar CSV', 'bloques-actuaciones-import'); ?></button>
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php /* ═══════════════════════════════════════════
|
||||
* PASO 2 — Mapeo de categorías
|
||||
* ═══════════════════════════════════════════ */ ?>
|
||||
<?php if ($step === 'mapping' && $analysis) :
|
||||
$unmatched_ini = $analysis['unmatched_iniciativas'];
|
||||
$unmatched_lin = $analysis['unmatched_lineas'];
|
||||
$existing_ini = $analysis['existing_iniciativas'];
|
||||
$existing_lin = $analysis['existing_lineas'];
|
||||
$default_map = $analysis['default_map'];
|
||||
$has_unmatched = !empty($unmatched_ini) || !empty($unmatched_lin);
|
||||
?>
|
||||
<div class="card" style="max-width:780px;padding:20px;">
|
||||
<h2 style="margin-top:0;">
|
||||
<?php esc_html_e('2. Revisar mapeo de categorías', 'bloques-actuaciones-import'); ?>
|
||||
</h2>
|
||||
<p>
|
||||
<?php echo esc_html(sprintf(
|
||||
__('Se han encontrado %d filas en el CSV.', 'bloques-actuaciones-import'),
|
||||
$analysis['total_rows']
|
||||
)); ?>
|
||||
</p>
|
||||
|
||||
<form method="post">
|
||||
<?php wp_nonce_field('bloques_import_actuaciones', 'bloques_import_nonce'); ?>
|
||||
<input type="hidden" name="bloques_import_step" value="import">
|
||||
<input type="hidden" name="csv_path" value="<?php echo esc_attr($analysis['csv_path']); ?>">
|
||||
<input type="hidden" name="reverse_geocode" value="<?php echo !empty($_POST['reverse_geocode']) ? '1' : '0'; ?>">
|
||||
|
||||
<?php if (!$has_unmatched) : ?>
|
||||
<div class="notice notice-success inline" style="margin:12px 0;">
|
||||
<p><?php esc_html_e('Todas las categorías del CSV coinciden con términos existentes en WordPress. Puedes importar directamente.', 'bloques-actuaciones-import'); ?></p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php /* ── Iniciativas ── */ ?>
|
||||
<?php if (!empty($unmatched_ini)) : ?>
|
||||
<h3><?php esc_html_e('Iniciativas', 'bloques-actuaciones-import'); ?></h3>
|
||||
<p class="description"><?php esc_html_e('Estos valores de la columna "Iniciativa" no coinciden exactamente con ningún término existente. Elige a qué iniciativa asignarlos o crea uno nuevo.', 'bloques-actuaciones-import'); ?></p>
|
||||
<table class="widefat striped" style="max-width:720px;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?php esc_html_e('Valor en CSV', 'bloques-actuaciones-import'); ?></th>
|
||||
<th><?php esc_html_e('Asignar a…', 'bloques-actuaciones-import'); ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($unmatched_ini as $csv_val => $suggested_slug) : ?>
|
||||
<tr>
|
||||
<td><strong><?php echo esc_html($csv_val); ?></strong></td>
|
||||
<td>
|
||||
<select name="ini_map[<?php echo esc_attr($csv_val); ?>]" style="width:100%;">
|
||||
<option value="__new__"><?php echo esc_html(sprintf(__('Crear nueva: "%s"', 'bloques-actuaciones-import'), $csv_val)); ?></option>
|
||||
<?php foreach ($existing_ini as $slug => $name) : ?>
|
||||
<option value="<?php echo esc_attr($slug); ?>"<?php selected($suggested_slug, $slug); ?>>
|
||||
<?php echo esc_html($name); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php /* ── Líneas de trabajo ── */ ?>
|
||||
<?php if (!empty($unmatched_lin)) : ?>
|
||||
<h3 style="margin-top:24px;"><?php esc_html_e('Líneas de trabajo', 'bloques-actuaciones-import'); ?></h3>
|
||||
<p class="description"><?php esc_html_e('Estos valores de "Línea de trabajo" no coinciden con ningún término existente.', 'bloques-actuaciones-import'); ?></p>
|
||||
<table class="widefat striped" style="max-width:720px;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?php esc_html_e('Valor en CSV', 'bloques-actuaciones-import'); ?></th>
|
||||
<th><?php esc_html_e('Asignar a…', 'bloques-actuaciones-import'); ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($unmatched_lin as $csv_val => $suggested_slug) : ?>
|
||||
<tr>
|
||||
<td><strong><?php echo esc_html($csv_val); ?></strong></td>
|
||||
<td>
|
||||
<select name="linea_map[<?php echo esc_attr($csv_val); ?>]" style="width:100%;">
|
||||
<option value="__new__"><?php echo esc_html(sprintf(__('Crear nueva: "%s"', 'bloques-actuaciones-import'), $csv_val)); ?></option>
|
||||
<?php foreach ($existing_lin as $slug => $name) : ?>
|
||||
<option value="<?php echo esc_attr($slug); ?>"<?php selected($suggested_slug, $slug); ?>>
|
||||
<?php echo esc_html($name); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php endif; ?>
|
||||
|
||||
<p style="margin-top:20px;">
|
||||
<button type="submit" class="button button-primary button-hero">
|
||||
<?php esc_html_e('Confirmar e importar', 'bloques-actuaciones-import'); ?>
|
||||
</button>
|
||||
<a href="<?php echo esc_url(admin_url('admin.php?page=bloques-import-actuaciones')); ?>" class="button" style="margin-left:8px;">
|
||||
<?php esc_html_e('Cancelar', 'bloques-actuaciones-import'); ?>
|
||||
</a>
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
Reference in New Issue
Block a user