<?php
/**
 * Admin menu and page controllers.
 *
 * @package Logistica
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

require_once __DIR__ . '/class-logistica-rotulo-rollos-print.php';
require_once __DIR__ . '/class-logistica-rotulo-rollos-print-history.php';
require_once __DIR__ . '/class-logistica-rotulo-rollos-catalog.php';
require_once __DIR__ . '/class-logistica-label-pack-history.php';
require_once __DIR__ . '/class-logistica-label-logo-history.php';

/**
 * Registers admin pages and handles admin actions.
 */
class Logistica_Admin_Menu {

	/** Posición del menú principal (debajo de JB Galeria, posición 62). */
	const MENU_POSITION = 63;

	/** Posición de Apariencia, justo debajo de JB Planeación. */
	const APPEARANCE_MENU_POSITION = 64;

	/**
	 * Registers hooks.
	 *
	 * @return void
	 */
	public function init_request_handlers() {
		add_action( 'admin_post_logistica_save_formula_settings', array( $this, 'save_formula_settings' ) );
		add_action( 'admin_post_logistica_save_label_print_settings', array( $this, 'save_label_print_settings' ) );
		add_action( 'wp_ajax_logistica_save_label_print_settings', array( $this, 'ajax_save_label_print_settings' ) );
		add_action( 'admin_post_logistica_save_etiqueta_producto', array( $this, 'save_etiqueta_producto' ) );
		add_action( 'admin_post_logistica_delete_etiqueta_producto', array( $this, 'delete_etiqueta_producto' ) );
		add_action( 'admin_post_logistica_save_rotulo_rollos', array( $this, 'save_rotulo_rollos' ) );
		add_action( 'admin_post_logistica_delete_rotulo_rollos', array( $this, 'delete_rotulo_rollos' ) );
		add_action( 'admin_post_logistica_save_rotulo_rollos_print_settings', array( $this, 'save_rotulo_rollos_print_settings' ) );
		add_action( 'wp_ajax_logistica_save_rotulo_rollos_print_settings', array( $this, 'ajax_save_rotulo_rollos_print_settings' ) );
		add_action( 'wp_ajax_logistica_get_rotulo_rollos_row', array( $this, 'get_rotulo_rollos_row' ) );
		add_action( 'wp_ajax_logistica_save_rotulo_rollos_ajax', array( $this, 'ajax_save_rotulo_rollos' ) );
		add_action( 'wp_ajax_logistica_get_rotulo_rollos_print_history', array( $this, 'get_rotulo_rollos_print_history' ) );
		add_action( 'wp_ajax_logistica_save_rotulo_rollos_print_history', array( $this, 'save_rotulo_rollos_print_history' ) );
		add_action( 'admin_post_logistica_export_rotulo_rollos', array( $this, 'export_rotulo_rollos' ) );
		add_action( 'admin_post_logistica_import_rotulo_rollos', array( $this, 'import_rotulo_rollos' ) );
		add_action( 'admin_post_logistica_export_etiqueta_productos', array( $this, 'export_etiqueta_productos' ) );
		add_action( 'admin_post_logistica_import_etiqueta_productos', array( $this, 'import_etiqueta_productos' ) );
		add_action( 'wp_ajax_logistica_preview_label_barcode', array( $this, 'preview_label_barcode' ) );
		add_action( 'wp_ajax_logistica_get_label_pack_history', array( $this, 'get_label_pack_history' ) );
		add_action( 'wp_ajax_logistica_save_label_pack_history', array( $this, 'save_label_pack_history' ) );
		add_action( 'wp_ajax_logistica_save_label_logo_history', array( $this, 'save_label_logo_history' ) );
		add_action( 'wp_ajax_logistica_get_etiqueta_producto_row', array( $this, 'get_etiqueta_producto_row' ) );
		add_action( 'wp_ajax_logistica_save_etiqueta_producto_ajax', array( $this, 'ajax_save_etiqueta_producto' ) );
		add_action( 'wp_ajax_logistica_preview_etiqueta_ean_fields', array( $this, 'preview_etiqueta_ean_fields' ) );
		add_action( 'wp_ajax_logistica_save_etiqueta_tamano', array( $this, 'save_etiqueta_tamano' ) );
		add_action( 'wp_ajax_logistica_save_etiqueta_tipo_producto', array( $this, 'save_etiqueta_tipo_producto' ) );
		add_action( 'wp_ajax_logistica_update_etiqueta_tipo_producto', array( $this, 'update_etiqueta_tipo_producto' ) );
		add_action( 'wp_ajax_logistica_delete_etiqueta_tipo_producto', array( $this, 'delete_etiqueta_tipo_producto' ) );
		add_action( 'wp_ajax_logistica_update_etiqueta_tamano', array( $this, 'update_etiqueta_tamano' ) );
		add_action( 'wp_ajax_logistica_delete_etiqueta_tamano', array( $this, 'delete_etiqueta_tamano' ) );
		add_action( 'wp_ajax_logistica_update_etiqueta_mercado', array( $this, 'update_etiqueta_mercado' ) );
		add_action( 'wp_ajax_logistica_save_etiqueta_mercado', array( $this, 'save_etiqueta_mercado' ) );
		add_action( 'wp_ajax_logistica_delete_etiqueta_mercado', array( $this, 'delete_etiqueta_mercado' ) );
		add_action( 'wp_ajax_logistica_check_etiqueta_option_usage', array( $this, 'check_etiqueta_option_usage' ) );
		add_action( 'wp_ajax_logistica_restore_etiqueta_tipo_producto', array( $this, 'restore_etiqueta_tipo_producto' ) );
		add_action( 'wp_ajax_logistica_restore_etiqueta_mercado', array( $this, 'restore_etiqueta_mercado' ) );
		add_action( 'wp_ajax_logistica_restore_etiqueta_tamano', array( $this, 'restore_etiqueta_tamano' ) );
		add_action( 'wp_ajax_logistica_select_catalog_add', array( $this, 'select_catalog_add' ) );
		add_action( 'wp_ajax_logistica_select_catalog_update', array( $this, 'select_catalog_update' ) );
		add_action( 'wp_ajax_logistica_select_catalog_delete', array( $this, 'select_catalog_delete' ) );
		add_action( 'wp_ajax_logistica_select_catalog_check_usage', array( $this, 'select_catalog_check_usage' ) );
		add_action( 'wp_ajax_logistica_render_tab_panel', array( $this, 'render_tab_panel' ) );
		add_action( 'wp_ajax_logistica_add_pedidos_board', array( $this, 'add_pedidos_board' ) );
		add_action( 'wp_ajax_logistica_update_pedidos_board', array( $this, 'update_pedidos_board' ) );
		add_action( 'wp_ajax_logistica_delete_pedidos_board', array( $this, 'delete_pedidos_board' ) );
		add_action( 'wp_ajax_logistica_save_pedidos_inventario', array( $this, 'save_pedidos_inventario' ) );
		add_action( 'wp_ajax_logistica_delete_pedidos_inventario', array( $this, 'delete_pedidos_inventario' ) );
		add_action( 'wp_ajax_logistica_add_pedidos_stock', array( $this, 'add_pedidos_stock' ) );
		add_action( 'wp_ajax_logistica_subtract_pedidos_stock', array( $this, 'subtract_pedidos_stock' ) );
		add_action( 'wp_ajax_logistica_save_pedidos_entrega', array( $this, 'save_pedidos_entrega' ) );
		add_action( 'wp_ajax_logistica_save_pedidos_orden_linea', array( $this, 'save_pedidos_orden_linea' ) );
		add_action( 'wp_ajax_logistica_save_pedidos_orden_linea_entrega', array( $this, 'save_pedidos_orden_linea_entrega' ) );
		add_action( 'wp_ajax_logistica_delete_pedidos_orden_linea', array( $this, 'delete_pedidos_orden_linea' ) );
		add_action( 'wp_ajax_logistica_get_pedidos_clientes', array( $this, 'get_pedidos_clientes' ) );
		add_action( 'wp_ajax_logistica_save_pedidos_cliente', array( $this, 'save_pedidos_cliente' ) );
		add_action( 'wp_ajax_logistica_delete_pedidos_cliente', array( $this, 'delete_pedidos_cliente' ) );
		add_action( 'admin_post_logistica_save_frontend_menu', array( $this, 'save_frontend_menu' ) );
		add_action( 'admin_post_logistica_save_operations_menu_order', array( $this, 'save_operations_menu_order' ) );
		add_action( 'admin_post_logistica_save_help_settings', array( $this, 'save_help_settings' ) );
		add_action( 'admin_post_logistica_save_panel_settings', array( $this, 'save_panel_settings' ) );
		add_action( 'admin_post_logistica_save_cierre_ordenes', array( $this, 'save_cierre_ordenes' ) );
		add_action( 'admin_post_logistica_save_cierre_caracteristica', array( $this, 'save_cierre_caracteristica' ) );
		add_action( 'admin_post_logistica_delete_cierre_caracteristica', array( $this, 'delete_cierre_caracteristica' ) );
		add_action( 'admin_post_logistica_export_cierre_caracteristicas', array( $this, 'export_cierre_caracteristicas' ) );
		add_action( 'admin_post_logistica_import_cierre_caracteristicas', array( $this, 'import_cierre_caracteristicas' ) );
		add_action( 'admin_post_logistica_export_cierre_excel', array( $this, 'export_cierre_excel' ) );
		add_action( 'admin_post_logistica_import_cierre_excel', array( $this, 'import_cierre_excel' ) );
		add_action( 'admin_post_logistica_export_inventario_bodega', array( $this, 'export_inventario_bodega' ) );
		add_action( 'admin_post_logistica_export_planeacion_impresion', array( $this, 'export_planeacion_impresion' ) );
		add_action( 'admin_post_logistica_export_planeacion_formado', array( $this, 'export_planeacion_formado' ) );
		add_action( 'admin_post_logistica_import_inventario_bodega', array( $this, 'import_inventario_bodega' ) );
		add_action( 'admin_post_logistica_export_minutas', array( $this, 'export_minutas' ) );
		add_action( 'admin_post_logistica_import_minutas', array( $this, 'import_minutas' ) );
		add_action( 'admin_post_logistica_export_pedidos_ordenes', array( $this, 'export_pedidos_ordenes' ) );
		add_action( 'admin_post_logistica_export_pedidos_ordenes_historial', array( $this, 'export_pedidos_ordenes_historial' ) );
		add_action( 'admin_post_logistica_import_pedidos_ordenes', array( $this, 'import_pedidos_ordenes' ) );
		add_action( 'wp_ajax_logistica_save_minuta_revision', array( $this, 'save_minuta_revision' ) );
		add_action( 'wp_ajax_logistica_autosave_cierre_ordenes', array( $this, 'autosave_cierre_ordenes' ) );
		add_action( 'wp_ajax_logistica_refresh_cierre_bases', array( $this, 'refresh_cierre_bases' ) );
		add_action( 'wp_ajax_logistica_save_warehouse_fisico', array( $this, 'save_warehouse_fisico' ) );
		add_action( 'wp_ajax_logistica_query_warehouse_rows', array( $this, 'query_warehouse_rows' ) );
		add_action( 'wp_ajax_logistica_get_warehouse_row', array( $this, 'get_warehouse_row' ) );
		add_action( 'wp_ajax_logistica_save_warehouse_row', array( $this, 'save_warehouse_row' ) );
		add_action( 'wp_ajax_logistica_warehouse_delete_row', array( $this, 'warehouse_delete_row' ) );
		add_action( 'wp_ajax_logistica_warehouse_duplicate_row', array( $this, 'warehouse_duplicate_row' ) );
		add_action( 'wp_ajax_logistica_warehouse_toggle_estado', array( $this, 'warehouse_toggle_estado' ) );
		add_action( 'wp_ajax_logistica_warehouse_add_row', array( $this, 'warehouse_add_row' ) );
		add_action( 'wp_ajax_logistica_get_warehouse_lote', array( $this, 'get_warehouse_lote' ) );
		add_action( 'wp_ajax_logistica_get_warehouse_lote_map', array( $this, 'get_warehouse_lote_map' ) );
		add_action( 'wp_ajax_logistica_update_warehouse_lote_from_cierre', array( $this, 'update_warehouse_lote_from_cierre' ) );
		add_action( 'wp_ajax_logistica_get_cierre_caracteristica', array( $this, 'get_cierre_caracteristica' ) );
		add_action( 'wp_ajax_logistica_get_cierre_caracteristicas_map', array( $this, 'get_cierre_caracteristicas_map' ) );
		add_action( 'wp_ajax_logistica_save_cierre_caracteristica', array( $this, 'ajax_save_cierre_caracteristica' ) );
		add_action( 'wp_ajax_logistica_delete_cierre_caracteristica', array( $this, 'ajax_delete_cierre_caracteristica' ) );
		add_action( 'wp_ajax_logistica_get_cierre_caracteristica_row', array( $this, 'get_cierre_caracteristica_row' ) );
		add_action( 'wp_ajax_logistica_get_conversion_templates', array( $this, 'get_conversion_templates' ) );
		add_action( 'wp_ajax_logistica_save_conversion_template', array( $this, 'save_conversion_template' ) );
		add_action( 'wp_ajax_logistica_delete_conversion_template', array( $this, 'delete_conversion_template' ) );
		add_action( 'wp_ajax_logistica_get_conversion_tables', array( $this, 'get_conversion_tables' ) );
		add_action( 'wp_ajax_logistica_save_conversion_tables', array( $this, 'save_conversion_tables' ) );
		add_action( 'wp_ajax_logistica_query_lifecycle', array( $this, 'query_lifecycle' ) );
		add_action( 'wp_ajax_logistica_restore_lifecycle_trash', array( $this, 'restore_lifecycle_trash' ) );
		add_action( 'wp_ajax_logistica_restore_lifecycle_version', array( $this, 'restore_lifecycle_version' ) );
		add_action( 'wp_ajax_logistica_purge_lifecycle_trash', array( $this, 'purge_lifecycle_trash' ) );
		add_action( 'admin_post_logistica_export_lifecycle', array( $this, 'export_lifecycle' ) );
		add_action( 'wp_ajax_logistica_get_planeacion_impresion', array( $this, 'get_planeacion_impresion' ) );
		add_action( 'wp_ajax_logistica_get_planeacion_impresion_metrics', array( $this, 'get_planeacion_impresion_metrics' ) );
		add_action( 'wp_ajax_logistica_save_planeacion_impresion', array( $this, 'save_planeacion_impresion' ) );
		add_action( 'wp_ajax_logistica_import_planeacion_impresion', array( $this, 'import_planeacion_impresion' ) );
		add_action( 'wp_ajax_logistica_get_planeacion_formado', array( $this, 'get_planeacion_formado' ) );
		add_action( 'wp_ajax_logistica_get_planeacion_formado_metrics', array( $this, 'get_planeacion_formado_metrics' ) );
		add_action( 'wp_ajax_logistica_get_planeacion_formado_metrics_period', array( $this, 'get_planeacion_formado_metrics_period' ) );
		add_action( 'wp_ajax_logistica_get_pedidos_metrics_week', array( $this, 'get_pedidos_metrics_week' ) );
		add_action( 'wp_ajax_logistica_get_pedidos_metrics_period', array( $this, 'get_pedidos_metrics_period' ) );
		add_action( 'wp_ajax_logistica_save_planeacion_formado', array( $this, 'save_planeacion_formado' ) );
		add_action( 'wp_ajax_logistica_import_planeacion_formado', array( $this, 'import_planeacion_formado' ) );
		add_action( 'wp_ajax_logistica_get_tablero_produccion', array( $this, 'get_tablero_produccion' ) );
		add_action( 'wp_ajax_logistica_save_tablero_produccion_job', array( $this, 'save_tablero_produccion_job' ) );
		add_action( 'wp_ajax_logistica_add_planeacion_machine', array( $this, 'add_planeacion_machine' ) );
		add_action( 'wp_ajax_logistica_update_planeacion_machine', array( $this, 'update_planeacion_machine' ) );
		add_action( 'wp_ajax_logistica_delete_planeacion_machine', array( $this, 'delete_planeacion_machine' ) );
		add_action( 'wp_ajax_logistica_save_planeacion_field_options', array( $this, 'save_planeacion_field_options' ) );
		add_action( 'wp_ajax_logistica_add_planeacion_formado_machine', array( $this, 'add_planeacion_formado_machine' ) );
		add_action( 'wp_ajax_logistica_update_planeacion_formado_machine', array( $this, 'update_planeacion_formado_machine' ) );
		add_action( 'wp_ajax_logistica_delete_planeacion_formado_machine', array( $this, 'delete_planeacion_formado_machine' ) );
		add_action( 'wp_ajax_logistica_save_planeacion_formado_field_options', array( $this, 'save_planeacion_formado_field_options' ) );
		add_action( 'wp_ajax_logistica_save_forming_yield_catalog', array( $this, 'save_forming_yield_catalog' ) );
		add_action( 'wp_ajax_logistica_get_pedidos_pendientes_planeacion', array( $this, 'get_pedidos_pendientes_planeacion' ) );
		add_action( 'wp_ajax_logistica_programar_pedidos_planeacion', array( $this, 'programar_pedidos_planeacion' ) );
		add_action( 'wp_ajax_logistica_get_user_panel', array( $this, 'get_user_panel' ) );
		add_action( 'wp_ajax_logistica_save_user_panel', array( $this, 'save_user_panel' ) );
		add_action( 'wp_ajax_logistica_mark_panel_avisos_read', array( $this, 'mark_panel_avisos_read' ) );
		add_action( 'wp_ajax_logistica_delete_panel_avisos', array( $this, 'delete_panel_avisos' ) );
		add_action( 'wp_ajax_logistica_send_panel_aviso_email', array( $this, 'send_panel_aviso_email' ) );
		add_action( 'admin_post_logistica_export_panel_proveedores', array( $this, 'export_panel_proveedores' ) );
		add_action( 'wp_ajax_logistica_export_panel_proveedores', array( $this, 'ajax_export_panel_proveedores' ) );
		add_action( 'wp_ajax_logistica_import_panel_proveedores', array( $this, 'import_panel_proveedores' ) );
		add_action( 'admin_post_logistica_export_panel_compras', array( $this, 'export_panel_compras' ) );
		add_action( 'wp_ajax_logistica_export_panel_compras', array( $this, 'ajax_export_panel_compras' ) );
		add_action( 'wp_ajax_logistica_import_panel_compras', array( $this, 'import_panel_compras' ) );
		add_action( 'wp_ajax_logistica_record_resume_point', array( $this, 'record_resume_point' ) );
		add_action( 'wp_ajax_logistica_dismiss_resume_point', array( $this, 'dismiss_resume_point' ) );
		add_action( 'wp_ajax_logistica_save_quick_links', array( $this, 'save_quick_links' ) );
	}

	/**
	 * Registers hooks.
	 *
	 * @return void
	 */
	public function init() {
		add_action( 'admin_menu', array( $this, 'register_menu' ) );
		add_action( 'admin_menu', array( $this, 'reorder_admin_menu' ), 999 );
		add_action( 'admin_init', array( $this, 'redirect_legacy_menu_pages' ) );
		add_action( 'admin_init', array( $this, 'redirect_legacy_cierre_caracteristicas' ) );
		add_action( 'admin_init', array( $this, 'redirect_operational_admin_tabs' ) );
		add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) );
	}

	/**
	 * Adds the main menu and internal pages.
	 *
	 * @return void
	 */
	public function register_menu() {
		add_menu_page(
			__( 'JB Planeación', 'logistica' ),
			__( 'JB Planeación', 'logistica' ),
			'manage_options',
			'logistica',
			array( $this, 'render_tabbed_page' ),
			'dashicons-clipboard',
			self::MENU_POSITION
		);
	}

	/**
	 * Redirige ?cierre_section=caracteristicas a la pestaña principal.
	 *
	 * @return void
	 */
	public function redirect_legacy_cierre_caracteristicas() {
		require_once LOGISTICA_PLUGIN_DIR . 'admin/views/partials.php';
		logistica_redirect_legacy_cierre_caracteristicas_section();
	}

	/**
	 * Redirects old submenu slugs to ?page=logistica&tab=...
	 *
	 * @return void
	 */
	public function redirect_legacy_menu_pages() {
		if ( ! is_admin() || ! isset( $_GET['page'] ) ) {
			return;
		}

		$page = sanitize_key( wp_unslash( $_GET['page'] ) );
		if ( 'logistica' === $page || 0 !== strpos( $page, 'logistica-' ) ) {
			return;
		}

		$removed_pages = array(
			'logistica-productos',
			'logistica-materia-prima',
			'logistica-movimientos',
			'logistica-reportes',
		);

		if ( in_array( $page, $removed_pages, true ) ) {
			wp_safe_redirect( admin_url( 'admin.php?page=logistica' ) );
			exit;
		}

		wp_safe_redirect(
			add_query_arg(
				array(
					'page' => 'logistica',
					'tab'  => $page,
				),
				admin_url( 'admin.php' )
			)
		);
		exit;
	}

	/**
	 * Redirige pestañas operativas al panel del sitio antes de enviar HTML.
	 *
	 * @return void
	 */
	public function redirect_operational_admin_tabs() {
		if ( ! is_admin() || ! isset( $_GET['page'] ) ) {
			return;
		}

		$page = sanitize_key( wp_unslash( $_GET['page'] ) );
		if ( 'logistica' !== $page ) {
			return;
		}

		require_once LOGISTICA_PLUGIN_DIR . 'admin/views/partials.php';

		$tab = logistica_get_current_tab();
		if ( Logistica_Tabs::is_admin_only_tab( $tab ) ) {
			return;
		}

		wp_safe_redirect( Logistica_Roles::get_operations_panel_url( $tab ) );
		exit;
	}

	/**
	 * Renders the active tab view in wp-admin.
	 *
	 * @return void
	 */
	public function render_tabbed_page() {
		require_once LOGISTICA_PLUGIN_DIR . 'admin/views/partials.php';

		$tab        = logistica_get_current_tab();
		$admin_tabs = logistica_get_nav_tabs_for_context( 'admin' );

		if ( ! isset( $admin_tabs[ $tab ] ) ) {
			$tab = 'logistica-configuracion';
		}

		$this->render_view( logistica_get_tab_views()[ $tab ] );
	}

	/**
	 * Coloca «Apariencia» debajo de JB Planeación en el menú lateral.
	 *
	 * @return void
	 */
	public function reorder_admin_menu() {
		global $menu;

		if ( ! is_array( $menu ) ) {
			return;
		}

		$appearance     = null;
		$appearance_key = null;

		foreach ( $menu as $key => $item ) {
			if ( isset( $item[2] ) && 'themes.php' === $item[2] ) {
				$appearance     = $item;
				$appearance_key = $key;
				break;
			}
		}

		if ( null === $appearance ) {
			return;
		}

		unset( $menu[ $appearance_key ] );
		$menu[ self::APPEARANCE_MENU_POSITION ] = $appearance;
	}

	/**
	 * Loads admin CSS and JS only on plugin pages.
	 *
	 * @param string $hook Current admin hook.
	 * @return void
	 */
	public function enqueue_assets( $hook = '' ) {
		if ( is_string( $hook ) && '' !== $hook && false === strpos( $hook, 'logistica' ) ) {
			return;
		}

		require_once LOGISTICA_PLUGIN_DIR . 'admin/views/partials.php';

		wp_enqueue_style( 'dashicons' );

		wp_enqueue_style(
			'logistica-admin',
			LOGISTICA_PLUGIN_URL . 'assets/css/admin.css',
			array( 'dashicons' ),
			logistica_asset_version( 'assets/css/admin.css' )
		);

		wp_enqueue_style(
			'logistica-user-panel',
			LOGISTICA_PLUGIN_URL . 'assets/css/panel.css',
			array( 'logistica-admin' ),
			logistica_asset_version( 'assets/css/panel.css' )
		);

		wp_enqueue_style(
			'logistica-config-panel',
			LOGISTICA_PLUGIN_URL . 'assets/css/config-panel.css',
			array( 'logistica-user-panel' ),
			logistica_asset_version( 'assets/css/config-panel.css' )
		);

		$this->enqueue_label_print_assets();
		$this->enqueue_rotulo_rollos_assets();

		Logistica_Panel_Auth::enqueue_session_guard();

		wp_enqueue_script(
			'logistica-panel-nav',
			LOGISTICA_PLUGIN_URL . 'assets/js/panel-nav.js',
			array( 'logistica-session-guard' ),
			logistica_asset_version( 'assets/js/panel-nav.js' ),
			true
		);

		wp_enqueue_script(
			'logistica-config-nav',
			LOGISTICA_PLUGIN_URL . 'assets/js/config-nav.js',
			array( 'logistica-panel-nav' ),
			logistica_asset_version( 'assets/js/config-nav.js' ),
			true
		);

		wp_enqueue_script(
			'logistica-admin',
			LOGISTICA_PLUGIN_URL . 'assets/js/admin.js',
			array( 'logistica-session-guard', 'logistica-panel-nav', 'logistica-config-nav', 'logistica-label-print', 'logistica-rotulo-rollos-print', 'logistica-rotulo-estibas-print', 'logistica-orden-produccion-vasos' ),
			logistica_asset_version( 'assets/js/admin.js' ),
			true
		);

		$tab = logistica_get_current_tab();

		if ( 'logistica-planeacion-impresion' === $tab ) {
			wp_enqueue_script(
				'chart-js',
				'https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js',
				array(),
				'4.4.1',
				true
			);

			wp_enqueue_script(
				'logistica-planeacion-metrics',
				LOGISTICA_PLUGIN_URL . 'assets/js/planeacion-metrics.js',
				array(),
				logistica_asset_version( 'assets/js/planeacion-metrics.js' ),
				true
			);

			wp_enqueue_style(
				'logistica-planeacion-impresion',
				LOGISTICA_PLUGIN_URL . 'assets/css/planeacion-impresion.css',
				array( 'logistica-admin' ),
				logistica_asset_version( 'assets/css/planeacion-impresion.css' )
			);

			wp_enqueue_script(
				'logistica-planeacion-planner',
				LOGISTICA_PLUGIN_URL . 'assets/js/planeacion-planner.js',
				array(),
				logistica_asset_version( 'assets/js/planeacion-planner.js' ),
				true
			);

			wp_enqueue_script(
				'logistica-planeacion-task-colors',
				LOGISTICA_PLUGIN_URL . 'assets/js/planeacion-task-colors.js',
				array(),
				logistica_asset_version( 'assets/js/planeacion-task-colors.js' ),
				true
			);

			wp_enqueue_script(
				'logistica-planeacion-pedidos-bridge',
				LOGISTICA_PLUGIN_URL . 'assets/js/planeacion-pedidos-bridge.js',
				array(),
				logistica_asset_version( 'assets/js/planeacion-pedidos-bridge.js' ),
				true
			);

			wp_enqueue_script(
				'logistica-planeacion-impresion',
				LOGISTICA_PLUGIN_URL . 'assets/js/planeacion-impresion.js',
				array( 'logistica-admin', 'logistica-planeacion-metrics', 'logistica-planeacion-planner', 'logistica-planeacion-task-colors', 'logistica-planeacion-pedidos-bridge', 'chart-js' ),
				logistica_asset_version( 'assets/js/planeacion-impresion.js' ),
				true
			);
		}

		if ( 'logistica-planeacion-formado' === $tab ) {
			wp_enqueue_script(
				'chart-js',
				'https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js',
				array(),
				'4.4.1',
				true
			);

			wp_enqueue_script(
				'logistica-planeacion-metrics',
				LOGISTICA_PLUGIN_URL . 'assets/js/planeacion-metrics.js',
				array(),
				logistica_asset_version( 'assets/js/planeacion-metrics.js' ),
				true
			);

			wp_enqueue_style(
				'logistica-planeacion-formado',
				LOGISTICA_PLUGIN_URL . 'assets/css/planeacion-impresion.css',
				array( 'logistica-admin' ),
				logistica_asset_version( 'assets/css/planeacion-impresion.css' )
			);

			wp_enqueue_script(
				'logistica-planeacion-planner',
				LOGISTICA_PLUGIN_URL . 'assets/js/planeacion-planner.js',
				array(),
				logistica_asset_version( 'assets/js/planeacion-planner.js' ),
				true
			);

			wp_enqueue_script(
				'logistica-planeacion-task-colors',
				LOGISTICA_PLUGIN_URL . 'assets/js/planeacion-task-colors.js',
				array(),
				logistica_asset_version( 'assets/js/planeacion-task-colors.js' ),
				true
			);

			wp_enqueue_script(
				'logistica-planeacion-pedidos-bridge',
				LOGISTICA_PLUGIN_URL . 'assets/js/planeacion-pedidos-bridge.js',
				array(),
				logistica_asset_version( 'assets/js/planeacion-pedidos-bridge.js' ),
				true
			);

			wp_enqueue_script(
				'logistica-planeacion-formado',
				LOGISTICA_PLUGIN_URL . 'assets/js/planeacion-formado.js',
				array( 'logistica-admin', 'logistica-planeacion-metrics', 'logistica-planeacion-planner', 'logistica-planeacion-task-colors', 'logistica-planeacion-pedidos-bridge', 'chart-js' ),
				logistica_asset_version( 'assets/js/planeacion-formado.js' ),
				true
			);
		}

		if ( 'logistica-cuadro-pedidos' === $tab ) {
			wp_enqueue_style(
				'logistica-planeacion-formado',
				LOGISTICA_PLUGIN_URL . 'assets/css/planeacion-impresion.css',
				array( 'logistica-admin' ),
				logistica_asset_version( 'assets/css/planeacion-impresion.css' )
			);
			wp_enqueue_script(
				'chart-js',
				'https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js',
				array(),
				'4.4.1',
				true
			);
			wp_enqueue_script(
				'logistica-planeacion-metrics',
				LOGISTICA_PLUGIN_URL . 'assets/js/planeacion-metrics.js',
				array( 'chart-js' ),
				logistica_asset_version( 'assets/js/planeacion-metrics.js' ),
				true
			);
			wp_enqueue_script(
				'logistica-pedidos-board',
				LOGISTICA_PLUGIN_URL . 'assets/js/pedidos-board.js',
				array( 'logistica-admin', 'logistica-planeacion-metrics', 'chart-js' ),
				logistica_asset_version( 'assets/js/pedidos-board.js' ),
				true
			);
		}

		if ( 'logistica-tablero-produccion' === $tab ) {
			wp_enqueue_style(
				'logistica-tablero-produccion',
				LOGISTICA_PLUGIN_URL . 'assets/css/tablero-produccion.css',
				array( 'logistica-admin' ),
				logistica_asset_version( 'assets/css/tablero-produccion.css' )
			);
			wp_enqueue_script(
				'logistica-tablero-produccion',
				LOGISTICA_PLUGIN_URL . 'assets/js/tablero-produccion.js',
				array( 'logistica-admin' ),
				logistica_asset_version( 'assets/js/tablero-produccion.js' ),
				true
			);
		}

		if ( 'logistica-configuracion' === $tab || 'logistica-ayuda' === $tab ) {
			if ( function_exists( 'wp_enqueue_editor' ) ) {
				wp_enqueue_editor();
			}
		}

		if ( 'logistica' === $tab ) {
			wp_enqueue_script(
				'logistica-user-panel',
				LOGISTICA_PLUGIN_URL . 'assets/js/panel.js',
				array( 'logistica-session-guard' ),
				logistica_asset_version( 'assets/js/panel.js' ),
				true
			);

			wp_enqueue_script(
				'logistica-user-panel-nav',
				LOGISTICA_PLUGIN_URL . 'assets/js/user-panel-nav.js',
				array( 'logistica-panel-nav' ),
				logistica_asset_version( 'assets/js/user-panel-nav.js' ),
				true
			);
		}

		if ( 'logistica-configuracion' === $tab ) {
			wp_enqueue_script(
				'logistica-menu-order',
				LOGISTICA_PLUGIN_URL . 'assets/js/menu-order.js',
				array(),
				logistica_asset_version( 'assets/js/menu-order.js' ),
				true
			);

			wp_enqueue_script(
				'logistica-lifecycle-tables',
				LOGISTICA_PLUGIN_URL . 'assets/js/lifecycle-tables.js',
				array( 'logistica-admin' ),
				logistica_asset_version( 'assets/js/lifecycle-tables.js' ),
				true
			);

			wp_enqueue_script(
				'logistica-user-admin',
				LOGISTICA_PLUGIN_URL . 'assets/js/user-admin.js',
				array(),
				logistica_asset_version( 'assets/js/user-admin.js' ),
				true
			);

			wp_enqueue_script(
				'logistica-role-permissions-modal',
				LOGISTICA_PLUGIN_URL . 'assets/js/role-permissions-modal.js',
				array(),
				logistica_asset_version( 'assets/js/role-permissions-modal.js' ),
				true
			);
		}

		// Full client config always: tabs load via AJAX without reload, so Inicio
		// must still expose cierre/warehouse/etc. nonces for later panels.
		wp_localize_script(
			'logistica-admin',
			'LogisticaAdmin',
			array(
				'ajaxUrl'                      => admin_url( 'admin-ajax.php' ),
				'cierreAutosaveNonce'          => wp_create_nonce( 'logistica_autosave_cierre_ordenes' ),
				'cierreRefreshNonce'           => wp_create_nonce( 'logistica_refresh_cierre_bases' ),
				'cierreNonce'                  => wp_create_nonce( 'logistica_cierre_ordenes' ),
				'cierreCaracteristicasNonce'   => wp_create_nonce( 'logistica_cierre_caracteristicas' ),
				'labelPrintNonce'              => wp_create_nonce( 'logistica_label_print' ),
				'labelPrintConfig'             => Logistica_Label_Print::client_config(),
				'rotulosRollosNonce'           => wp_create_nonce( 'logistica_rotulos_rollos' ),
				'rotuloRollosPrintConfig'      => Logistica_Rotulo_Rollos_Print::client_config(),
				'pedidosBoardNonce'            => wp_create_nonce( 'logistica_pedidos_board' ),
				'jsBarcodeUrl'                 => LOGISTICA_PLUGIN_URL . 'assets/js/vendor/jsbarcode.all.min.js',
				'conversionTemplatesNonce'     => wp_create_nonce( 'logistica_conversion_templates' ),
				'conversionTablesNonce'        => wp_create_nonce( 'logistica_conversion_tables' ),
				'warehouseNonce'               => wp_create_nonce( 'logistica_warehouse_inventory' ),
				'minutasNonce'                 => wp_create_nonce( 'logistica_minutas' ),
				'planeacionNonce'              => wp_create_nonce( 'logistica_planeacion_impresion' ),
				'planeacionExportNonce'        => wp_create_nonce( 'logistica_export_planeacion_impresion' ),
				'planeacionFormadoNonce'       => wp_create_nonce( 'logistica_planeacion_formado' ),
				'planeacionFormadoExportNonce' => wp_create_nonce( 'logistica_export_planeacion_formado' ),
				'tableroProduccionNonce'       => wp_create_nonce( 'logistica_tablero_produccion' ),
				'userPanelNonce'               => wp_create_nonce( 'logistica_user_panel' ),
				'adminPostUrl'                 => admin_url( 'admin-post.php' ),
				'navTabs'                      => logistica_get_nav_tabs(),
				'navTabUrls'                   => ( static function () {
					$urls = array();
					foreach ( logistica_get_nav_tabs() as $slug => $label ) {
						$urls[ $slug ] = logistica_tab_url( $slug, 'admin' );
					}
					return $urls;
				} )(),
				'closingRollTypes'             => Logistica_Order_Closing::base_types_roll_like(),
				'closingSheetTypes'            => Logistica_Order_Closing::base_types_sheet_like(),
				'closingBaseLabels'            => Logistica_Order_Closing::base_type_options(),
				'closingBaseTypeFields'        => Logistica_Order_Closing::get_base_type_fields_map(),
				'closingBaseFieldDefinitions'  => Logistica_Order_Closing::base_field_definitions(),
				'closingFormulaLabels'         => Logistica_Order_Closing::formula_options(),
				'lifecycleNonce'               => wp_create_nonce( 'logistica_lifecycle' ),
				'canRestoreLifecycle'          => Logistica_Entity_Lifecycle::can_restore(),
				'canPurgeLifecycle'            => Logistica_Entity_Lifecycle::can_purge(),
				'selectCatalogNonce'           => wp_create_nonce( 'logistica_select_catalog' ),
				'tabPanelNonce'                => wp_create_nonce( 'logistica_tab_panel' ),
			)
		);
	}

	/**
	 * Renders dashboard.
	 *
	 * @return void
	 */
	public function render_dashboard() {
		$this->render_view( 'dashboard.php' );
	}

	/**
	 * Renders conversions page.
	 *
	 * @return void
	 */
	public function render_conversiones() {
		$this->render_view( 'conversiones.php' );
	}

	/**
	 * Renders order closing page.
	 *
	 * @return void
	 */
	public function render_cierre_ordenes() {
		$this->render_view( 'cierre-ordenes.php' );
	}

	/**
	 * Renders settings page.
	 *
	 * @return void
	 */
	public function render_configuracion() {
		$this->render_view( 'configuracion.php' );
	}

	/**
	 * Saves configurable conversion formulas.
	 *
	 * @return void
	 */
	public function save_formula_settings() {
		$this->verify_request( 'logistica_save_formula_settings' );
		Logistica_Conversions::save_formula_settings( $_POST['formulas'] ?? array() );
		$this->redirect_with_status( 'logistica-conversiones', 'saved' );
	}

	/**
	 * Guarda las pestañas visibles en el panel público.
	 *
	 * @return void
	 */
	public function save_frontend_menu() {
		$this->verify_request( 'logistica_save_frontend_menu' );
		Logistica_Settings::save_frontend_menu_tabs( $_POST['frontend_menu_tabs'] ?? array() );
		Logistica_Settings::save_frontend_page_id( $_POST['frontend_page_id'] ?? 0 );
		$this->redirect_with_status( 'logistica-configuracion', 'menu_saved' );
	}

	/**
	 * Guarda el orden del menú lateral del panel de operaciones.
	 *
	 * @return void
	 */
	public function save_operations_menu_order() {
		$this->verify_request( 'logistica_save_operations_menu_order' );
		Logistica_Settings::save_operations_menu_order( $_POST['menu_order'] ?? array() );
		$this->redirect_with_status( 'logistica-configuracion', 'menu_order_saved' );
	}

	/**
	 * Guarda el contenido de la pestaña Ayuda.
	 *
	 * @return void
	 */
	public function save_help_settings() {
		$this->verify_request( 'logistica_save_help_settings' );
		Logistica_Settings::save_help_settings( $_POST );
		require_once LOGISTICA_PLUGIN_DIR . 'admin/views/partials.php';
		$redirect = ! empty( $_POST['logistica_redirect_to'] )
			? esc_url_raw( wp_unslash( (string) $_POST['logistica_redirect_to'] ) )
			: logistica_config_panel_url( 'logistica-ayuda' );
		wp_safe_redirect(
			add_query_arg(
				'logistica_status',
				'help_saved',
				remove_query_arg( 'logistica_status', $redirect )
			)
		);
		exit;
	}

	/**
	 * Guarda opciones administrativas del panel operativo.
	 *
	 * @return void
	 */
	public function save_panel_settings() {
		$this->verify_request( 'logistica_save_panel_settings' );
		Logistica_Settings::save_compra_statuses( wp_unslash( $_POST['compra_statuses'] ?? array() ) );
		Logistica_Settings::save_panel_holidays( wp_unslash( $_POST['panel_holidays'] ?? array() ) );
		$this->redirect_with_status( 'logistica', 'panel_saved' );
	}

	/**
	 * Saves order closing configuration.
	 *
	 * @return void
	 */
	public function save_cierre_ordenes() {
		$this->verify_request( 'logistica_save_cierre_ordenes' );
		$payload = $this->get_cierre_payload( true );

		if ( is_wp_error( $payload ) ) {
			$this->redirect_with_status( 'logistica-cierre-ordenes', 'error' );
		}

		Logistica_Order_Closing::save_config( $payload );
		$this->redirect_with_status( 'logistica-cierre-ordenes', 'saved' );
	}

	/**
	 * Autosaves order closing configuration through AJAX.
	 *
	 * @return void
	 */
	public function refresh_cierre_bases() {
		if ( ! Logistica_Roles::current_user_can_action( 'logistica_refresh_cierre_bases' ) ) {
			wp_send_json_error( array( 'message' => __( 'Sin permisos.', 'logistica' ) ), 403 );
		}

		check_ajax_referer( 'logistica_refresh_cierre_bases', 'nonce' );

		$stats = Logistica_Order_Closing::refresh_config_from_catalog();

		wp_send_json_success(
			array(
				'message' => sprintf(
					/* translators: 1: total bases, 2: saldo bases */
					__( 'Bases actualizadas: %1$d en total (%2$d de cajas saldo).', 'logistica' ),
					(int) $stats['bases'],
					(int) $stats['saldo_bases']
				),
				'stats'   => $stats,
			)
		);
	}

	/**
	 * Autosaves order closing configuration through AJAX.
	 *
	 * @return void
	 */
	public function autosave_cierre_ordenes() {
		if ( ! Logistica_Roles::current_user_can_action( 'logistica_autosave_cierre_ordenes' ) ) {
			wp_send_json_error( array( 'message' => __( 'Sin permisos.', 'logistica' ) ), 403 );
		}

		check_ajax_referer( 'logistica_autosave_cierre_ordenes', 'nonce' );

		$payload = $this->get_cierre_payload( true );

		if ( is_wp_error( $payload ) ) {
			wp_send_json_error(
				array(
					'message' => $payload->get_error_message(),
				),
				400
			);
		}

		Logistica_Order_Closing::save_config( $payload, false );
		wp_send_json_success(
			array(
				'message' => __( 'Guardado en base de datos.', 'logistica' ),
				'time'    => Logistica_User_Panel::current_time_hms(),
			)
		);
	}

	/**
	 * Returns conversion templates for supported sheets.
	 *
	 * @return void
	 */
	public function get_conversion_templates() {
		if ( ! Logistica_Roles::current_user_can_action( 'logistica_conversion_templates' ) ) {
			wp_send_json_error( array( 'message' => __( 'Sin permisos.', 'logistica' ) ), 403 );
		}

		check_ajax_referer( 'logistica_conversion_templates', 'nonce' );
		wp_send_json_success(
			array(
				'templates' => $this->conversion_templates_option(),
			)
		);
	}

	/**
	 * Creates or updates a conversion template.
	 *
	 * @return void
	 */
	public function save_conversion_template() {
		if ( ! Logistica_Roles::current_user_can_action( 'logistica_conversion_templates' ) ) {
			wp_send_json_error( array( 'message' => __( 'Sin permisos.', 'logistica' ) ), 403 );
		}

		check_ajax_referer( 'logistica_conversion_templates', 'nonce' );

		$type = isset( $_POST['sheet_type'] ) ? sanitize_key( wp_unslash( $_POST['sheet_type'] ) ) : '';
		if ( ! in_array( $type, array( 'bobina-pliego', 'pliego-conversion', 'montaje-compartido' ), true ) ) {
			wp_send_json_error( array( 'message' => __( 'Tipo de conversión inválido.', 'logistica' ) ), 400 );
		}

		$name = isset( $_POST['name'] ) ? sanitize_text_field( wp_unslash( $_POST['name'] ) ) : '';
		if ( '' === $name ) {
			wp_send_json_error( array( 'message' => __( 'El nombre es obligatorio.', 'logistica' ) ), 400 );
		}

		$payload_raw = isset( $_POST['payload'] ) ? wp_unslash( $_POST['payload'] ) : '';
		$payload     = json_decode( is_string( $payload_raw ) ? $payload_raw : '', true );
		if ( ! is_array( $payload ) || ! isset( $payload['rows'] ) || ! is_array( $payload['rows'] ) ) {
			wp_send_json_error( array( 'message' => __( 'Datos de conversión inválidos.', 'logistica' ) ), 400 );
		}

		$templates = $this->conversion_templates_option();
		if ( ! isset( $templates[ $type ] ) || ! is_array( $templates[ $type ] ) ) {
			$templates[ $type ] = array();
		}

		$id = isset( $_POST['template_id'] ) ? sanitize_text_field( wp_unslash( $_POST['template_id'] ) ) : '';
		if ( '' === $id ) {
			$id = uniqid( 'tpl_', false );
		}

		$updated = false;
		foreach ( $templates[ $type ] as $index => $item ) {
			if ( isset( $item['id'] ) && (string) $item['id'] === $id ) {
				$templates[ $type ][ $index ]['name'] = $name;
				$templates[ $type ][ $index ]['data'] = $payload;
				$updated = true;
				break;
			}
		}

		if ( ! $updated ) {
			$templates[ $type ][] = array(
				'id'   => $id,
				'name' => $name,
				'data' => $payload,
			);
		}

		update_option( 'logistica_conversion_templates', $templates, false );
		wp_send_json_success(
			array(
				'templates'   => $templates,
				'template_id' => $id,
			)
		);
	}

	/**
	 * Deletes one conversion template.
	 *
	 * @return void
	 */
	public function delete_conversion_template() {
		if ( ! Logistica_Roles::current_user_can_action( 'logistica_conversion_templates' ) ) {
			wp_send_json_error( array( 'message' => __( 'Sin permisos.', 'logistica' ) ), 403 );
		}

		check_ajax_referer( 'logistica_conversion_templates', 'nonce' );

		$type = isset( $_POST['sheet_type'] ) ? sanitize_key( wp_unslash( $_POST['sheet_type'] ) ) : '';
		$id   = isset( $_POST['template_id'] ) ? sanitize_text_field( wp_unslash( $_POST['template_id'] ) ) : '';

		if ( '' === $type || '' === $id ) {
			wp_send_json_error( array( 'message' => __( 'Solicitud incompleta.', 'logistica' ) ), 400 );
		}

		$templates = $this->conversion_templates_option();
		$list      = isset( $templates[ $type ] ) && is_array( $templates[ $type ] ) ? $templates[ $type ] : array();

		$target = null;
		foreach ( $list as $item ) {
			if ( isset( $item['id'] ) && (string) $item['id'] === $id ) {
				$target = $item;
				break;
			}
		}

		if ( ! $target ) {
			wp_send_json_error( array( 'message' => __( 'Plantilla no encontrada.', 'logistica' ) ), 404 );
		}

		$result = Logistica_Option_Lifecycle::delete(
			'configuracion',
			'conversion_template',
			$type . ':' . $id,
			(string) ( $target['name'] ?? $target['label'] ?? $id ),
			array(
				'sheet_type' => $type,
				'template'   => $target,
			),
			function () use ( $type, $id, $templates, $list ) {
				$templates[ $type ] = array_values(
					array_filter(
						$list,
						static function ( $item ) use ( $id ) {
							return ! isset( $item['id'] ) || (string) $item['id'] !== $id;
						}
					)
				);
				update_option( 'logistica_conversion_templates', $templates, false );
			}
		);

		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ) );
		}

		wp_send_json_success( array( 'templates' => $this->conversion_templates_option() ) );
	}

	/**
	 * @return array<string, array<int, array<string, mixed>>>
	 */
	private function conversion_templates_option() {
		$saved = get_option( 'logistica_conversion_templates', array() );
		if ( ! is_array( $saved ) ) {
			$saved = array();
		}

		return $saved;
	}

	/**
	 * Returns shared conversion table state from the database.
	 *
	 * @return void
	 */
	public function get_conversion_tables() {
		if ( ! Logistica_Conversion_Tables::user_can_access() ) {
			wp_send_json_error( array( 'message' => __( 'Sin permisos.', 'logistica' ) ), 403 );
		}

		check_ajax_referer( Logistica_Conversion_Tables::NONCE, 'nonce' );

		wp_send_json_success(
			array(
				'state' => Logistica_Conversion_Tables::get_state(),
			)
		);
	}

	/**
	 * Persists shared conversion table state to the database.
	 *
	 * @return void
	 */
	public function save_conversion_tables() {
		if ( ! Logistica_Conversion_Tables::user_can_access() ) {
			wp_send_json_error( array( 'message' => __( 'Sin permisos.', 'logistica' ) ), 403 );
		}

		check_ajax_referer( Logistica_Conversion_Tables::NONCE, 'nonce' );

		$raw   = isset( $_POST['state'] ) ? wp_unslash( $_POST['state'] ) : '';
		$state = json_decode( is_string( $raw ) ? $raw : '', true );
		if ( ! is_array( $state ) ) {
			wp_send_json_error( array( 'message' => __( 'Datos de tabla inválidos.', 'logistica' ) ), 400 );
		}

		if ( ! Logistica_Conversion_Tables::save_state( $state ) ) {
			wp_send_json_error( array( 'message' => __( 'No se pudo guardar en el servidor.', 'logistica' ) ), 500 );
		}

		wp_send_json_success(
			array(
				'message' => __( 'Guardado en el servidor.', 'logistica' ),
				'state'   => Logistica_Conversion_Tables::get_state(),
			)
		);
	}

	/**
	 * Gets order closing payload from JSON first to avoid max_input_vars truncation.
	 *
	 * @param bool $strict When true, invalid or empty JSON returns WP_Error (autosave / explicit save).
	 * @return array|\WP_Error
	 */
	private function get_cierre_payload( $strict = false ) {
		require_once LOGISTICA_PLUGIN_DIR . 'admin/views/partials.php';

		$json = isset( $_POST['cierre_json'] ) ? wp_unslash( $_POST['cierre_json'] ) : '';

		if ( is_string( $json ) && '' !== trim( $json ) ) {
			$decoded = Logistica_Order_Closing::parse_cierre_json( $json );

			if ( is_wp_error( $decoded ) ) {
				return $strict ? $decoded : Logistica_Order_Closing::get_config();
			}

			return $decoded;
		}

		if ( isset( $_POST['cierre'] ) && is_array( $_POST['cierre'] ) ) {
			return wp_unslash( $_POST['cierre'] );
		}

		if ( $strict ) {
			return new WP_Error(
				'logistica_cierre_empty',
				__( 'Sin datos para guardar. Recarga la página e intenta de nuevo.', 'logistica' )
			);
		}

		// Sin payload util: conservar datos actuales (compatibilidad).
		return Logistica_Order_Closing::get_config();
	}

	/**
	 * Exports order closing as Excel-compatible file.
	 *
	 * @return void
	 */
	public function export_cierre_excel() {
		$this->verify_request( 'logistica_export_cierre_excel' );
		Logistica_Order_Closing::download_excel();
	}

	/**
	 * Imports bulk Excel template and updates order closing config.
	 *
	 * @return void
	 */
	public function import_cierre_excel() {
		$this->verify_request( 'logistica_import_cierre_excel' );

		$result = Logistica_Order_Closing_Bulk_Excel::import_upload( $_FILES['cierre_excel_file'] ?? array() );

		set_transient(
			'logistica_cierre_import_' . get_current_user_id(),
			$result,
			MINUTE_IN_SECONDS
		);

		$status = ! empty( $result['error'] ) ? 'import_error' : 'imported';
		$this->redirect_import_status( $status );
	}

	/**
	 * Exports characteristics Excel template.
	 *
	 * @return void
	 */
	public function export_cierre_caracteristicas() {
		$this->verify_request( 'logistica_export_cierre_caracteristicas' );
		Logistica_Order_Closing_Characteristics_Excel::download_template();
	}

	/**
	 * Imports characteristics from Excel template.
	 *
	 * @return void
	 */
	public function import_cierre_caracteristicas() {
		$this->verify_request( 'logistica_import_cierre_caracteristicas' );

		$result = Logistica_Order_Closing_Characteristics_Excel::import_upload( $_FILES['cierre_caracteristicas_file'] ?? array() );

		set_transient(
			'logistica_cierre_caracteristicas_import_' . get_current_user_id(),
			$result,
			MINUTE_IN_SECONDS
		);

		$status = ! empty( $result['error'] ) ? 'import_error' : 'imported';
		$this->redirect_import_status( $status );
	}

	/**
	 * Exports warehouse inventory Excel template.
	 *
	 * @return void
	 */
	public function export_inventario_bodega() {
		$this->verify_request( 'logistica_export_inventario_bodega' );
		Logistica_Warehouse_Inventory_Excel::download_template();
	}

	/**
	 * Imports warehouse inventory from Excel template.
	 *
	 * @return void
	 */
	public function import_inventario_bodega() {
		$this->verify_request( 'logistica_import_inventario_bodega' );

		$result = Logistica_Warehouse_Inventory_Excel::import_upload( $_FILES['inventario_bodega_file'] ?? array() );

		set_transient(
			'logistica_warehouse_import_' . get_current_user_id(),
			$result,
			MINUTE_IN_SECONDS
		);

		$status = ! empty( $result['error'] ) ? 'import_error' : 'imported';
		$this->redirect_import_status( $status );
	}

	/**
	 * Exporta minutas a Excel.
	 *
	 * @return void
	 */
	public function export_minutas() {
		$this->verify_request( 'logistica_export_minutas' );
		Logistica_Minutas_Excel::download_template();
	}

	/**
	 * Importa minutas desde Excel/CSV.
	 *
	 * @return void
	 */
	public function import_minutas() {
		$this->verify_request( 'logistica_import_minutas' );

		$result = Logistica_Minutas_Excel::import_upload( $_FILES['minutas_file'] ?? array() );

		set_transient(
			'logistica_minutas_import_' . get_current_user_id(),
			$result,
			MINUTE_IN_SECONDS
		);

		$status = ! empty( $result['error'] ) ? 'import_error' : 'imported';
		$this->redirect_import_status( $status );
	}

	/**
	 * Exporta órdenes de compra de un estilo del cuadro de pedidos.
	 *
	 * @return void
	 */
	public function export_pedidos_ordenes() {
		$this->verify_request( 'logistica_export_pedidos_ordenes' );
		$board_slug = isset( $_GET['board_slug'] ) ? sanitize_title( wp_unslash( $_GET['board_slug'] ) ) : '';
		Logistica_Pedidos_Board_Excel::download_template( $board_slug );
	}

	/**
	 * Exporta historial de líneas entregadas en un periodo.
	 *
	 * @return void
	 */
	public function export_pedidos_ordenes_historial() {
		$this->verify_request( 'logistica_export_pedidos_ordenes_historial' );
		$board_slug = isset( $_GET['board_slug'] ) ? sanitize_title( wp_unslash( $_GET['board_slug'] ) ) : '';
		$date_from  = isset( $_GET['desde'] ) ? sanitize_text_field( wp_unslash( $_GET['desde'] ) ) : '';
		$date_to    = isset( $_GET['hasta'] ) ? sanitize_text_field( wp_unslash( $_GET['hasta'] ) ) : '';
		Logistica_Pedidos_Board_Excel::download_historial( $board_slug, $date_from, $date_to );
	}

	/**
	 * Importa órdenes de compra desde Excel/CSV para un estilo del cuadro.
	 *
	 * @return void
	 */
	public function import_pedidos_ordenes() {
		$this->verify_request( 'logistica_import_pedidos_ordenes' );

		$board_slug = isset( $_POST['board_slug'] ) ? sanitize_title( wp_unslash( $_POST['board_slug'] ) ) : '';
		$result     = Logistica_Pedidos_Board_Excel::import_upload(
			$board_slug,
			$_FILES['pedidos_ordenes_file'] ?? array()
		);

		set_transient(
			'logistica_pedidos_ordenes_import_' . get_current_user_id(),
			$result,
			MINUTE_IN_SECONDS
		);

		$status = ! empty( $result['error'] ) ? 'import_error' : 'imported';
		$this->redirect_import_status( $status );
	}

	/**
	 * AJAX: guarda estado de revisión de una minuta.
	 *
	 * @return void
	 */
	public function save_minuta_revision() {
		$this->verify_ajax( 'logistica_minutas' );

		$row_id   = isset( $_POST['row_id'] ) ? (int) wp_unslash( $_POST['row_id'] ) : 0;
		$revision = isset( $_POST['revision'] ) ? sanitize_text_field( wp_unslash( $_POST['revision'] ) ) : '';

		$result = Logistica_Minutas::update_revision( $row_id, $revision );
		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ), 400 );
		}

		wp_send_json_success(
			array(
				'revision' => $revision,
			)
		);
	}

	/**
	 * AJAX: guarda inventario fisico desde el frontend.
	 *
	 * @return void
	 */
	public function save_warehouse_fisico() {
		$this->verify_ajax( 'logistica_warehouse_inventory' );

		$row_id = isset( $_POST['row_id'] ) ? (int) wp_unslash( $_POST['row_id'] ) : 0;
		$fisico = isset( $_POST['inventario_fisico'] ) ? (float) wp_unslash( $_POST['inventario_fisico'] ) : 0;

		$result = Logistica_Warehouse_Inventory::update_inventario_fisico( $row_id, $fisico );
		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ), 400 );
		}

		$row = Logistica_Warehouse_Inventory::get_by_id( $row_id );
		wp_send_json_success( array( 'row' => $row ) );
	}

	/**
	 * AJAX: consulta paginada de inventario bodega.
	 *
	 * @return void
	 */
	public function query_warehouse_rows() {
		$this->verify_ajax( 'logistica_warehouse_inventory' );

		$result = Logistica_Warehouse_Inventory::query(
			array(
				'search'      => isset( $_POST['search'] ) ? sanitize_text_field( wp_unslash( $_POST['search'] ) ) : '',
				'descripcion' => isset( $_POST['descripcion'] ) ? sanitize_text_field( wp_unslash( $_POST['descripcion'] ) ) : '',
				'clase'       => isset( $_POST['clase'] ) ? sanitize_text_field( wp_unslash( $_POST['clase'] ) ) : '',
				'status'      => isset( $_POST['status'] ) ? sanitize_key( wp_unslash( $_POST['status'] ) ) : '',
				'saldo'       => isset( $_POST['saldo'] ) ? sanitize_key( wp_unslash( $_POST['saldo'] ) ) : '',
				'sort'        => isset( $_POST['sort'] ) ? sanitize_key( wp_unslash( $_POST['sort'] ) ) : 'codigo',
				'dir'         => isset( $_POST['dir'] ) ? sanitize_key( wp_unslash( $_POST['dir'] ) ) : 'asc',
				'page'        => isset( $_POST['page'] ) ? (int) wp_unslash( $_POST['page'] ) : 1,
				'per_page'    => isset( $_POST['per_page'] ) ? (int) wp_unslash( $_POST['per_page'] ) : 25,
			)
		);

		$payload = array(
			'rows'        => $result['rows'],
			'total'       => $result['total'],
			'page'        => $result['page'],
			'per_page'    => $result['per_page'],
			'total_pages' => $result['total_pages'],
		);

		$include_stats = ! isset( $_POST['include_stats'] ) || '0' !== (string) wp_unslash( $_POST['include_stats'] );
		if ( $include_stats ) {
			$payload['stats'] = Logistica_Warehouse_Inventory::get_stats();
		}

		wp_send_json_success( $payload );
	}

	/**
	 * AJAX: obtiene fila completa para editar.
	 *
	 * @return void
	 */
	public function get_warehouse_row() {
		$this->verify_ajax( 'logistica_warehouse_inventory' );

		$row_id = isset( $_POST['row_id'] ) ? (int) wp_unslash( $_POST['row_id'] ) : 0;
		$row    = Logistica_Warehouse_Inventory::get_by_id( $row_id );

		if ( ! $row ) {
			wp_send_json_error( array( 'message' => __( 'Registro no encontrado.', 'logistica' ) ), 404 );
		}

		wp_send_json_success( array( 'row' => $row ) );
	}

	/**
	 * AJAX: guarda configuracion completa de una fila.
	 *
	 * @return void
	 */
	public function save_warehouse_row() {
		$this->verify_ajax( 'logistica_warehouse_inventory' );

		$row_id = isset( $_POST['row_id'] ) ? (int) wp_unslash( $_POST['row_id'] ) : 0;
		if ( $row_id <= 0 && isset( $_POST['codigo'] ) ) {
			$legacy = Logistica_Warehouse_Inventory::get_by_codigo( sanitize_text_field( wp_unslash( $_POST['codigo'] ) ) );
			$row_id = $legacy ? (int) ( $legacy['id'] ?? 0 ) : 0;
		}
		$row    = Logistica_Warehouse_Inventory::get_by_id( $row_id );

		if ( ! $row ) {
			wp_send_json_error( array( 'message' => __( 'Registro no encontrado.', 'logistica' ) ), 404 );
		}

		$data = $row;
		foreach ( array_keys( Logistica_Warehouse_Inventory::form_edit_fields() ) as $field ) {
			if ( isset( $_POST[ $field ] ) ) {
				$data[ $field ] = wp_unslash( $_POST[ $field ] );
			}
		}

		$lote_antes = (string) ( $row['lote'] ?? '' );
		$codigo     = (string) ( $row['codigo'] ?? '' );
		$result     = Logistica_Warehouse_Inventory::save_row( $data );
		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ), 400 );
		}

		$saved_row    = Logistica_Warehouse_Inventory::get_by_id( (int) $result );
		$lote_despues = (string) ( $saved_row['lote'] ?? '' );
		$bases_synced = 0;

		if ( ! empty( $saved_row['es_lote_activo'] ) && $lote_antes !== $lote_despues ) {
			$bases_synced = Logistica_Order_Closing::sync_lote_by_codigo( $codigo, $lote_despues );
		}

		$active = Logistica_Warehouse_Inventory::get_active_row_for_codigo( $codigo );

		wp_send_json_success(
			array(
				'row'            => $saved_row,
				'lote_changed'   => $lote_antes !== $lote_despues,
				'bases_synced'   => $bases_synced,
				'vigente_row_id' => $active ? (int) ( $active['id'] ?? 0 ) : 0,
			)
		);
	}

	/**
	 * AJAX: elimina un codigo del inventario bodega.
	 *
	 * @return void
	 */
	public function warehouse_delete_row() {
		$this->verify_ajax( 'logistica_warehouse_inventory' );

		$row_id = isset( $_POST['row_id'] ) ? (int) wp_unslash( $_POST['row_id'] ) : 0;
		$row    = Logistica_Warehouse_Inventory::get_by_id( $row_id );
		$codigo = $row ? (string) ( $row['codigo'] ?? '' ) : '';
		$result = Logistica_Warehouse_Inventory::delete_row( $row_id );

		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ), 400 );
		}

		$active = '' !== $codigo ? Logistica_Warehouse_Inventory::get_active_row_for_codigo( $codigo ) : null;

		wp_send_json_success(
			array(
				'row_id'         => $row_id,
				'codigo'         => $codigo,
				'vigente_row_id' => $active ? (int) ( $active['id'] ?? 0 ) : 0,
			)
		);
	}

	/**
	 * AJAX: duplica un codigo del inventario bodega.
	 *
	 * @return void
	 */
	public function warehouse_duplicate_row() {
		$this->verify_ajax( 'logistica_warehouse_inventory' );

		$row_id    = isset( $_POST['row_id'] ) ? (int) wp_unslash( $_POST['row_id'] ) : 0;
		$new_lote  = isset( $_POST['new_lote'] ) ? sanitize_text_field( wp_unslash( $_POST['new_lote'] ) ) : '';
		$new_codigo = isset( $_POST['new_codigo'] ) ? sanitize_text_field( wp_unslash( $_POST['new_codigo'] ) ) : '';
		$result    = Logistica_Warehouse_Inventory::duplicate_row( $row_id, $new_lote, $new_codigo );

		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ), 400 );
		}

		$codigo = (string) ( $result['codigo'] ?? '' );
		$active = '' !== $codigo ? Logistica_Warehouse_Inventory::get_active_row_for_codigo( $codigo ) : null;

		wp_send_json_success(
			array(
				'row'            => $result,
				'vigente_row_id' => $active ? (int) ( $active['id'] ?? 0 ) : 0,
			)
		);
	}

	/**
	 * AJAX: activa o desactiva un registro de inventario bodega.
	 *
	 * @return void
	 */
	public function warehouse_toggle_estado() {
		$this->verify_ajax( 'logistica_warehouse_inventory' );

		$row_id = isset( $_POST['row_id'] ) ? (int) wp_unslash( $_POST['row_id'] ) : 0;
		$estado = isset( $_POST['estado'] ) ? sanitize_key( wp_unslash( $_POST['estado'] ) ) : '';
		$row    = Logistica_Warehouse_Inventory::get_by_id( $row_id );

		if ( ! $row ) {
			wp_send_json_error( array( 'message' => __( 'Registro no encontrado.', 'logistica' ) ), 404 );
		}

		if ( ! in_array( $estado, array( 'activo', 'inactivo' ), true ) ) {
			$estado = ( 'activo' === ( $row['estado'] ?? 'activo' ) ) ? 'inactivo' : 'activo';
		}

		$result = Logistica_Warehouse_Inventory::set_row_estado( $row_id, $estado );
		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ), 400 );
		}

		$active = Logistica_Warehouse_Inventory::get_active_row_for_codigo( (string) ( $result['codigo'] ?? '' ) );

		wp_send_json_success(
			array(
				'row'             => $result,
				'vigente_row_id'  => $active ? (int) ( $active['id'] ?? 0 ) : 0,
			)
		);
	}

	/**
	 * AJAX: alta manual de codigo en inventario bodega.
	 *
	 * @return void
	 */
	public function warehouse_add_row() {
		$this->verify_ajax( 'logistica_warehouse_inventory' );

		$codigo = isset( $_POST['codigo'] ) ? sanitize_text_field( wp_unslash( $_POST['codigo'] ) ) : '';
		$data   = array(
			'codigo' => $codigo,
		);

		foreach ( array_keys( Logistica_Warehouse_Inventory::form_edit_fields() ) as $field ) {
			if ( isset( $_POST[ $field ] ) ) {
				$data[ $field ] = wp_unslash( $_POST[ $field ] );
			}
		}

		$result = Logistica_Warehouse_Inventory::save_row( $data );
		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ), 400 );
		}

		$row    = Logistica_Warehouse_Inventory::get_by_id( (int) $result );
		$active = Logistica_Warehouse_Inventory::get_active_row_for_codigo( $codigo );

		wp_send_json_success(
			array(
				'row'            => $row,
				'vigente_row_id' => $active ? (int) ( $active['id'] ?? 0 ) : 0,
			)
		);
	}

	/**
	 * AJAX: lote activo por codigo (maestro inventario bodega).
	 *
	 * @return void
	 */
	public function get_warehouse_lote() {
		$this->verify_ajax( 'logistica_warehouse_inventory' );

		$codigo = isset( $_POST['codigo'] ) ? sanitize_text_field( wp_unslash( $_POST['codigo'] ) ) : '';
		$row    = Logistica_Warehouse_Inventory::get_by_codigo( $codigo );

		if ( ! $row ) {
			wp_send_json_success( array( 'found' => false, 'lote' => '' ) );
		}

		wp_send_json_success(
			array(
				'found' => true,
				'lote'  => (string) ( $row['lote'] ?? '' ),
			)
		);
	}

	/**
	 * AJAX: mapa codigo => lote activo (inventario bodega).
	 *
	 * @return void
	 */
	public function get_warehouse_lote_map() {
		$this->verify_ajax( 'logistica_warehouse_inventory' );

		wp_send_json_success(
			array(
				'map' => Logistica_Warehouse_Inventory::lote_map(),
			)
		);
	}

	/**
	 * AJAX: actualiza lote maestro desde cierre y sincroniza bases.
	 *
	 * @return void
	 */
	public function update_warehouse_lote_from_cierre() {
		$this->verify_ajax( 'logistica_warehouse_inventory' );

		$codigo = isset( $_POST['codigo'] ) ? sanitize_text_field( wp_unslash( $_POST['codigo'] ) ) : '';
		$lote   = isset( $_POST['lote'] ) ? sanitize_text_field( wp_unslash( $_POST['lote'] ) ) : '';

		$result = Logistica_Warehouse_Inventory::update_lote_from_cierre( $codigo, $lote );
		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ), 400 );
		}

		wp_send_json_success( array( 'synced' => true ) );
	}

	/**
	 * Guarda caracteristica por codigo (cierre de ordenes).
	 *
	 * @return void
	 */
	public function save_cierre_caracteristica() {
		$this->verify_request( 'logistica_save_cierre_caracteristica' );

		$redirect = isset( $_POST['redirect_to'] ) ? esc_url_raw( wp_unslash( $_POST['redirect_to'] ) ) : '';
		if ( ! $redirect || ! wp_validate_redirect( $redirect, false ) ) {
			require_once LOGISTICA_PLUGIN_DIR . 'admin/views/partials.php';
			$redirect = logistica_tab_url( 'logistica-cierre-caracteristicas', 'admin' );
		}

		$result = Logistica_Order_Closing_Characteristics::save_row( wp_unslash( $_POST ) );
		if ( is_wp_error( $result ) ) {
			set_transient(
				'logistica_cierre_caracteristica_error_' . get_current_user_id(),
				$result->get_error_message(),
				60
			);
			wp_safe_redirect( add_query_arg( 'logistica_status', 'caracteristica_error', $redirect ) );
			exit;
		}

		wp_safe_redirect( add_query_arg( 'logistica_status', 'caracteristica_saved', $redirect ) );
		exit;
	}

	/**
	 * Elimina caracteristica por codigo.
	 *
	 * @return void
	 */
	public function delete_cierre_caracteristica() {
		$this->verify_request( 'logistica_delete_cierre_caracteristica' );

		$row_id = isset( $_POST['row_id'] ) ? (int) wp_unslash( $_POST['row_id'] ) : 0;
		$redirect = isset( $_POST['redirect_to'] ) ? esc_url_raw( wp_unslash( $_POST['redirect_to'] ) ) : '';
		if ( ! $redirect || ! wp_validate_redirect( $redirect, false ) ) {
			require_once LOGISTICA_PLUGIN_DIR . 'admin/views/partials.php';
			$redirect = logistica_tab_url( 'logistica-cierre-caracteristicas', 'admin' );
		}

		$result = Logistica_Order_Closing_Characteristics::delete_row( $row_id );
		if ( is_wp_error( $result ) ) {
			set_transient(
				'logistica_cierre_caracteristica_error_' . get_current_user_id(),
				$result->get_error_message(),
				60
			);
			wp_safe_redirect( add_query_arg( 'logistica_status', 'caracteristica_error', $redirect ) );
			exit;
		}

		wp_safe_redirect( add_query_arg( 'logistica_status', 'caracteristica_deleted', $redirect ) );
		exit;
	}

	/**
	 * AJAX: caracteristicas por codigo.
	 *
	 * @return void
	 */
	public function get_cierre_caracteristica() {
		$this->verify_ajax( 'logistica_cierre_ordenes' );

		$codigo = isset( $_POST['codigo'] ) ? sanitize_text_field( wp_unslash( $_POST['codigo'] ) ) : '';
		$row    = Logistica_Order_Closing_Characteristics::get_by_codigo( $codigo );

		if ( ! $row ) {
			wp_send_json_success( array( 'found' => false ) );
		}

		wp_send_json_success(
			array(
				'found' => true,
				'row'   => array(
					'largo' => (string) ( $row['largo'] ?? '' ),
					'ancho' => (string) ( $row['ancho'] ?? '' ),
					'alto'  => (string) ( $row['alto'] ?? '' ),
					'clave' => (string) ( $row['clave'] ?? '' ),
				),
			)
		);
	}

	/**
	 * AJAX: mapa completo codigo => caracteristicas.
	 *
	 * @return void
	 */
	public function get_cierre_caracteristicas_map() {
		$this->verify_ajax( 'logistica_cierre_ordenes' );

		wp_send_json_success(
			array(
				'map' => Logistica_Order_Closing_Characteristics::codigo_map(),
			)
		);
	}

	/**
	 * AJAX: obtiene una fila de características por ID.
	 *
	 * @return void
	 */
	public function get_cierre_caracteristica_row() {
		$this->verify_ajax( 'logistica_cierre_caracteristicas' );

		$row_id = isset( $_POST['row_id'] ) ? (int) wp_unslash( $_POST['row_id'] ) : 0;
		$row    = Logistica_Order_Closing_Characteristics::get_by_id( $row_id );

		if ( ! $row ) {
			wp_send_json_error( array( 'message' => __( 'Registro no encontrado.', 'logistica' ) ), 404 );
		}

		wp_send_json_success(
			array(
				'row' => Logistica_Order_Closing_Characteristics::format_row_for_client( $row ),
			)
		);
	}

	/**
	 * AJAX: guarda característica por código sin recargar la página.
	 *
	 * @return void
	 */
	public function ajax_save_cierre_caracteristica() {
		$this->verify_ajax( 'logistica_cierre_caracteristicas' );

		$result = Logistica_Order_Closing_Characteristics::save_row( wp_unslash( $_POST ) );
		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ), 400 );
		}

		$row = Logistica_Order_Closing_Characteristics::get_by_id( (int) $result );
		if ( ! $row ) {
			$row = Logistica_Order_Closing_Characteristics::get_by_codigo( sanitize_text_field( wp_unslash( $_POST['codigo'] ?? '' ) ) );
		}

		if ( ! $row ) {
			wp_send_json_error( array( 'message' => __( 'No se pudo leer el registro guardado.', 'logistica' ) ), 500 );
		}

		wp_send_json_success(
			array(
				'row'     => Logistica_Order_Closing_Characteristics::format_row_for_client( $row ),
				'message' => __( 'Característica guardada.', 'logistica' ),
				'total'   => count( Logistica_Order_Closing_Characteristics::get_all() ),
			)
		);
	}

	/**
	 * AJAX: elimina característica por código sin recargar la página.
	 *
	 * @return void
	 */
	public function ajax_delete_cierre_caracteristica() {
		$this->verify_ajax( 'logistica_cierre_caracteristicas' );

		$row_id = isset( $_POST['row_id'] ) ? (int) wp_unslash( $_POST['row_id'] ) : 0;
		$result = Logistica_Order_Closing_Characteristics::delete_row( $row_id );

		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ), 400 );
		}

		wp_send_json_success(
			array(
				'row_id'  => $row_id,
				'message' => __( 'Característica eliminada.', 'logistica' ),
				'total'   => count( Logistica_Order_Closing_Characteristics::get_all() ),
			)
		);
	}

	/**
	 * Exporta planeación de impresión a Excel personalizado.
	 *
	 * @return void
	 */
	public function export_planeacion_impresion() {
		$this->verify_request( 'logistica_export_planeacion_impresion' );

		$week    = isset( $_GET['semana_inicio'] ) ? sanitize_text_field( wp_unslash( $_GET['semana_inicio'] ) ) : '';
		$machine = isset( $_GET['maquina'] ) ? sanitize_key( wp_unslash( $_GET['maquina'] ) ) : 'sm74';

		Logistica_Print_Planning_Excel::download( $week, $machine );
	}

	/**
	 * AJAX: obtiene planeación de impresión.
	 *
	 * @return void
	 */
	public function get_planeacion_impresion() {
		$this->verify_ajax( 'logistica_planeacion_impresion' );

		$week    = isset( $_POST['semana_inicio'] ) ? sanitize_text_field( wp_unslash( $_POST['semana_inicio'] ) ) : '';
		$machine = isset( $_POST['maquina'] ) ? sanitize_key( wp_unslash( $_POST['maquina'] ) ) : 'sm74';

		$schedule = Logistica_Print_Planning::get_schedule( $week, $machine );

		wp_send_json_success(
			array(
				'schedule'             => $schedule,
				'days'                 => Logistica_Print_Planning::week_days( $schedule['semana_inicio'] ),
				'machines'             => Logistica_Print_Planning::get_machines(),
				'custom_machine_slugs' => array_keys( Logistica_Print_Planning::get_custom_machines() ),
				'field_options'        => Logistica_Print_Planning::get_field_options(),
				'statuses'             => Logistica_Print_Planning::get_statuses(),
				'labor_types'          => Logistica_Print_Planning::get_labor_types(),
				'labor_defaults'       => Logistica_Print_Planning::LABOR_DEFAULT_MINUTES,
				'labor_field_rules'    => Logistica_Print_Planning::labor_field_rules(),
				'day_types'            => Logistica_Print_Planning::day_types(),
				'holiday_settings'     => Logistica_Settings::get_panel_holidays_client_config(),
				'festivo_dates'        => $this->festivo_dates_for_week( $schedule['semana_inicio'] ?? $week ),
			)
		);
	}

	/**
	 * AJAX: comparación semana anterior vs actual (planeación impresión).
	 *
	 * @return void
	 */
	public function get_planeacion_impresion_metrics() {
		$this->verify_ajax( 'logistica_planeacion_impresion' );

		$week    = isset( $_POST['semana_inicio'] ) ? sanitize_text_field( wp_unslash( $_POST['semana_inicio'] ) ) : Logistica_Print_Planning::week_start();
		$machine = isset( $_POST['maquina'] ) ? sanitize_key( wp_unslash( $_POST['maquina'] ) ) : 'sm74';

		wp_send_json_success(
			Logistica_Planning_Metrics::get_week_comparison_summary(
				Logistica_Print_Planning::class,
				$week,
				$machine
			)
		);
	}

	/**
	 * AJAX: registra una máquina nueva para planeación independiente.
	 *
	 * @return void
	 */
	public function add_planeacion_machine() {
		$this->verify_ajax( 'logistica_planeacion_impresion' );

		$label = isset( $_POST['nombre'] ) ? sanitize_text_field( wp_unslash( $_POST['nombre'] ) ) : '';
		$slug  = Logistica_Print_Planning::add_custom_machine( $label );

		if ( is_wp_error( $slug ) ) {
			wp_send_json_error( array( 'message' => $slug->get_error_message() ), 400 );
		}

		wp_send_json_success(
			array(
				'slug'     => $slug,
				'label'    => Logistica_Print_Planning::get_machines()[ $slug ] ?? $label,
				'machines' => Logistica_Print_Planning::get_machines(),
				'custom_machine_slugs' => array_keys( Logistica_Print_Planning::get_custom_machines() ),
				'message'  => __( 'Máquina creada. Cada máquina tiene su propia programación semanal.', 'logistica' ),
			)
		);
	}

	/**
	 * AJAX: actualiza el nombre de una máquina.
	 *
	 * @return void
	 */
	public function update_planeacion_machine() {
		$this->verify_ajax( 'logistica_planeacion_impresion' );

		$slug  = isset( $_POST['maquina'] ) ? sanitize_key( wp_unslash( $_POST['maquina'] ) ) : '';
		$label = isset( $_POST['nombre'] ) ? sanitize_text_field( wp_unslash( $_POST['nombre'] ) ) : '';
		$result = Logistica_Print_Planning::update_machine_label( $slug, $label );

		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ), 400 );
		}

		wp_send_json_success(
			array(
				'slug'                 => $result,
				'label'                => Logistica_Print_Planning::get_machines()[ $result ] ?? $label,
				'machines'             => Logistica_Print_Planning::get_machines(),
				'custom_machine_slugs' => array_keys( Logistica_Print_Planning::get_custom_machines() ),
				'message'              => __( 'Máquina actualizada.', 'logistica' ),
			)
		);
	}

	/**
	 * AJAX: elimina una máquina personalizada.
	 *
	 * @return void
	 */
	public function delete_planeacion_machine() {
		$this->verify_ajax( 'logistica_planeacion_impresion' );

		$slug   = isset( $_POST['maquina'] ) ? sanitize_key( wp_unslash( $_POST['maquina'] ) ) : '';
		$result = Logistica_Print_Planning::delete_machine( $slug );

		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ), 400 );
		}

		wp_send_json_success(
			array(
				'machines'             => Logistica_Print_Planning::get_machines(),
				'custom_machine_slugs' => array_keys( Logistica_Print_Planning::get_custom_machines() ),
				'message'              => __( 'Máquina eliminada.', 'logistica' ),
			)
		);
	}

	/**
	 * AJAX: guarda opciones de listas desplegables de planeación.
	 *
	 * @return void
	 */
	public function save_planeacion_field_options() {
		$this->verify_ajax( 'logistica_planeacion_impresion' );

		$json    = isset( $_POST['options_json'] ) ? wp_unslash( $_POST['options_json'] ) : '';
		$payload = json_decode( is_string( $json ) ? $json : '', true );
		if ( ! is_array( $payload ) ) {
			wp_send_json_error( array( 'message' => __( 'Datos de opciones inválidos.', 'logistica' ) ), 400 );
		}

		$result = Logistica_Print_Planning::save_field_options( $payload );
		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ), 400 );
		}

		wp_send_json_success(
			array(
				'field_options' => $result,
				'message'       => __( 'Listas desplegables guardadas.', 'logistica' ),
			)
		);
	}

	/**
	 * AJAX: obtiene el panel personal del usuario.
	 *
	 * @return void
	 */
	public function get_user_panel() {
		$this->verify_ajax( 'logistica_user_panel' );

		$user_id = get_current_user_id();
		wp_send_json_success( Logistica_User_Panel::bootstrap( $user_id ) );
	}

	/**
	 * Descarga Excel de proveedores del panel personal.
	 *
	 * @return void
	 */
	public function export_panel_proveedores() {
		$this->verify_request( 'logistica_export_panel_proveedores' );
		$this->send_panel_proveedores_export();
	}

	/**
	 * AJAX: descarga Excel de proveedores del panel personal.
	 *
	 * @return void
	 */
	public function ajax_export_panel_proveedores() {
		$this->verify_ajax( 'logistica_user_panel' );
		$this->send_panel_proveedores_export();
	}

	/**
	 * Envía el archivo Excel/CSV de proveedores del panel.
	 *
	 * @return void
	 */
	private function send_panel_proveedores_export() {
		$panel = Logistica_User_Panel::get_panel( get_current_user_id() );
		$rows  = is_array( $panel['proveedores'] ?? null ) ? $panel['proveedores'] : array();

		Logistica_User_Panel_Proveedores_Excel::download( $rows );
	}

	/**
	 * AJAX: importa proveedores al panel personal.
	 *
	 * @return void
	 */
	public function import_panel_proveedores() {
		$this->verify_ajax( 'logistica_user_panel' );

		$rows = Logistica_User_Panel_Proveedores_Excel::parse_upload( $_FILES['proveedores_file'] ?? array() );
		if ( is_wp_error( $rows ) ) {
			wp_send_json_error( array( 'message' => $rows->get_error_message() ), 400 );
		}

		$result = Logistica_User_Panel::import_proveedores_rows( get_current_user_id(), $rows );
		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ), 400 );
		}

		wp_send_json_success(
			array_merge(
				Logistica_User_Panel::clock_context(),
				array(
					'panel'   => Logistica_User_Panel::get_panel( get_current_user_id() ),
					'message' => $result['message'],
				)
			)
		);
	}

	/**
	 * Descarga Excel de compras del panel personal.
	 *
	 * @return void
	 */
	public function export_panel_compras() {
		$this->verify_request( 'logistica_export_panel_compras' );
		$this->send_panel_compras_export();
	}

	/**
	 * AJAX: descarga Excel de compras del panel personal.
	 *
	 * @return void
	 */
	public function ajax_export_panel_compras() {
		$this->verify_ajax( 'logistica_user_panel' );
		$this->send_panel_compras_export();
	}

	/**
	 * Envía el archivo Excel/CSV de compras del panel.
	 *
	 * @return void
	 */
	private function send_panel_compras_export() {
		$panel = Logistica_User_Panel::get_panel( get_current_user_id() );
		$rows  = is_array( $panel['compras'] ?? null ) ? $panel['compras'] : array();
		$prov  = is_array( $panel['proveedores'] ?? null ) ? $panel['proveedores'] : array();

		Logistica_User_Panel_Compras_Excel::download( $rows, $prov );
	}

	/**
	 * AJAX: importa compras al panel personal.
	 *
	 * @return void
	 */
	public function import_panel_compras() {
		$this->verify_ajax( 'logistica_user_panel' );

		$rows = Logistica_User_Panel_Compras_Excel::parse_upload( $_FILES['compras_file'] ?? array() );
		if ( is_wp_error( $rows ) ) {
			wp_send_json_error( array( 'message' => $rows->get_error_message() ), 400 );
		}

		$result = Logistica_User_Panel::import_compras_rows( get_current_user_id(), $rows );
		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ), 400 );
		}

		wp_send_json_success(
			array_merge(
				Logistica_User_Panel::clock_context(),
				array(
					'panel'   => Logistica_User_Panel::get_panel( get_current_user_id() ),
					'message' => $result['message'],
				)
			)
		);
	}

	/**
	 * AJAX: guarda el panel personal del usuario.
	 *
	 * @return void
	 */
	public function save_user_panel() {
		$this->verify_ajax( 'logistica_user_panel' );

		$user_id = get_current_user_id();
		$json    = isset( $_POST['payload'] ) ? wp_unslash( $_POST['payload'] ) : '';
		$payload = json_decode( is_string( $json ) ? $json : '', true );

		if ( ! is_array( $payload ) ) {
			wp_send_json_error( array( 'message' => __( 'Datos del panel inválidos.', 'logistica' ) ), 400 );
		}

		$result = Logistica_User_Panel::save_panel( $user_id, $payload );
		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ), 400 );
		}

		wp_send_json_success(
			array_merge(
				Logistica_User_Panel::clock_context(),
				array(
					'panel'   => $result,
					'message' => __( 'Panel guardado.', 'logistica' ),
				)
			)
		);
	}

	/**
	 * AJAX: marca avisos del panel como leídos.
	 *
	 * @return void
	 */
	public function mark_panel_avisos_read() {
		$this->verify_ajax( 'logistica_user_panel' );

		$ids_raw = isset( $_POST['aviso_ids'] ) ? wp_unslash( $_POST['aviso_ids'] ) : 'all';
		$ids     = 'all';
		if ( is_string( $ids_raw ) && 'all' !== $ids_raw ) {
			$decoded = json_decode( $ids_raw, true );
			$ids     = is_array( $decoded ) ? $decoded : array( $ids_raw );
		} elseif ( is_array( $ids_raw ) ) {
			$ids = $ids_raw;
		}

		$result = Logistica_User_Panel::mark_avisos_read( get_current_user_id(), $ids );
		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ), 400 );
		}

		wp_send_json_success(
			array(
				'panel'   => $result,
				'message' => __( 'Avisos actualizados.', 'logistica' ),
			)
		);
	}

	/**
	 * AJAX: elimina avisos del panel.
	 *
	 * @return void
	 */
	public function delete_panel_avisos() {
		$this->verify_ajax( 'logistica_user_panel' );

		$ids_raw = isset( $_POST['aviso_ids'] ) ? wp_unslash( $_POST['aviso_ids'] ) : '';
		$ids     = array();
		if ( is_string( $ids_raw ) && in_array( $ids_raw, array( 'all', 'read' ), true ) ) {
			$ids = $ids_raw;
		} elseif ( is_string( $ids_raw ) ) {
			$decoded = json_decode( $ids_raw, true );
			$ids     = is_array( $decoded ) ? $decoded : array( $ids_raw );
		} elseif ( is_array( $ids_raw ) ) {
			$ids = $ids_raw;
		}

		$result = Logistica_User_Panel::delete_avisos( get_current_user_id(), $ids );
		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ), 400 );
		}

		$message = __( 'Aviso eliminado.', 'logistica' );
		if ( 'read' === $ids ) {
			$message = __( 'Avisos leídos eliminados.', 'logistica' );
		} elseif ( 'all' === $ids ) {
			$message = __( 'Todos los avisos eliminados.', 'logistica' );
		}

		wp_send_json_success(
			array(
				'panel'   => $result,
				'message' => $message,
			)
		);
	}

	/**
	 * AJAX: envía un aviso del panel por correo.
	 *
	 * @return void
	 */
	public function send_panel_aviso_email() {
		$this->verify_ajax( 'logistica_user_panel' );

		$aviso_id = isset( $_POST['aviso_id'] ) ? sanitize_key( wp_unslash( (string) $_POST['aviso_id'] ) ) : '';
		$result   = Logistica_User_Panel::send_aviso_email( get_current_user_id(), $aviso_id );
		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ), 400 );
		}

		wp_send_json_success(
			array(
				'panel'   => $result,
				'message' => __( 'Aviso enviado por correo.', 'logistica' ),
			)
		);
	}

	/**
	 * AJAX: registra automáticamente un punto de retoma al salir de una sección.
	 *
	 * @return void
	 */
	public function record_resume_point() {
		$this->verify_ajax( 'logistica_user_panel' );

		$user_id  = get_current_user_id();
		$tab_slug  = isset( $_POST['tab_slug'] ) ? sanitize_key( wp_unslash( $_POST['tab_slug'] ) ) : '';
		$title     = isset( $_POST['title'] ) ? sanitize_text_field( wp_unslash( $_POST['title'] ) ) : '';
		$context   = isset( $_POST['context'] ) ? sanitize_textarea_field( wp_unslash( $_POST['context'] ) ) : '';
		$deep_link = isset( $_POST['deep_link'] ) ? sanitize_text_field( wp_unslash( $_POST['deep_link'] ) ) : '';

		$result = Logistica_User_Panel::record_resume_point( $user_id, $tab_slug, $title, $context, $deep_link );
		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ), 400 );
		}

		wp_send_json_success(
			array(
				'entry'         => $result,
				'resume_points' => Logistica_User_Panel::get_panel( $user_id )['resume_points'] ?? array(),
			)
		);
	}

	/**
	 * AJAX: quita un punto de retoma de la lista.
	 *
	 * @return void
	 */
	public function dismiss_resume_point() {
		$this->verify_ajax( 'logistica_user_panel' );

		$user_id  = get_current_user_id();
		$point_id = isset( $_POST['point_id'] ) ? sanitize_text_field( wp_unslash( $_POST['point_id'] ) ) : '';

		$result = Logistica_User_Panel::remove_resume_point( $user_id, $point_id );
		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ), 400 );
		}

		wp_send_json_success(
			array(
				'resume_points' => $result,
				'message'       => __( 'Registro eliminado.', 'logistica' ),
			)
		);
	}

	/**
	 * AJAX: guarda los accesos rápidos personalizados del usuario.
	 *
	 * @return void
	 */
	public function save_quick_links() {
		$this->verify_ajax( 'logistica_user_panel' );

		$user_id = get_current_user_id();
		$json    = isset( $_POST['slugs'] ) ? wp_unslash( $_POST['slugs'] ) : '';
		$slugs   = json_decode( is_string( $json ) ? $json : '', true );

		if ( ! is_array( $slugs ) ) {
			wp_send_json_error( array( 'message' => __( 'Datos de accesos rápidos inválidos.', 'logistica' ) ), 400 );
		}

		$result = Logistica_User_Panel::save_quick_links( $user_id, $slugs );
		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ), 400 );
		}

		wp_send_json_success(
			array(
				'quick_links' => $result,
				'message'     => __( 'Accesos rápidos guardados.', 'logistica' ),
			)
		);
	}

	/**
	 * AJAX: guarda planeación de impresión.
	 *
	 * @return void
	 */
	public function save_planeacion_impresion() {
		$this->verify_ajax( 'logistica_planeacion_impresion' );

		$week    = isset( $_POST['semana_inicio'] ) ? sanitize_text_field( wp_unslash( $_POST['semana_inicio'] ) ) : '';
		$machine = isset( $_POST['maquina'] ) ? sanitize_key( wp_unslash( $_POST['maquina'] ) ) : 'sm74';
		$json    = isset( $_POST['schedule_json'] ) ? wp_unslash( $_POST['schedule_json'] ) : '';

		$payload = json_decode( is_string( $json ) ? $json : '', true );
		if ( ! is_array( $payload ) ) {
			wp_send_json_error( array( 'message' => __( 'Datos de planeación inválidos.', 'logistica' ) ), 400 );
		}

		$result = Logistica_Print_Planning::save_schedule( $week, $machine, $payload );
		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ), 400 );
		}

		wp_send_json_success(
			array(
				'message'  => __( 'Planeación guardada.', 'logistica' ),
				'schedule' => $result,
				'time'     => Logistica_User_Panel::current_time_hms(),
			)
		);
	}

	/**
	 * AJAX: importa planeación de impresión desde Excel.
	 *
	 * @return void
	 */
	public function import_planeacion_impresion() {
		$this->verify_ajax( 'logistica_planeacion_impresion' );

		$week    = isset( $_POST['semana_inicio'] ) ? sanitize_text_field( wp_unslash( $_POST['semana_inicio'] ) ) : '';
		$machine = isset( $_POST['maquina'] ) ? sanitize_key( wp_unslash( $_POST['maquina'] ) ) : 'sm74';
		$file    = isset( $_FILES['excel_file'] ) ? $_FILES['excel_file'] : null; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized

		$import = Logistica_Print_Planning_Excel::import_upload( $file );
		if ( is_wp_error( $import ) ) {
			wp_send_json_error( array( 'message' => $import->get_error_message() ), 400 );
		}

		$schedule = Logistica_Print_Planning::get_schedule( $week, $machine );
		$payload  = array(
			'jornada_inicio' => (int) ( $schedule['jornada_inicio'] ?? 420 ),
			'jornada_fin'    => (int) ( $schedule['jornada_fin'] ?? 1020 ),
			'day_hours'      => $schedule['day_hours'] ?? array(),
			'include_lunch'  => ! empty( $schedule['include_lunch'] ),
			'tasks'          => $import['tasks'],
		);

		$result = Logistica_Print_Planning::save_schedule( $week, $machine, $payload );
		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ), 400 );
		}

		wp_send_json_success(
			array(
				'message'  => sprintf(
					/* translators: %d: number of imported tasks */
					__( 'Importadas %d actividad(es).', 'logistica' ),
					(int) ( $import['count'] ?? 0 )
				),
				'schedule' => $result,
				'count'    => (int) ( $import['count'] ?? 0 ),
				'time'     => Logistica_User_Panel::current_time_hms(),
			)
		);
	}

	/**
	 * Exporta planeación de formado a Excel personalizado.
	 *
	 * @return void
	 */
	public function export_planeacion_formado() {
		$this->verify_request( 'logistica_export_planeacion_formado' );

		$week    = isset( $_GET['semana_inicio'] ) ? sanitize_text_field( wp_unslash( $_GET['semana_inicio'] ) ) : '';
		$machine = isset( $_GET['maquina'] ) ? sanitize_key( wp_unslash( $_GET['maquina'] ) ) : Logistica_Forming_Planning::default_machine_slug();

		Logistica_Forming_Planning_Excel::download( $week, $machine );
	}

	/**
	 * AJAX: obtiene planeación de formado.
	 *
	 * @return void
	 */
	public function get_planeacion_formado() {
		$this->verify_ajax( 'logistica_planeacion_formado' );

		$week    = isset( $_POST['semana_inicio'] ) ? sanitize_text_field( wp_unslash( $_POST['semana_inicio'] ) ) : '';
		$machine = isset( $_POST['maquina'] ) ? sanitize_key( wp_unslash( $_POST['maquina'] ) ) : Logistica_Forming_Planning::default_machine_slug();

		$schedule = Logistica_Forming_Planning::get_schedule( $week, $machine );

		wp_send_json_success(
			array(
				'schedule'             => $schedule,
				'days'                 => Logistica_Forming_Planning::week_days( $schedule['semana_inicio'] ),
				'machines'             => Logistica_Forming_Planning::get_machines(),
				'custom_machine_slugs' => array_keys( Logistica_Forming_Planning::get_custom_machines() ),
				'field_options'        => Logistica_Forming_Planning::get_field_options(),
				'statuses'             => Logistica_Forming_Planning::get_statuses(),
				'labor_types'          => Logistica_Forming_Planning::get_labor_types(),
				'labor_defaults'       => Logistica_Forming_Planning::LABOR_DEFAULT_MINUTES,
				'labor_field_rules'    => Logistica_Forming_Planning::labor_field_rules(),
				'day_types'            => Logistica_Forming_Planning::day_types(),
				'holiday_settings'     => Logistica_Settings::get_panel_holidays_client_config(),
				'festivo_dates'        => $this->festivo_dates_for_week( $schedule['semana_inicio'] ?? $week ),
			)
		);
	}

	/**
	 * AJAX: comparación semana anterior vs actual (planeación formado).
	 *
	 * @return void
	 */
	public function get_planeacion_formado_metrics() {
		$this->verify_ajax( 'logistica_planeacion_formado' );

		$week    = isset( $_POST['semana_inicio'] ) ? sanitize_text_field( wp_unslash( $_POST['semana_inicio'] ) ) : Logistica_Forming_Planning::week_start();
		$machine = isset( $_POST['maquina'] ) ? sanitize_key( wp_unslash( $_POST['maquina'] ) ) : Logistica_Forming_Planning::default_machine_slug();

		wp_send_json_success(
			Logistica_Planning_Metrics::get_week_comparison_summary(
				Logistica_Forming_Planning::class,
				$week,
				$machine
			)
		);
	}

	/**
	 * AJAX: métricas de planeación formado por rango de fechas (gráficos).
	 *
	 * @return void
	 */
	public function get_planeacion_formado_metrics_period() {
		$this->verify_ajax( 'logistica_planeacion_formado' );

		$date_from = isset( $_POST['fecha_desde'] ) ? sanitize_text_field( wp_unslash( $_POST['fecha_desde'] ) ) : '';
		$date_to   = isset( $_POST['fecha_hasta'] ) ? sanitize_text_field( wp_unslash( $_POST['fecha_hasta'] ) ) : '';
		$machine   = isset( $_POST['maquina'] ) ? sanitize_key( wp_unslash( $_POST['maquina'] ) ) : Logistica_Forming_Planning::default_machine_slug();

		$result = Logistica_Planning_Metrics::get_period_summary(
			Logistica_Forming_Planning::class,
			$date_from,
			$date_to,
			$machine
		);
		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ), 400 );
		}

		wp_send_json_success( $result );
	}

	/**
	 * AJAX: tablero TV del día actual.
	 *
	 * @return void
	 */
	public function get_tablero_produccion() {
		$this->verify_ajax( 'logistica_tablero_produccion' );

		$date  = isset( $_POST['fecha'] ) ? sanitize_text_field( wp_unslash( $_POST['fecha'] ) ) : '';
		$etapa = isset( $_POST['etapa'] ) ? sanitize_key( wp_unslash( $_POST['etapa'] ) ) : '';
		if ( '' === $etapa && isset( $_POST['filter'] ) ) {
			$etapa = sanitize_key( wp_unslash( $_POST['filter'] ) );
		}
		if ( '' === $etapa ) {
			$etapa = 'all';
		}

		wp_send_json_success( Logistica_Production_Board::get_board_payload( $date, $etapa ) );
	}

	/**
	 * AJAX: actualizar persona / prioridad / novedad desde el tablero.
	 *
	 * @return void
	 */
	public function save_tablero_produccion_job() {
		$this->verify_ajax( 'logistica_tablero_produccion' );

		$source  = isset( $_POST['source'] ) ? sanitize_key( wp_unslash( $_POST['source'] ) ) : '';
		$machine = isset( $_POST['machine'] ) ? sanitize_key( wp_unslash( $_POST['machine'] ) ) : '';
		$task_id = isset( $_POST['id'] ) ? sanitize_text_field( wp_unslash( $_POST['id'] ) ) : '';

		$result = Logistica_Production_Board::update_job_fields(
			$source,
			$machine,
			$task_id,
			array(
				'assigned_person' => isset( $_POST['assigned_person'] ) ? wp_unslash( $_POST['assigned_person'] ) : '',
				'board_priority'  => isset( $_POST['board_priority'] ) ? wp_unslash( $_POST['board_priority'] ) : 'plan_b',
				'board_alert'     => isset( $_POST['board_alert'] ) ? wp_unslash( $_POST['board_alert'] ) : '',
			)
		);

		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ), 400 );
		}

		wp_send_json_success( $result );
	}

	/**
	 * AJAX: resumen semanal del cuadro de pedidos (semana actual).
	 *
	 * @return void
	 */
	public function get_pedidos_metrics_week() {
		$this->verify_ajax( 'logistica_pedidos_board' );

		$week = isset( $_POST['semana_inicio'] ) ? sanitize_text_field( wp_unslash( $_POST['semana_inicio'] ) ) : Logistica_Forming_Planning::week_start();

		wp_send_json_success( Logistica_Pedidos_Metrics::get_week_summary( $week ) );
	}

	/**
	 * AJAX: métricas de pedidos por rango de fechas (gráficos).
	 *
	 * @return void
	 */
	public function get_pedidos_metrics_period() {
		$this->verify_ajax( 'logistica_pedidos_board' );

		$date_from = isset( $_POST['fecha_desde'] ) ? sanitize_text_field( wp_unslash( $_POST['fecha_desde'] ) ) : '';
		$date_to   = isset( $_POST['fecha_hasta'] ) ? sanitize_text_field( wp_unslash( $_POST['fecha_hasta'] ) ) : '';

		$result = Logistica_Pedidos_Metrics::get_period_summary( $date_from, $date_to );
		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ), 400 );
		}

		wp_send_json_success( $result );
	}

	/**
	 * AJAX: guarda planeación de formado.
	 *
	 * @return void
	 */
	public function save_planeacion_formado() {
		$this->verify_ajax( 'logistica_planeacion_formado' );

		$week    = isset( $_POST['semana_inicio'] ) ? sanitize_text_field( wp_unslash( $_POST['semana_inicio'] ) ) : '';
		$machine = isset( $_POST['maquina'] ) ? sanitize_key( wp_unslash( $_POST['maquina'] ) ) : Logistica_Forming_Planning::default_machine_slug();
		$json    = isset( $_POST['schedule_json'] ) ? wp_unslash( $_POST['schedule_json'] ) : '';

		$payload = json_decode( is_string( $json ) ? $json : '', true );
		if ( ! is_array( $payload ) ) {
			wp_send_json_error( array( 'message' => __( 'Datos de planeación inválidos.', 'logistica' ) ), 400 );
		}

		$result = Logistica_Forming_Planning::save_schedule( $week, $machine, $payload );
		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ), 400 );
		}

		wp_send_json_success(
			array(
				'message'  => __( 'Planeación guardada.', 'logistica' ),
				'schedule' => $result,
				'time'     => Logistica_User_Panel::current_time_hms(),
			)
		);
	}

	/**
	 * AJAX: importa planeación de formado desde Excel.
	 *
	 * @return void
	 */
	public function import_planeacion_formado() {
		$this->verify_ajax( 'logistica_planeacion_formado' );

		$week    = isset( $_POST['semana_inicio'] ) ? sanitize_text_field( wp_unslash( $_POST['semana_inicio'] ) ) : '';
		$machine = isset( $_POST['maquina'] ) ? sanitize_key( wp_unslash( $_POST['maquina'] ) ) : Logistica_Forming_Planning::default_machine_slug();
		$file    = isset( $_FILES['excel_file'] ) ? $_FILES['excel_file'] : null; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized

		$import = Logistica_Forming_Planning_Excel::import_upload( $file );
		if ( is_wp_error( $import ) ) {
			wp_send_json_error( array( 'message' => $import->get_error_message() ), 400 );
		}

		$schedule = Logistica_Forming_Planning::get_schedule( $week, $machine );
		$payload  = array(
			'jornada_inicio' => (int) ( $schedule['jornada_inicio'] ?? 420 ),
			'jornada_fin'    => (int) ( $schedule['jornada_fin'] ?? 1020 ),
			'day_hours'      => $schedule['day_hours'] ?? array(),
			'include_lunch'  => ! empty( $schedule['include_lunch'] ),
			'tasks'          => $import['tasks'],
		);

		$result = Logistica_Forming_Planning::save_schedule( $week, $machine, $payload );
		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ), 400 );
		}

		wp_send_json_success(
			array(
				'message'  => sprintf(
					/* translators: %d: number of imported tasks */
					__( 'Importadas %d actividad(es).', 'logistica' ),
					(int) ( $import['count'] ?? 0 )
				),
				'schedule' => $result,
				'count'    => (int) ( $import['count'] ?? 0 ),
				'time'     => Logistica_User_Panel::current_time_hms(),
			)
		);
	}

	/**
	 * AJAX: registra una máquina nueva para planeación de formado.
	 *
	 * @return void
	 */
	public function add_planeacion_formado_machine() {
		$this->verify_ajax( 'logistica_planeacion_formado' );

		$label = isset( $_POST['nombre'] ) ? sanitize_text_field( wp_unslash( $_POST['nombre'] ) ) : '';
		$slug  = Logistica_Forming_Planning::add_custom_machine( $label );

		if ( is_wp_error( $slug ) ) {
			wp_send_json_error( array( 'message' => $slug->get_error_message() ), 400 );
		}

		wp_send_json_success(
			array(
				'slug'                 => $slug,
				'label'                => Logistica_Forming_Planning::get_machines()[ $slug ] ?? $label,
				'machines'             => Logistica_Forming_Planning::get_machines(),
				'custom_machine_slugs' => array_keys( Logistica_Forming_Planning::get_custom_machines() ),
				'message'              => __( 'Máquina creada. Cada máquina tiene su propia programación semanal.', 'logistica' ),
			)
		);
	}

	/**
	 * AJAX: actualiza el nombre de una máquina de formado.
	 *
	 * @return void
	 */
	public function update_planeacion_formado_machine() {
		$this->verify_ajax( 'logistica_planeacion_formado' );

		$slug   = isset( $_POST['maquina'] ) ? sanitize_key( wp_unslash( $_POST['maquina'] ) ) : '';
		$label  = isset( $_POST['nombre'] ) ? sanitize_text_field( wp_unslash( $_POST['nombre'] ) ) : '';
		$result = Logistica_Forming_Planning::update_machine_label( $slug, $label );

		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ), 400 );
		}

		wp_send_json_success(
			array(
				'slug'                 => $result,
				'label'                => Logistica_Forming_Planning::get_machines()[ $result ] ?? $label,
				'machines'             => Logistica_Forming_Planning::get_machines(),
				'custom_machine_slugs' => array_keys( Logistica_Forming_Planning::get_custom_machines() ),
				'message'              => __( 'Máquina actualizada.', 'logistica' ),
			)
		);
	}

	/**
	 * AJAX: elimina una máquina de planeación de formado.
	 *
	 * @return void
	 */
	public function delete_planeacion_formado_machine() {
		$this->verify_ajax( 'logistica_planeacion_formado' );

		$slug   = isset( $_POST['maquina'] ) ? sanitize_key( wp_unslash( $_POST['maquina'] ) ) : '';
		$result = Logistica_Forming_Planning::delete_machine( $slug );

		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ), 400 );
		}

		wp_send_json_success(
			array(
				'machines'             => Logistica_Forming_Planning::get_machines(),
				'custom_machine_slugs' => array_keys( Logistica_Forming_Planning::get_custom_machines() ),
				'message'              => __( 'Máquina eliminada.', 'logistica' ),
			)
		);
	}

	/**
	 * AJAX: guarda opciones de listas desplegables de planeación de formado.
	 *
	 * @return void
	 */
	public function save_planeacion_formado_field_options() {
		$this->verify_ajax( 'logistica_planeacion_formado' );

		$json    = isset( $_POST['options_json'] ) ? wp_unslash( $_POST['options_json'] ) : '';
		$payload = json_decode( is_string( $json ) ? $json : '', true );
		if ( ! is_array( $payload ) ) {
			wp_send_json_error( array( 'message' => __( 'Datos de opciones inválidos.', 'logistica' ) ), 400 );
		}

		$result = Logistica_Forming_Planning::save_field_options( $payload );
		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ), 400 );
		}

		wp_send_json_success(
			array(
				'field_options' => $result,
				'message'       => __( 'Listas desplegables guardadas.', 'logistica' ),
			)
		);
	}

	/**
	 * AJAX: guarda catálogo de rendimiento por referencia (formado).
	 *
	 * @return void
	 */
	public function save_forming_yield_catalog() {
		$this->verify_ajax( 'logistica_planeacion_formado' );

		$json    = isset( $_POST['catalog_json'] ) ? wp_unslash( $_POST['catalog_json'] ) : '';
		$payload = json_decode( is_string( $json ) ? $json : '', true );
		if ( ! is_array( $payload ) ) {
			wp_send_json_error( array( 'message' => __( 'Datos de rendimiento inválidos.', 'logistica' ) ), 400 );
		}

		$result = Logistica_Forming_Yield::save_catalog( $payload );
		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ), 400 );
		}

		wp_send_json_success(
			array(
				'forming_yield' => $result,
				'message'       => __( 'Rendimiento por referencia guardado.', 'logistica' ),
			)
		);
	}

	/**
	 * Redirects back to the closing screen after Excel import.
	 *
	 * @param string $status Status flag.
	 * @return void
	 */
	private function redirect_import_status( $status ) {
		require_once LOGISTICA_PLUGIN_DIR . 'admin/views/partials.php';

		$redirect = isset( $_POST['redirect_to'] ) ? esc_url_raw( wp_unslash( $_POST['redirect_to'] ) ) : '';

		if ( ! $redirect || ! wp_validate_redirect( $redirect, false ) ) {
			$redirect = logistica_tab_url( 'logistica-cierre-ordenes', 'admin' );
		}

		wp_safe_redirect(
			add_query_arg(
				'logistica_status',
				$status,
				remove_query_arg( 'logistica_status', $redirect )
			)
		);
		exit;
	}

	/**
	 * CSS/JS para impresión de etiquetas de producción.
	 *
	 * @return void
	 */
	public function enqueue_label_print_assets() {
		if ( function_exists( 'wp_enqueue_media' ) ) {
			wp_enqueue_media();
		}
		Logistica_Label_Print::register_assets();
	}

	/**
	 * CSS/JS para rótulos de rollos.
	 *
	 * @return void
	 */
	public function enqueue_rotulo_rollos_assets() {
		Logistica_Rotulo_Rollos_Print::register_assets();
		Logistica_Orden_Produccion_Vasos::register_assets();
	}

	/**
	 * Guarda ajustes de impresión de etiquetas.
	 *
	 * @return void
	 */
	public function save_label_print_settings() {
		$this->verify_request( 'logistica_save_label_print_settings' );

		require_once LOGISTICA_PLUGIN_DIR . 'admin/views/partials.php';

		if ( ! logistica_can_edit_etiqueta_settings() ) {
			wp_die( esc_html__( 'No tienes permiso para modificar esta configuración.', 'logistica' ), 403 );
		}

		$raw = isset( $_POST['label_print'] ) && is_array( $_POST['label_print'] ) ? wp_unslash( $_POST['label_print'] ) : array();
		Logistica_Label_Print::save_settings( $raw, true );

		$redirect_context = logistica_is_config_panel() ? 'config' : 'admin';

		wp_safe_redirect(
			add_query_arg(
				'logistica_status',
				'print_settings_saved',
				logistica_tab_url( 'logistica-codigos-etiqueta', $redirect_context )
			)
		);
		exit;
	}

	/**
	 * AJAX: guarda tamaño de etiqueta de producción sin recargar la página.
	 *
	 * @return void
	 */
	public function ajax_save_label_print_settings() {
		$this->verify_ajax( 'logistica_label_print' );

		require_once LOGISTICA_PLUGIN_DIR . 'admin/views/partials.php';

		if ( ! logistica_can_edit_etiqueta_settings() ) {
			wp_send_json_error( array( 'message' => __( 'No tienes permiso para modificar esta configuración.', 'logistica' ) ), 403 );
		}

		$raw = array();
		if ( isset( $_POST['label_print'] ) && is_array( $_POST['label_print'] ) ) {
			$raw = wp_unslash( $_POST['label_print'] );
		} else {
			if ( isset( $_POST[ Logistica_Label_Print::OPTION_WIDTH_MM ] ) ) {
				$raw[ Logistica_Label_Print::OPTION_WIDTH_MM ] = wp_unslash( $_POST[ Logistica_Label_Print::OPTION_WIDTH_MM ] );
			}
			if ( isset( $_POST[ Logistica_Label_Print::OPTION_HEIGHT_MM ] ) ) {
				$raw[ Logistica_Label_Print::OPTION_HEIGHT_MM ] = wp_unslash( $_POST[ Logistica_Label_Print::OPTION_HEIGHT_MM ] );
			}
		}

		$allow_admin_options = ! empty( $_POST['label_print_admin_options'] ) && logistica_can_edit_etiqueta_settings();
		Logistica_Label_Print::save_settings( $raw, $allow_admin_options );

		$save_mode = isset( $_POST['label_print_save_mode'] ) ? sanitize_key( wp_unslash( $_POST['label_print_save_mode'] ) ) : 'size';
		$message   = 'options' === $save_mode
			? __( 'Opción de numeración guardada.', 'logistica' )
			: __( 'Tamaño de rótulo guardado.', 'logistica' );

		wp_send_json_success(
			array(
				'message' => $message,
				'config'  => Logistica_Label_Print::client_config(),
			)
		);
	}

	/**
	 * Guarda ajustes BarTender.
	 *
	 * @return void
	 * @deprecated
	 */
	public function save_bartender_settings() {
		$this->save_label_print_settings();
	}

	/**
	 * Guarda código de producto para etiqueta.
	 *
	 * @return void
	 */
	public function save_etiqueta_producto() {
		$this->verify_request( 'logistica_save_etiqueta_producto' );

		$redirect = isset( $_POST['redirect_to'] ) ? esc_url_raw( wp_unslash( $_POST['redirect_to'] ) ) : '';
		if ( ! $redirect || ! wp_validate_redirect( $redirect, false ) ) {
			require_once LOGISTICA_PLUGIN_DIR . 'admin/views/partials.php';
			$redirect = logistica_tab_url( 'logistica-codigos-etiqueta', 'admin' );
		}

		$result = Logistica_Label_Product_Catalog::save_row( wp_unslash( $_POST ) );
		if ( is_wp_error( $result ) ) {
			set_transient( 'logistica_etiqueta_producto_error_' . get_current_user_id(), $result->get_error_message(), 60 );
			wp_safe_redirect( add_query_arg( 'logistica_status', 'error', $redirect ) );
			exit;
		}

		wp_safe_redirect( add_query_arg( 'logistica_status', 'saved', remove_query_arg( 'edit_id', $redirect ) ) );
		exit;
	}

	/**
	 * Elimina código de producto para etiqueta.
	 *
	 * @return void
	 */
	public function delete_etiqueta_producto() {
		$this->verify_request( 'logistica_delete_etiqueta_producto' );

		$redirect = isset( $_POST['redirect_to'] ) ? esc_url_raw( wp_unslash( $_POST['redirect_to'] ) ) : '';
		if ( ! $redirect || ! wp_validate_redirect( $redirect, false ) ) {
			require_once LOGISTICA_PLUGIN_DIR . 'admin/views/partials.php';
			$redirect = logistica_tab_url( 'logistica-codigos-etiqueta', 'admin' );
		}

		$id     = isset( $_POST['id'] ) ? (int) wp_unslash( $_POST['id'] ) : 0;
		$result = Logistica_Label_Product_Catalog::delete_row( $id );
		if ( is_wp_error( $result ) ) {
			set_transient( 'logistica_etiqueta_producto_error_' . get_current_user_id(), $result->get_error_message(), 60 );
			wp_safe_redirect( add_query_arg( 'logistica_status', 'error', $redirect ) );
			exit;
		}

		wp_safe_redirect( add_query_arg( 'logistica_status', 'deleted', $redirect ) );
		exit;
	}

	/**
	 * Guarda definición de ancho para rótulo de rollo.
	 *
	 * @return void
	 */
	public function save_rotulo_rollos() {
		$this->verify_request( 'logistica_save_rotulo_rollos' );

		$redirect = isset( $_POST['redirect_to'] ) ? esc_url_raw( wp_unslash( $_POST['redirect_to'] ) ) : '';
		if ( ! $redirect || ! wp_validate_redirect( $redirect, false ) ) {
			require_once LOGISTICA_PLUGIN_DIR . 'admin/views/partials.php';
			$redirect = logistica_tab_url( 'logistica-rotulos-rollos', 'admin' );
		}

		$result = Logistica_Rotulo_Rollos_Catalog::save_row( wp_unslash( $_POST ) );
		if ( is_wp_error( $result ) ) {
			set_transient( 'logistica_rotulo_rollos_error_' . get_current_user_id(), $result->get_error_message(), 60 );
			wp_safe_redirect( add_query_arg( 'logistica_status', 'error', $redirect ) );
			exit;
		}

		wp_safe_redirect( add_query_arg( 'logistica_status', 'saved', $redirect ) );
		exit;
	}

	/**
	 * Elimina definición de ancho para rótulo de rollo.
	 *
	 * @return void
	 */
	public function delete_rotulo_rollos() {
		$this->verify_request( 'logistica_delete_rotulo_rollos' );

		$redirect = isset( $_POST['redirect_to'] ) ? esc_url_raw( wp_unslash( $_POST['redirect_to'] ) ) : '';
		if ( ! $redirect || ! wp_validate_redirect( $redirect, false ) ) {
			require_once LOGISTICA_PLUGIN_DIR . 'admin/views/partials.php';
			$redirect = logistica_tab_url( 'logistica-rotulos-rollos', 'admin' );
		}

		$id     = isset( $_POST['id'] ) ? (int) wp_unslash( $_POST['id'] ) : 0;
		$result = Logistica_Rotulo_Rollos_Catalog::delete_row( $id );
		if ( is_wp_error( $result ) ) {
			set_transient( 'logistica_rotulo_rollos_error_' . get_current_user_id(), $result->get_error_message(), 60 );
			wp_safe_redirect( add_query_arg( 'logistica_status', 'error', $redirect ) );
			exit;
		}

		wp_safe_redirect( add_query_arg( 'logistica_status', 'deleted', $redirect ) );
		exit;
	}

	/**
	 * AJAX: obtiene un rótulo de rollo por ID.
	 *
	 * @return void
	 */
	public function get_rotulo_rollos_row() {
		$this->verify_ajax( 'logistica_rotulos_rollos' );

		$rotulo_id = isset( $_POST['rotulo_id'] ) ? (int) wp_unslash( $_POST['rotulo_id'] ) : 0;
		$row       = Logistica_Rotulo_Rollos_Catalog::get_by_id( $rotulo_id );

		if ( ! $row ) {
			wp_send_json_error( array( 'message' => __( 'Registro no encontrado.', 'logistica' ) ), 404 );
		}

		wp_send_json_success(
			array(
				'row' => Logistica_Rotulo_Rollos_Catalog::format_row_for_client( $row ),
			)
		);
	}

	/**
	 * AJAX: guarda ancho de rótulo sin recargar la página.
	 *
	 * @return void
	 */
	public function ajax_save_rotulo_rollos() {
		$this->verify_ajax( 'logistica_rotulos_rollos' );

		$result = Logistica_Rotulo_Rollos_Catalog::save_row( wp_unslash( $_POST ) );
		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ), 400 );
		}

		$formatted = Logistica_Rotulo_Rollos_Catalog::format_row_for_client( $result );
		if ( ! $formatted ) {
			wp_send_json_error( array( 'message' => __( 'No se pudo leer el registro guardado.', 'logistica' ) ), 500 );
		}

		$is_new = empty( $_POST['id'] ) || (int) wp_unslash( $_POST['id'] ) <= 0;

		wp_send_json_success(
			array(
				'row'     => $formatted,
				'message' => $is_new ? __( 'Registro guardado.', 'logistica' ) : __( 'Registro actualizado.', 'logistica' ),
				'total'   => count( Logistica_Rotulo_Rollos_Catalog::get_all() ),
				'is_new'  => $is_new,
			)
		);
	}

	/**
	 * AJAX: historial de impresión de rótulos para un ancho.
	 *
	 * @return void
	 */
	public function get_rotulo_rollos_print_history() {
		$this->verify_ajax( 'logistica_rotulos_rollos' );

		$rotulo_id = isset( $_POST['rotulo_id'] ) ? (int) wp_unslash( $_POST['rotulo_id'] ) : 0;
		if ( $rotulo_id <= 0 ) {
			wp_send_json_error( array( 'message' => __( 'Registro de rótulo no válido.', 'logistica' ) ) );
		}

		wp_send_json_success(
			array(
				'entries' => Logistica_Rotulo_Rollos_Print_History::get_for_rotulo( $rotulo_id ),
			)
		);
	}

	/**
	 * AJAX: guarda automáticamente una impresión en el historial de rotulación.
	 *
	 * @return void
	 */
	public function save_rotulo_rollos_print_history() {
		$this->verify_ajax( 'logistica_rotulos_rollos' );

		$pesos_raw = isset( $_POST['pesos'] ) ? wp_unslash( $_POST['pesos'] ) : array();
		if ( is_string( $pesos_raw ) ) {
			$decoded = json_decode( $pesos_raw, true );
			$pesos_raw = is_array( $decoded ) ? $decoded : $pesos_raw;
		}

		$result = Logistica_Rotulo_Rollos_Print_History::save_entry(
			array(
				'rotulo_id'       => isset( $_POST['rotulo_id'] ) ? (int) wp_unslash( $_POST['rotulo_id'] ) : 0,
				'lote_inicio'     => isset( $_POST['lote_inicio'] ) ? sanitize_text_field( wp_unslash( $_POST['lote_inicio'] ) ) : '',
				'lote_fin'        => isset( $_POST['lote_fin'] ) ? sanitize_text_field( wp_unslash( $_POST['lote_fin'] ) ) : '',
				'num_consecutivo' => isset( $_POST['num_consecutivo'] ) ? (int) wp_unslash( $_POST['num_consecutivo'] ) : 1,
				'num_copias'      => isset( $_POST['num_copias'] ) ? (int) wp_unslash( $_POST['num_copias'] ) : 1,
				'gramaje'         => isset( $_POST['gramaje'] ) ? sanitize_text_field( wp_unslash( $_POST['gramaje'] ) ) : '',
				'pesos'           => $pesos_raw,
			)
		);

		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ), 400 );
		}

		wp_send_json_success( $result );
	}

	/**
	 * Guarda tamaño de impresión de rótulos de rollos.
	 *
	 * @return void
	 */
	public function save_rotulo_rollos_print_settings() {
		$this->verify_request( 'logistica_save_rotulo_rollos_print_settings' );
		require_once LOGISTICA_PLUGIN_DIR . 'admin/views/partials.php';

		if ( ! function_exists( 'logistica_can_edit_rotulo_rollos_settings' ) || ! logistica_can_edit_rotulo_rollos_settings() ) {
			wp_die( esc_html__( 'No tienes permiso para modificar esta configuración.', 'logistica' ), '', array( 'response' => 403 ) );
		}

		$raw = isset( $_POST['rotulo_rollos_print'] ) && is_array( $_POST['rotulo_rollos_print'] ) ? wp_unslash( $_POST['rotulo_rollos_print'] ) : array();
		Logistica_Rotulo_Rollos_Print::save_settings( $raw );

		$redirect = isset( $_POST['redirect_to'] ) ? trim( (string) wp_unslash( $_POST['redirect_to'] ) ) : '';
		$redirect = wp_validate_redirect( $redirect, '' );
		if ( '' === $redirect ) {
			$redirect = function_exists( 'logistica_config_panel_url' )
				? logistica_config_panel_url( 'logistica-rotulos-rollos' )
				: logistica_tab_url( 'logistica-rotulos-rollos', 'frontend' );
		}

		wp_safe_redirect( add_query_arg( 'logistica_status', 'print_settings_saved', $redirect ) );
		exit;
	}

	/**
	 * AJAX: guarda tamaño de impresión de rótulos de rollos.
	 *
	 * @return void
	 */
	public function ajax_save_rotulo_rollos_print_settings() {
		$this->verify_ajax( 'logistica_rotulos_rollos' );
		require_once LOGISTICA_PLUGIN_DIR . 'admin/views/partials.php';

		if ( ! function_exists( 'logistica_can_edit_rotulo_rollos_settings' ) || ! logistica_can_edit_rotulo_rollos_settings() ) {
			wp_send_json_error( array( 'message' => __( 'No tienes permiso para modificar esta configuración.', 'logistica' ) ), 403 );
		}

		$raw = isset( $_POST['rotulo_rollos_print'] ) && is_array( $_POST['rotulo_rollos_print'] ) ? wp_unslash( $_POST['rotulo_rollos_print'] ) : array();
		Logistica_Rotulo_Rollos_Print::save_settings( $raw );

		wp_send_json_success(
			array(
				'message' => __( 'Tamaño de rótulo guardado.', 'logistica' ),
				'config'  => Logistica_Rotulo_Rollos_Print::client_config(),
			)
		);
	}

	/**
	 * Exporta catálogo de rótulos de rollos como Excel (.xlsx).
	 *
	 * @return void
	 */
	public function export_rotulo_rollos() {
		$this->verify_request( 'logistica_export_rotulo_rollos' );
		Logistica_Rotulo_Rollos_Catalog_Excel::download_template();
	}

	/**
	 * Importa catálogo de rótulos de rollos desde Excel.
	 *
	 * @return void
	 */
	public function import_rotulo_rollos() {
		$this->verify_request( 'logistica_import_rotulo_rollos' );

		$result = Logistica_Rotulo_Rollos_Catalog_Excel::import_upload( $_FILES['rotulo_rollos_file'] ?? array() );

		set_transient(
			'logistica_rotulo_rollos_import_' . get_current_user_id(),
			$result,
			MINUTE_IN_SECONDS
		);

		$redirect = isset( $_POST['redirect_to'] ) ? esc_url_raw( wp_unslash( $_POST['redirect_to'] ) ) : '';
		if ( ! $redirect || ! wp_validate_redirect( $redirect, false ) ) {
			require_once LOGISTICA_PLUGIN_DIR . 'admin/views/partials.php';
			$redirect = logistica_tab_url( 'logistica-rotulos-rollos', 'admin' );
		}

		$status = ! empty( $result['error'] ) ? 'import_error' : 'imported';
		wp_safe_redirect(
			add_query_arg(
				'logistica_status',
				$status,
				remove_query_arg( 'logistica_status', $redirect )
			)
		);
		exit;
	}

	/**
	 * Exporta catálogo de códigos etiqueta como Excel (.xlsx).
	 *
	 * @return void
	 */
	public function export_etiqueta_productos() {
		$this->verify_request( 'logistica_export_etiqueta_productos' );
		Logistica_Label_Product_Catalog_Excel::download_template();
	}

	/**
	 * Importa catálogo de códigos etiqueta desde Excel.
	 *
	 * @return void
	 */
	public function import_etiqueta_productos() {
		$this->verify_request( 'logistica_import_etiqueta_productos' );

		$result = Logistica_Label_Product_Catalog_Excel::import_upload( $_FILES['etiqueta_productos_file'] ?? array() );

		set_transient(
			'logistica_etiqueta_productos_import_' . get_current_user_id(),
			$result,
			MINUTE_IN_SECONDS
		);

		$redirect = isset( $_POST['redirect_to'] ) ? esc_url_raw( wp_unslash( $_POST['redirect_to'] ) ) : '';
		if ( ! $redirect || ! wp_validate_redirect( $redirect, false ) ) {
			require_once LOGISTICA_PLUGIN_DIR . 'admin/views/partials.php';
			$redirect = logistica_tab_url( 'logistica-codigos-etiqueta', 'admin' );
		}

		$status = ! empty( $result['error'] ) ? 'import_error' : 'imported';
		wp_safe_redirect(
			add_query_arg(
				'logistica_status',
				$status,
				remove_query_arg( 'logistica_status', $redirect )
			)
		);
		exit;
	}

	/**
	 * AJAX: vista previa EAN-13 desde catálogo de etiquetas.
	 *
	 * @return void
	 */
	public function preview_label_barcode() {
		$this->verify_ajax( 'logistica_label_print' );

		$catalog_id = isset( $_POST['catalog_id'] ) ? (int) wp_unslash( $_POST['catalog_id'] ) : 0;
		if ( $catalog_id <= 0 ) {
			wp_send_json_error( array( 'message' => __( 'Registro de etiqueta no válido.', 'logistica' ) ) );
		}

		$result = Logistica_Label_Product_Catalog::resolve_barcode_for_catalog_id( $catalog_id );
		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ) );
		}

		wp_send_json_success( $result );
	}

	/**
	 * AJAX: historial de paquetes/unidades para un código de etiqueta.
	 *
	 * @return void
	 */
	public function get_label_pack_history() {
		$this->verify_ajax( 'logistica_label_print' );

		$catalog_id = isset( $_POST['catalog_id'] ) ? (int) wp_unslash( $_POST['catalog_id'] ) : 0;
		if ( $catalog_id <= 0 ) {
			wp_send_json_error( array( 'message' => __( 'Registro de etiqueta no válido.', 'logistica' ) ) );
		}

		wp_send_json_success(
			array(
				'entries' => Logistica_Label_Pack_History::get_for_catalog( $catalog_id ),
			)
		);
	}

	/**
	 * AJAX: guarda automáticamente una combinación en el historial.
	 *
	 * @return void
	 */
	public function save_label_pack_history() {
		$this->verify_ajax( 'logistica_label_print' );

		$catalog_id      = isset( $_POST['catalog_id'] ) ? (int) wp_unslash( $_POST['catalog_id'] ) : 0;
		$paquetes        = isset( $_POST['paquetes'] ) ? sanitize_text_field( wp_unslash( $_POST['paquetes'] ) ) : '';
		$und_por_paquete = isset( $_POST['und_por_paquete'] ) ? sanitize_text_field( wp_unslash( $_POST['und_por_paquete'] ) ) : '';

		$result = Logistica_Label_Pack_History::save_entry( $catalog_id, $paquetes, $und_por_paquete );
		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ), 400 );
		}

		wp_send_json_success( $result );
	}

	/**
	 * AJAX: guarda logo de texto o imagen en el historial global.
	 *
	 * @return void
	 */
	public function save_label_logo_history() {
		$this->verify_ajax( 'logistica_label_print' );

		$mode     = isset( $_POST['logo_mode'] ) ? sanitize_key( wp_unslash( $_POST['logo_mode'] ) ) : 'image';
		$logo_url = isset( $_POST['logo_url'] ) ? esc_url_raw( wp_unslash( $_POST['logo_url'] ) ) : '';
		$line1    = isset( $_POST['logo_text_line1'] ) ? sanitize_text_field( wp_unslash( $_POST['logo_text_line1'] ) ) : '';
		$line2    = isset( $_POST['logo_text_line2'] ) ? sanitize_text_field( wp_unslash( $_POST['logo_text_line2'] ) ) : '';

		$result = Logistica_Label_Logo_History::save_from_payload( $mode, $logo_url, $line1, $line2 );
		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ), 400 );
		}

		wp_send_json_success(
			array(
				'history' => Logistica_Label_Logo_History::client_format(),
			)
		);
	}

	/**
	 * AJAX: obtiene un código de etiqueta por ID.
	 *
	 * @return void
	 */
	public function get_etiqueta_producto_row() {
		$this->verify_ajax( 'logistica_label_print' );

		$catalog_id = isset( $_POST['catalog_id'] ) ? (int) wp_unslash( $_POST['catalog_id'] ) : 0;
		$row        = Logistica_Label_Product_Catalog::get_by_id( $catalog_id );

		if ( ! $row ) {
			wp_send_json_error( array( 'message' => __( 'Registro no encontrado.', 'logistica' ) ), 404 );
		}

		wp_send_json_success(
			array(
				'row' => Logistica_Label_Product_Catalog::format_row_for_client( $row ),
			)
		);
	}

	/**
	 * AJAX: guarda código de etiqueta sin recargar la página.
	 *
	 * @return void
	 */
	public function ajax_save_etiqueta_producto() {
		$this->verify_ajax( 'logistica_label_print' );

		$result = Logistica_Label_Product_Catalog::save_row( wp_unslash( $_POST ) );
		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ), 400 );
		}

		$formatted = Logistica_Label_Product_Catalog::format_row_for_client( $result );
		if ( ! $formatted ) {
			wp_send_json_error( array( 'message' => __( 'No se pudo leer el registro guardado.', 'logistica' ) ), 500 );
		}

		$is_new = empty( $_POST['id'] ) || (int) wp_unslash( $_POST['id'] ) <= 0;

		wp_send_json_success(
			array(
				'row'        => $formatted,
				'message'    => $is_new ? __( 'Registro guardado.', 'logistica' ) : __( 'Registro actualizado.', 'logistica' ),
				'total'      => count( Logistica_Label_Product_Catalog::get_all() ),
				'is_new'     => $is_new,
				'duplicates' => Logistica_Label_Product_Catalog::get_duplicate_report(),
			)
		);
	}

	/**
	 * AJAX: vista previa EAN-13 desde campos del formulario.
	 *
	 * @return void
	 */
	public function preview_etiqueta_ean_fields() {
		$this->verify_ajax( 'logistica_label_print' );

		$ean = Logistica_Label_Barcode::build_ean13(
			wp_unslash( $_POST['tipo_producto'] ?? '' ),
			wp_unslash( $_POST['mercado'] ?? '' ),
			wp_unslash( $_POST['codigo_producto'] ?? '' )
		);

		if ( is_wp_error( $ean ) ) {
			wp_send_json_error( array( 'message' => $ean->get_error_message() ) );
		}

		wp_send_json_success(
			array(
				'ean13' => (string) $ean,
			)
		);
	}

	/**
	 * AJAX: guarda un tamaño nuevo en la lista del catálogo.
	 *
	 * @return void
	 */
	public function save_etiqueta_tamano() {
		$this->verify_ajax( 'logistica_label_print' );

		$label = isset( $_POST['label'] ) ? wp_unslash( $_POST['label'] ) : '';
		$result = Logistica_Label_Etiqueta_Tamanos::add( $label );

		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ), 400 );
		}

		wp_send_json_success(
			array(
				'label'   => (string) $result,
				'options' => Logistica_Label_Etiqueta_Tamanos::get_options(),
				'entries' => Logistica_Label_Etiqueta_Tamanos::get_admin_entries(),
				'message' => __( 'Tamaño guardado.', 'logistica' ),
			)
		);
	}

	/**
	 * AJAX: guarda un tipo de producto nuevo en la lista del catálogo.
	 *
	 * @return void
	 */
	public function save_etiqueta_tipo_producto() {
		$this->verify_ajax( 'logistica_label_print' );

		$label     = isset( $_POST['label'] ) ? wp_unslash( $_POST['label'] ) : '';
		$type_code = isset( $_POST['type_code'] ) ? wp_unslash( $_POST['type_code'] ) : '';
		$result    = Logistica_Label_Etiqueta_Tipos::add( $label, $type_code );

		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ), 400 );
		}

		wp_send_json_success(
			array(
				'key'       => (string) $result['key'],
				'label'     => (string) $result['label'],
				'type_code' => (string) $result['type_code'],
				'options'   => Logistica_Label_Etiqueta_Tipos::get_option_labels(),
				'entries'   => Logistica_Label_Etiqueta_Tipos::get_admin_entries(),
				'message'   => sprintf(
					/* translators: 1: product label, 2: EAN type code */
					__( 'Producto «%1$s» guardado (código tipo %2$s).', 'logistica' ),
					$result['label'],
					$result['type_code']
				),
			)
		);
	}

	/**
	 * AJAX: actualiza un tipo de producto del catálogo.
	 *
	 * @return void
	 */
	public function update_etiqueta_tipo_producto() {
		$this->verify_ajax( 'logistica_label_print' );

		$key       = isset( $_POST['key'] ) ? wp_unslash( $_POST['key'] ) : '';
		$label     = isset( $_POST['label'] ) ? wp_unslash( $_POST['label'] ) : '';
		$type_code = isset( $_POST['type_code'] ) ? wp_unslash( $_POST['type_code'] ) : '';
		$result    = Logistica_Label_Etiqueta_Tipos::update( $key, $label, $type_code );

		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ), 400 );
		}

		wp_send_json_success(
			array(
				'entry'   => $result,
				'options' => Logistica_Label_Etiqueta_Tipos::get_option_labels(),
				'entries' => Logistica_Label_Etiqueta_Tipos::get_admin_entries(),
				'message' => __( 'Tipo de producto actualizado.', 'logistica' ),
			)
		);
	}

	/**
	 * AJAX: elimina un tipo personalizado del catálogo.
	 *
	 * @return void
	 */
	public function delete_etiqueta_tipo_producto() {
		$this->verify_ajax( 'logistica_label_print' );

		$key    = isset( $_POST['key'] ) ? wp_unslash( $_POST['key'] ) : '';
		$result = Logistica_Label_Etiqueta_Tipos::remove( $key );

		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ), 400 );
		}

		wp_send_json_success(
			$this->etiqueta_catalog_hidden_payload(
				array(
					'key'     => sanitize_text_field( (string) $key ),
					'options' => Logistica_Label_Etiqueta_Tipos::get_option_labels(),
					'entries' => Logistica_Label_Etiqueta_Tipos::get_admin_entries(),
					'message' => __( 'Tipo de producto eliminado de la lista.', 'logistica' ),
				)
			)
		);
	}

	/**
	 * AJAX: actualiza un tamaño del catálogo.
	 *
	 * @return void
	 */
	public function update_etiqueta_tamano() {
		$this->verify_ajax( 'logistica_label_print' );

		$key       = isset( $_POST['key'] ) ? wp_unslash( $_POST['key'] ) : '';
		$label     = isset( $_POST['label'] ) ? wp_unslash( $_POST['label'] ) : '';
		$result    = Logistica_Label_Etiqueta_Tamanos::update( $key, $label );

		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ), 400 );
		}

		wp_send_json_success(
			array(
				'key'     => (string) $result,
				'label'   => (string) $result,
				'options' => Logistica_Label_Etiqueta_Tamanos::get_options(),
				'entries' => Logistica_Label_Etiqueta_Tamanos::get_admin_entries(),
				'message' => __( 'Tamaño actualizado.', 'logistica' ),
			)
		);
	}

	/**
	 * AJAX: elimina un tamaño de la lista guardada.
	 *
	 * @return void
	 */
	public function delete_etiqueta_tamano() {
		$this->verify_ajax( 'logistica_label_print' );

		$key    = isset( $_POST['key'] ) ? wp_unslash( $_POST['key'] ) : '';
		$result = Logistica_Label_Etiqueta_Tamanos::remove( $key );

		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ), 400 );
		}

		wp_send_json_success(
			$this->etiqueta_catalog_hidden_payload(
				array(
					'key'     => sanitize_text_field( (string) $key ),
					'options' => Logistica_Label_Etiqueta_Tamanos::get_options(),
					'entries' => Logistica_Label_Etiqueta_Tamanos::get_admin_entries(),
					'message' => __( 'Tamaño eliminado de la lista.', 'logistica' ),
				)
			)
		);
	}

	/**
	 * AJAX: actualiza la etiqueta visible de un mercado.
	 *
	 * @return void
	 */
	public function update_etiqueta_mercado() {
		$this->verify_ajax( 'logistica_label_print' );

		$key          = isset( $_POST['key'] ) ? wp_unslash( $_POST['key'] ) : '';
		$label        = isset( $_POST['label'] ) ? wp_unslash( $_POST['label'] ) : '';
		$market_digit = isset( $_POST['market_digit'] ) ? wp_unslash( $_POST['market_digit'] ) : '';
		$result       = Logistica_Label_Etiqueta_Mercados::update( $key, $label, $market_digit );

		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ), 400 );
		}

		wp_send_json_success(
			array(
				'entry'   => $result,
				'options' => Logistica_Label_Etiqueta_Mercados::get_option_labels(),
				'entries' => Logistica_Label_Etiqueta_Mercados::get_admin_entries(),
				'message' => __( 'Mercado actualizado.', 'logistica' ),
			)
		);
	}

	/**
	 * AJAX: agrega un mercado al catálogo.
	 *
	 * @return void
	 */
	public function save_etiqueta_mercado() {
		$this->verify_ajax( 'logistica_label_print' );

		$key          = isset( $_POST['key'] ) ? wp_unslash( $_POST['key'] ) : '';
		$label        = isset( $_POST['label'] ) ? wp_unslash( $_POST['label'] ) : '';
		$market_digit = isset( $_POST['market_digit'] ) ? wp_unslash( $_POST['market_digit'] ) : '';
		$result       = Logistica_Label_Etiqueta_Mercados::add( $key, $label, $market_digit );

		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ), 400 );
		}

		wp_send_json_success(
			array(
				'entry'   => $result,
				'options' => Logistica_Label_Etiqueta_Mercados::get_option_labels(),
				'entries' => Logistica_Label_Etiqueta_Mercados::get_admin_entries(),
				'message' => sprintf(
					/* translators: 1: market label, 2: EAN market digit */
					__( 'Mercado «%1$s» guardado (dígito %2$s).', 'logistica' ),
					$result['label'],
					$result['market_digit']
				),
			)
		);
	}

	/**
	 * AJAX: elimina un mercado personalizado del catálogo.
	 *
	 * @return void
	 */
	public function delete_etiqueta_mercado() {
		$this->verify_ajax( 'logistica_label_print' );

		$key    = isset( $_POST['key'] ) ? wp_unslash( $_POST['key'] ) : '';
		$result = Logistica_Label_Etiqueta_Mercados::remove( $key );

		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ), 400 );
		}

		wp_send_json_success(
			$this->etiqueta_catalog_hidden_payload(
				array(
					'key'     => sanitize_text_field( (string) $key ),
					'options' => Logistica_Label_Etiqueta_Mercados::get_option_labels(),
					'entries' => Logistica_Label_Etiqueta_Mercados::get_admin_entries(),
					'message' => __( 'Mercado eliminado de la lista.', 'logistica' ),
				)
			)
		);
	}

	/**
	 * @param array<string, mixed> $payload Respuesta AJAX parcial.
	 * @return array<string, mixed>
	 */
	private function etiqueta_catalog_hidden_payload( array $payload = array() ) {
		return array_merge(
			array(
				'hidden_entries' => Logistica_Label_Etiqueta_Option_Usage::hidden_catalog_entries(),
			),
			$payload
		);
	}

	/**
	 * AJAX: consulta uso de una opción en productos del catálogo.
	 *
	 * @return void
	 */
	public function check_etiqueta_option_usage() {
		$this->verify_ajax( 'logistica_label_print' );

		$kind = isset( $_POST['kind'] ) ? sanitize_key( wp_unslash( $_POST['kind'] ) ) : '';
		$key  = isset( $_POST['key'] ) ? wp_unslash( $_POST['key'] ) : '';

		if ( ! in_array( $kind, array( 'tipo', 'mercado', 'tamano' ), true ) || '' === trim( (string) $key ) ) {
			wp_send_json_error( array( 'message' => __( 'Opción no válida.', 'logistica' ) ), 400 );
		}

		$usage = Logistica_Label_Etiqueta_Option_Usage::count_for_kind( $kind, $key );

		wp_send_json_success(
			array(
				'usage'   => $usage,
				'message' => Logistica_Label_Etiqueta_Option_Usage::usage_message( $kind, $key ),
			)
		);
	}

	/**
	 * AJAX: restaura un tipo oculto del catálogo.
	 *
	 * @return void
	 */
	public function restore_etiqueta_tipo_producto() {
		$this->verify_ajax( 'logistica_label_print' );

		$key    = isset( $_POST['key'] ) ? wp_unslash( $_POST['key'] ) : '';
		$result = Logistica_Label_Etiqueta_Tipos::restore( $key );

		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ), 400 );
		}

		wp_send_json_success(
			$this->etiqueta_catalog_hidden_payload(
				array(
					'key'     => sanitize_text_field( (string) $key ),
					'options' => Logistica_Label_Etiqueta_Tipos::get_option_labels(),
					'entries' => Logistica_Label_Etiqueta_Tipos::get_admin_entries(),
					'message' => __( 'Tipo de producto restaurado.', 'logistica' ),
				)
			)
		);
	}

	/**
	 * AJAX: restaura un mercado oculto del catálogo.
	 *
	 * @return void
	 */
	public function restore_etiqueta_mercado() {
		$this->verify_ajax( 'logistica_label_print' );

		$key    = isset( $_POST['key'] ) ? wp_unslash( $_POST['key'] ) : '';
		$result = Logistica_Label_Etiqueta_Mercados::restore( $key );

		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ), 400 );
		}

		wp_send_json_success(
			$this->etiqueta_catalog_hidden_payload(
				array(
					'key'     => sanitize_text_field( (string) $key ),
					'options' => Logistica_Label_Etiqueta_Mercados::get_option_labels(),
					'entries' => Logistica_Label_Etiqueta_Mercados::get_admin_entries(),
					'message' => __( 'Mercado restaurado.', 'logistica' ),
				)
			)
		);
	}

	/**
	 * AJAX: restaura un tamaño oculto del catálogo.
	 *
	 * @return void
	 */
	public function restore_etiqueta_tamano() {
		$this->verify_ajax( 'logistica_label_print' );

		$key    = isset( $_POST['key'] ) ? wp_unslash( $_POST['key'] ) : '';
		$result = Logistica_Label_Etiqueta_Tamanos::restore( $key );

		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ), 400 );
		}

		wp_send_json_success(
			$this->etiqueta_catalog_hidden_payload(
				array(
					'key'     => sanitize_text_field( (string) $key ),
					'label'   => sanitize_text_field( (string) $key ),
					'options' => Logistica_Label_Etiqueta_Tamanos::get_options(),
					'entries' => Logistica_Label_Etiqueta_Tamanos::get_admin_entries(),
					'message' => __( 'Tamaño restaurado.', 'logistica' ),
				)
			)
		);
	}

	/**
	 * AJAX: crea un estilo de pedido.
	 *
	 * @return void
	 */
	public function add_pedidos_board() {
		$this->verify_ajax( 'logistica_pedidos_board' );

		$label = isset( $_POST['label'] ) ? wp_unslash( $_POST['label'] ) : '';
		$board = Logistica_Pedidos_Board::add_board( $label );

		if ( ! $board ) {
			wp_send_json_error( array( 'message' => __( 'No se pudo crear el estilo de pedido.', 'logistica' ) ), 400 );
		}

		wp_send_json_success(
			array(
				'board'   => $board,
				'boards'  => Logistica_Pedidos_Board::get_boards(),
				'message' => __( 'Estilo de pedido creado.', 'logistica' ),
			)
		);
	}

	/**
	 * AJAX: renombra un estilo de pedido.
	 *
	 * @return void
	 */
	public function update_pedidos_board() {
		$this->verify_ajax( 'logistica_pedidos_board' );

		$slug  = sanitize_title( (string) ( isset( $_POST['slug'] ) ? wp_unslash( $_POST['slug'] ) : '' ) );
		$label = sanitize_text_field( (string) ( isset( $_POST['label'] ) ? wp_unslash( $_POST['label'] ) : '' ) );

		if ( '' === $slug || '' === $label ) {
			wp_send_json_error( array( 'message' => __( 'No se pudo actualizar el estilo de pedido.', 'logistica' ) ), 400 );
		}

		$boards = Logistica_Pedidos_Board::get_boards();
		$found  = false;

		foreach ( $boards as $index => $board ) {
			if ( (string) ( $board['slug'] ?? '' ) !== $slug ) {
				continue;
			}

			$boards[ $index ]['label'] = $label;
			$found                     = true;
			break;
		}

		if ( ! $found || ! Logistica_Pedidos_Board::save_boards( $boards ) ) {
			wp_send_json_error( array( 'message' => __( 'No se pudo actualizar el estilo de pedido.', 'logistica' ) ), 400 );
		}

		wp_send_json_success(
			array(
				'board'   => array(
					'slug'  => $slug,
					'label' => $label,
				),
				'boards'  => Logistica_Pedidos_Board::get_boards(),
				'message' => __( 'Estilo de pedido actualizado.', 'logistica' ),
			)
		);
	}

	/**
	 * AJAX: elimina un estilo de pedido del listado.
	 *
	 * @return void
	 */
	public function delete_pedidos_board() {
		$this->verify_ajax( 'logistica_pedidos_board' );

		$slug   = sanitize_title( (string) ( isset( $_POST['slug'] ) ? wp_unslash( $_POST['slug'] ) : '' ) );
		$result = Logistica_Pedidos_Board::delete_board( $slug );

		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ) );
		}

		wp_send_json_success(
			array(
				'boards'  => Logistica_Pedidos_Board::get_boards(),
				'message' => __( 'Estilo de pedido enviado a papelera.', 'logistica' ),
			)
		);
	}

	/**
	 * AJAX: guarda fila de inventario Hornitos.
	 *
	 * @return void
	 */
	public function save_pedidos_inventario() {
		$this->verify_ajax( 'logistica_pedidos_board' );

		$result = Logistica_Pedidos_Board::save_inventario_row( wp_unslash( $_POST ) );
		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ), 400 );
		}

		$board_slug = sanitize_title( (string) ( $result['board_slug'] ?? '' ) );

		wp_send_json_success(
			array(
				'row'              => Logistica_Pedidos_Board::format_inventario_client( $result ),
				'articulo_options' => Logistica_Pedidos_Board::get_inventario_articulo_options( $board_slug ),
			)
		);
	}

	/**
	 * AJAX: elimina fila de inventario.
	 *
	 * @return void
	 */
	public function delete_pedidos_inventario() {
		$this->verify_ajax( 'logistica_pedidos_board' );

		$id       = isset( $_POST['id'] ) ? (int) wp_unslash( $_POST['id'] ) : 0;
		$existing = Logistica_Pedidos_Board::get_inventario_row_by_id( $id );
		$board_slug = is_array( $existing ) ? sanitize_title( (string) ( $existing['board_slug'] ?? '' ) ) : '';
		$result     = Logistica_Pedidos_Board::delete_inventario_row( $id );
		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ), 400 );
		}

		wp_send_json_success(
			array(
				'articulo_options' => Logistica_Pedidos_Board::get_inventario_articulo_options( $board_slug ),
			)
		);
	}

	/**
	 * AJAX: suma stock Latingraf.
	 *
	 * @return void
	 */
	public function add_pedidos_stock() {
		$this->verify_ajax( 'logistica_pedidos_board' );

		$id       = isset( $_POST['inventario_id'] ) ? (int) wp_unslash( $_POST['inventario_id'] ) : 0;
		$cantidad = isset( $_POST['cantidad'] ) ? wp_unslash( $_POST['cantidad'] ) : 0;
		$result   = Logistica_Pedidos_Board::add_stock_latingraf( $id, $cantidad );

		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ), 400 );
		}

		wp_send_json_success(
			array(
				'row' => Logistica_Pedidos_Board::format_inventario_client( $result ),
			)
		);
	}

	/**
	 * AJAX: resta stock Latingraf.
	 *
	 * @return void
	 */
	public function subtract_pedidos_stock() {
		$this->verify_ajax( 'logistica_pedidos_board' );

		$id       = isset( $_POST['inventario_id'] ) ? (int) wp_unslash( $_POST['inventario_id'] ) : 0;
		$cantidad = isset( $_POST['cantidad'] ) ? wp_unslash( $_POST['cantidad'] ) : 0;
		$result   = Logistica_Pedidos_Board::subtract_stock_latingraf( $id, $cantidad );

		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ), 400 );
		}

		wp_send_json_success(
			array(
				'row' => Logistica_Pedidos_Board::format_inventario_client( $result ),
			)
		);
	}

	/**
	 * AJAX: entregas de inventario (solo sincronización desde OC).
	 *
	 * @return void
	 */
	public function save_pedidos_entrega() {
		$this->verify_ajax( 'logistica_pedidos_board' );

		wp_send_json_error(
			array(
				'message' => __( 'Las entregas del inventario se sincronizan desde las órdenes de compra. No se pueden editar manualmente.', 'logistica' ),
			),
			403
		);
	}

	/**
	 * AJAX: guarda orden de compra y línea.
	 *
	 * @return void
	 */
	public function save_pedidos_orden_linea() {
		$this->verify_ajax( 'logistica_pedidos_board' );

		$orden_raw = isset( $_POST['orden_json'] ) ? wp_unslash( $_POST['orden_json'] ) : '';
		$linea_raw = isset( $_POST['linea_json'] ) ? wp_unslash( $_POST['linea_json'] ) : '';
		$orden     = json_decode( (string) $orden_raw, true );
		$linea     = json_decode( (string) $linea_raw, true );

		if ( ! is_array( $orden ) || ! is_array( $linea ) ) {
			wp_send_json_error( array( 'message' => __( 'Datos de OC no válidos.', 'logistica' ) ), 400 );
		}

		$orden_saved = Logistica_Pedidos_Board::save_orden_compra( $orden );
		if ( is_wp_error( $orden_saved ) ) {
			wp_send_json_error( array( 'message' => $orden_saved->get_error_message() ), 400 );
		}

		$linea['orden_id'] = (int) ( $orden_saved['id'] ?? 0 );
		$result            = Logistica_Pedidos_Board::save_orden_linea( $linea );
		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ), 400 );
		}

		$response = array(
			'orden'  => Logistica_Pedidos_Board::format_orden_compra_client( $orden_saved ),
			'linea'  => $result['linea'] ?? array(),
		);

		if ( ! empty( $result['entrega_sync'] ) && is_wp_error( $result['entrega_sync'] ) ) {
			$response['sync_warning'] = $result['entrega_sync']->get_error_message();
		} elseif ( ! empty( $result['entrega_sync'] ) ) {
			$response['entrega_sync'] = $result['entrega_sync'];
			$board_slug               = sanitize_title( (string) ( $orden_saved['board_slug'] ?? '' ) );
			if ( Logistica_Pedidos_Board::board_uses_inventario( $board_slug ) ) {
				$referencia = (string) ( $result['linea']['articulo'] ?? '' );
				$rows       = Logistica_Pedidos_Board::get_inventario_rows( $board_slug );
				$formatted  = array();
				foreach ( $rows as $row ) {
					if ( Logistica_Pedidos_Board::normalize_referencia( $row['referencia'] ?? '' ) === Logistica_Pedidos_Board::normalize_referencia( $referencia ) ) {
						$formatted[] = Logistica_Pedidos_Board::format_inventario_client( $row );
					}
				}
				$response['inventario_rows'] = $formatted;
			}
		}

		wp_send_json_success( $response );
	}

	/**
	 * AJAX: guarda entrega parcial de línea de OC.
	 *
	 * @return void
	 */
	public function save_pedidos_orden_linea_entrega() {
		$this->verify_ajax( 'logistica_pedidos_board' );

		$result = Logistica_Pedidos_Board::save_orden_linea_entrega( wp_unslash( $_POST ) );
		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ), 400 );
		}

		$linea = is_array( $result['linea'] ?? null ) ? $result['linea'] : array();
		$response = array(
			'linea' => $linea,
		);

		if ( ! empty( $result['entrega_sync'] ) && is_wp_error( $result['entrega_sync'] ) ) {
			$response['sync_warning'] = $result['entrega_sync']->get_error_message();
		} elseif ( ! empty( $result['entrega_sync'] ) ) {
			$response['entrega_sync'] = $result['entrega_sync'];
		}

		global $wpdb;
		$orden_id   = (int) ( $linea['orden_id'] ?? 0 );
		$board_slug = '';
		if ( $orden_id > 0 ) {
			$orden = $wpdb->get_row(
				$wpdb->prepare(
					'SELECT board_slug FROM ' . Logistica_Database::table( 'pedidos_ordenes_compra' ) . ' WHERE id = %d',
					$orden_id
				),
				ARRAY_A
			);
			$board_slug = sanitize_title( (string) ( $orden['board_slug'] ?? '' ) );
		}
		$referencia = (string) ( $linea['articulo'] ?? '' );
		if ( '' !== $board_slug && '' !== $referencia && Logistica_Pedidos_Board::board_uses_inventario( $board_slug ) ) {
			$rows      = Logistica_Pedidos_Board::get_inventario_rows( $board_slug );
			$formatted = array();
			foreach ( $rows as $row ) {
				if ( Logistica_Pedidos_Board::normalize_referencia( $row['referencia'] ?? '' ) === Logistica_Pedidos_Board::normalize_referencia( $referencia ) ) {
					$formatted[] = Logistica_Pedidos_Board::format_inventario_client( $row );
				}
			}
			if ( ! empty( $formatted ) ) {
				$response['inventario_rows'] = $formatted;
			}
		}

		wp_send_json_success( $response );
	}

	/**
	 * AJAX: elimina línea de OC.
	 *
	 * @return void
	 */
	public function delete_pedidos_orden_linea() {
		$this->verify_ajax( 'logistica_pedidos_board' );

		$id     = isset( $_POST['id'] ) ? (int) wp_unslash( $_POST['id'] ) : 0;
		$result = Logistica_Pedidos_Board::delete_orden_linea( $id );
		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ), 400 );
		}

		wp_send_json_success();
	}

	/**
	 * AJAX: lista clientes del cuadro de pedidos.
	 *
	 * @return void
	 */
	public function get_pedidos_clientes() {
		$this->verify_ajax( 'logistica_pedidos_board' );

		wp_send_json_success(
			array(
				'clientes' => Logistica_Pedidos_Board::get_clientes(),
			)
		);
	}

	/**
	 * AJAX: guarda cliente del cuadro de pedidos.
	 *
	 * @return void
	 */
	public function save_pedidos_cliente() {
		$this->verify_ajax( 'logistica_pedidos_board' );

		$raw     = isset( $_POST['cliente_json'] ) ? wp_unslash( $_POST['cliente_json'] ) : '';
		$payload = json_decode( (string) $raw, true );
		if ( ! is_array( $payload ) ) {
			wp_send_json_error( array( 'message' => __( 'Datos de cliente no válidos.', 'logistica' ) ), 400 );
		}

		$result = Logistica_Pedidos_Board::save_cliente( $payload );
		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ), 400 );
		}

		wp_send_json_success(
			array(
				'cliente'  => $result,
				'clientes' => Logistica_Pedidos_Board::get_clientes(),
				'message'  => __( 'Cliente guardado.', 'logistica' ),
			)
		);
	}

	/**
	 * AJAX: elimina cliente del cuadro de pedidos.
	 *
	 * @return void
	 */
	public function delete_pedidos_cliente() {
		$this->verify_ajax( 'logistica_pedidos_board' );

		$id     = isset( $_POST['id'] ) ? (int) wp_unslash( $_POST['id'] ) : 0;
		$result = Logistica_Pedidos_Board::delete_cliente( $id );
		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ), 400 );
		}

		wp_send_json_success(
			array(
				'clientes' => Logistica_Pedidos_Board::get_clientes(),
				'message'  => __( 'Cliente eliminado.', 'logistica' ),
			)
		);
	}

	/**
	 * Includes a view file.
	 *
	 * @param string $file View file name.
	 * @return void
	 */
	private function render_view( $file ) {
		$view = LOGISTICA_PLUGIN_DIR . 'admin/views/' . $file;
		if ( file_exists( $view ) ) {
			include $view;
		}
	}

	/**
	 * Verifies capability and nonce.
	 *
	 * @param string $nonce_action Nonce action.
	 * @return void
	 */
	private function verify_request( $nonce_action ) {
		if ( ! Logistica_Roles::current_user_can_action( $nonce_action ) ) {
			wp_die( esc_html__( 'No tienes permisos para realizar esta acción.', 'logistica' ) );
		}

		check_admin_referer( $nonce_action );
	}

	/**
	 * Verifies capability and AJAX nonce.
	 *
	 * @param string $nonce_action Nonce action.
	 * @return void
	 */
	private function festivo_dates_for_week( $week_start ) {
		$week_year  = (int) substr( (string) $week_start, 0, 4 );
		$panel_year = (int) wp_date( 'Y' );
		$from       = min( $panel_year - 1, $week_year > 0 ? $week_year - 1 : $panel_year - 1 );
		$to         = max( $panel_year + 2, $week_year > 0 ? $week_year + 1 : $panel_year + 2 );

		return Logistica_Settings::get_festivo_dates_map( $from, $to );
	}

	/**
	 * AJAX: líneas de pedido vigentes sin programar en planeación.
	 *
	 * @return void
	 */
	public function get_pedidos_pendientes_planeacion() {
		$module = isset( $_POST['module'] ) ? sanitize_key( wp_unslash( (string) $_POST['module'] ) ) : '';
		if ( ! Logistica_Pedidos_Planeacion_Bridge::is_valid_module( $module ) ) {
			wp_send_json_error( array( 'message' => __( 'Módulo no válido.', 'logistica' ) ), 400 );
		}

		$this->verify_ajax( Logistica_Pedidos_Planeacion_Bridge::nonce_action_for_module( $module ) );

		$lineas = Logistica_Pedidos_Planeacion_Bridge::get_pending_lineas( $module );

		wp_send_json_success(
			array(
				'lineas' => $lineas,
				'count'  => count( $lineas ),
			)
		);
	}

	/**
	 * AJAX: programa líneas de pedido seleccionadas en la planeación actual.
	 *
	 * @return void
	 */
	public function programar_pedidos_planeacion() {
		$module = isset( $_POST['module'] ) ? sanitize_key( wp_unslash( (string) $_POST['module'] ) ) : '';
		if ( ! Logistica_Pedidos_Planeacion_Bridge::is_valid_module( $module ) ) {
			wp_send_json_error( array( 'message' => __( 'Módulo no válido.', 'logistica' ) ), 400 );
		}

		$this->verify_ajax( Logistica_Pedidos_Planeacion_Bridge::nonce_action_for_module( $module ) );

		$week    = isset( $_POST['semana_inicio'] ) ? sanitize_text_field( wp_unslash( (string) $_POST['semana_inicio'] ) ) : '';
		$machine = isset( $_POST['maquina'] ) ? sanitize_key( wp_unslash( (string) $_POST['maquina'] ) ) : '';
		$day     = isset( $_POST['day'] ) ? (int) wp_unslash( $_POST['day'] ) : 0;

		$linea_ids_raw = isset( $_POST['linea_ids'] ) ? wp_unslash( $_POST['linea_ids'] ) : array();
		$linea_ids     = array();

		if ( is_string( $linea_ids_raw ) ) {
			$decoded = json_decode( $linea_ids_raw, true );
			$linea_ids = is_array( $decoded ) ? $decoded : array();
		} elseif ( is_array( $linea_ids_raw ) ) {
			$linea_ids = $linea_ids_raw;
		}

		if ( '' === $machine ) {
			$machine = Logistica_Pedidos_Planeacion_Bridge::MODULE_FORMADO === $module
				? Logistica_Forming_Planning::default_machine_slug()
				: 'sm74';
		}

		$result = Logistica_Pedidos_Planeacion_Bridge::programar_lineas( $module, $week, $machine, $day, $linea_ids );
		if ( is_wp_error( $result ) ) {
			wp_send_json_error(
				array(
					'message' => $result->get_error_message(),
					'data'    => $result->get_error_data(),
				),
				400
			);
		}

		wp_send_json_success(
			array(
				'message'  => sprintf(
					/* translators: %d: number of scheduled tasks */
					_n(
						'Se programó %d pedido en la planeación.',
						'Se programaron %d pedidos en la planeación.',
						(int) ( $result['created'] ?? 0 ),
						'logistica'
					),
					(int) ( $result['created'] ?? 0 )
				),
				'schedule' => $result['schedule'] ?? array(),
				'created'  => (int) ( $result['created'] ?? 0 ),
				'skipped'  => array_map( 'intval', (array) ( $result['skipped'] ?? array() ) ),
				'time'     => Logistica_User_Panel::current_time_hms(),
			)
		);
	}

	private function verify_ajax( $nonce_action ) {
		if ( ! Logistica_Roles::current_user_can_action( $nonce_action ) ) {
			wp_send_json_error( array( 'message' => __( 'Sin permisos.', 'logistica' ) ), 403 );
		}

		check_ajax_referer( $nonce_action, 'nonce' );
	}

	/**
	 * Verifica permiso y nonce para catálogos de selects (solo configuración).
	 *
	 * @return void
	 */
	private function verify_select_catalog_ajax() {
		if ( ! Logistica_Roles::user_can_manage_config() ) {
			wp_send_json_error( array( 'message' => __( 'Sin permisos para configurar listas.', 'logistica' ) ), 403 );
		}

		check_ajax_referer( 'logistica_select_catalog', 'nonce' );
	}

	/**
	 * @return array{catalog:string,entries:list<array<string,mixed>>,options:array}
	 */
	private function select_catalog_success_payload( $catalog_id ) {
		return array(
			'catalog' => sanitize_key( (string) $catalog_id ),
			'entries' => Logistica_Select_Catalog::get_admin_entries( $catalog_id ),
			'options' => Logistica_Select_Catalog::get_options( $catalog_id ),
		);
	}

	/**
	 * AJAX: agrega opción a un catálogo de select.
	 *
	 * @return void
	 */
	public function select_catalog_add() {
		$this->verify_select_catalog_ajax();

		$catalog = isset( $_POST['catalog'] ) ? sanitize_key( wp_unslash( $_POST['catalog'] ) ) : '';
		$label   = isset( $_POST['label'] ) ? wp_unslash( $_POST['label'] ) : '';
		$key     = isset( $_POST['key'] ) ? wp_unslash( $_POST['key'] ) : '';
		$result  = Logistica_Select_Catalog::add( $catalog, $label, $key );

		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ), 400 );
		}

		$payload = array_merge(
			$this->select_catalog_success_payload( $catalog ),
			array(
				'entry'   => $result,
				'message' => __( 'Opción agregada.', 'logistica' ),
			)
		);

		if ( 'closing_base_types' === $catalog && class_exists( 'Logistica_Order_Closing', false ) ) {
			$new_key = (string) ( $result['key'] ?? '' );
			$fields_result = Logistica_Order_Closing::save_base_type_fields(
				$new_key,
				Logistica_Order_Closing::default_fields_for_unknown_base_type()
			);
			if ( ! is_wp_error( $fields_result ) ) {
				$payload['baseTypeFields'] = $fields_result;
			}
		}

		wp_send_json_success( $payload );
	}

	/**
	 * AJAX: actualiza opción de un catálogo de select.
	 *
	 * @return void
	 */
	public function select_catalog_update() {
		$this->verify_select_catalog_ajax();

		$catalog = isset( $_POST['catalog'] ) ? sanitize_key( wp_unslash( $_POST['catalog'] ) ) : '';
		$key     = isset( $_POST['key'] ) ? wp_unslash( $_POST['key'] ) : '';
		$label   = isset( $_POST['label'] ) ? wp_unslash( $_POST['label'] ) : '';

		if ( '__empty__' === $key ) {
			$key = '';
		}

		$result = Logistica_Select_Catalog::update( $catalog, $key, $label );

		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ), 400 );
		}

		$payload = array_merge(
			$this->select_catalog_success_payload( $catalog ),
			array(
				'entry'   => $result,
				'message' => __( 'Opción actualizada.', 'logistica' ),
			)
		);

		if ( ! empty( $result['propagated'] ) ) {
			$payload['message'] = sprintf(
				/* translators: %d: number of fields updated in Operaciones */
				_n(
					'Opción actualizada y sincronizada en %d campo de Operaciones.',
					'Opción actualizada y sincronizada en %d campos de Operaciones.',
					(int) $result['propagated'],
					'logistica'
				),
				(int) $result['propagated']
			);
		}

		if ( 'closing_base_types' === $catalog && class_exists( 'Logistica_Order_Closing', false ) ) {
			$fields_raw = array();
			if ( isset( $_POST['fields'] ) ) {
				$fields_raw = wp_unslash( $_POST['fields'] );
			} elseif ( isset( $_POST['fields'] ) && is_string( $_POST['fields'] ) ) {
				$decoded = json_decode( wp_unslash( $_POST['fields'] ), true );
				$fields_raw = is_array( $decoded ) ? $decoded : array();
			}
			// Si el cliente envió el flag, respetar lista vacía (todos desmarcados).
			if ( isset( $_POST['fields_submitted'] ) || isset( $_POST['fields'] ) ) {
				if ( ! is_array( $fields_raw ) ) {
					$fields_raw = array();
				}
				$fields_result = Logistica_Order_Closing::save_base_type_fields( (string) ( $result['key'] ?? $key ), $fields_raw );
				if ( ! is_wp_error( $fields_result ) ) {
					$payload['baseTypeFields'] = $fields_result;
					$payload['message']        = __( 'Tipo de base y campos actualizados.', 'logistica' );
				}
			} else {
				$payload['baseTypeFields'] = Logistica_Order_Closing::get_base_type_fields_map();
			}
		}

		wp_send_json_success( $payload );
	}

	/**
	 * AJAX: consulta uso de una opción de catálogo de select.
	 *
	 * @return void
	 */
	public function select_catalog_check_usage() {
		$this->verify_select_catalog_ajax();

		$catalog = isset( $_POST['catalog'] ) ? sanitize_key( wp_unslash( $_POST['catalog'] ) ) : '';
		$key     = isset( $_POST['key'] ) ? wp_unslash( $_POST['key'] ) : '';

		if ( ! Logistica_Select_Catalog::exists( $catalog ) ) {
			wp_send_json_error( array( 'message' => __( 'Catálogo no válido.', 'logistica' ) ), 400 );
		}

		$usage = Logistica_Select_Catalog_Usage::count( $catalog, $key );

		wp_send_json_success(
			array(
				'usage'   => $usage,
				'message' => Logistica_Select_Catalog_Usage::message( $catalog, $key ),
			)
		);
	}

	/**
	 * AJAX: renderiza el HTML de una pestaña bajo demanda (carga diferida).
	 *
	 * @return void
	 */
	public function render_tab_panel() {
		if ( ! Logistica_Roles::user_can_access_plugin() ) {
			wp_send_json_error( array( 'message' => __( 'Sin permisos.', 'logistica' ) ), 403 );
		}

		check_ajax_referer( 'logistica_tab_panel', 'nonce' );

		require_once LOGISTICA_PLUGIN_DIR . 'admin/views/partials.php';

		$tab     = isset( $_POST['tab'] ) ? sanitize_key( wp_unslash( $_POST['tab'] ) ) : '';
		$context = isset( $_POST['context'] ) ? sanitize_key( wp_unslash( $_POST['context'] ) ) : 'operations';
		$context = Logistica_Tabs::normalize_context( $context );

		if ( '' === $tab || ! array_key_exists( $tab, logistica_get_nav_tabs() ) ) {
			wp_send_json_error( array( 'message' => __( 'Pestaña no válida.', 'logistica' ) ), 400 );
		}

		if ( Logistica_Tabs::CONTEXT_CONFIG === $context ) {
			if ( ! Logistica_Roles::user_can_manage_config() ) {
				wp_send_json_error( array( 'message' => __( 'Sin permisos de configuración.', 'logistica' ) ), 403 );
			}
			$visible = array_keys( logistica_get_nav_tabs_for_context( 'config' ) );
		} else {
			if ( ! Logistica_Roles::user_can_tab( $tab, 'backend' ) && ! Logistica_Roles::user_can_manage_config() ) {
				wp_send_json_error( array( 'message' => __( 'Sin permisos para esta pestaña.', 'logistica' ) ), 403 );
			}
			$visible = array_keys( logistica_get_nav_tabs_for_context( 'operations' ) );
		}

		if ( ! in_array( $tab, $visible, true ) ) {
			wp_send_json_error( array( 'message' => __( 'Pestaña no disponible.', 'logistica' ) ), 403 );
		}

		$html = logistica_render_tab_panel_inner( $tab, $context );
		if ( '' === trim( $html ) ) {
			wp_send_json_error( array( 'message' => __( 'No se pudo cargar la pestaña.', 'logistica' ) ), 500 );
		}

		wp_send_json_success(
			array(
				'tab'  => $tab,
				'html' => $html,
			)
		);
	}

	/**
	 * AJAX: elimina opción de un catálogo de select.
	 *
	 * @return void
	 */
	public function select_catalog_delete() {
		$this->verify_select_catalog_ajax();

		$catalog = isset( $_POST['catalog'] ) ? sanitize_key( wp_unslash( $_POST['catalog'] ) ) : '';
		$key     = isset( $_POST['key'] ) ? wp_unslash( $_POST['key'] ) : '';

		if ( '__empty__' === $key ) {
			$key = '';
		}

		$result = Logistica_Select_Catalog::remove( $catalog, $key );

		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ), 400 );
		}

		$payload = array_merge(
			$this->select_catalog_success_payload( $catalog ),
			array(
				'message' => __( 'Opción eliminada.', 'logistica' ),
			)
		);

		if ( 'closing_base_types' === $catalog && class_exists( 'Logistica_Order_Closing', false ) ) {
			Logistica_Order_Closing::delete_base_type_fields( $key );
			$payload['baseTypeFields'] = Logistica_Order_Closing::get_base_type_fields_map();
		}

		wp_send_json_success( $payload );
	}

	/**
	 * Consulta auditoría, papelera o historial de versiones.
	 *
	 * @return void
	 */
	public function query_lifecycle() {
		if ( ! Logistica_Roles::user_can_view_lifecycle() ) {
			wp_send_json_error( array( 'message' => __( 'Sin permisos para consultar trazabilidad.', 'logistica' ) ), 403 );
		}

		check_ajax_referer( 'logistica_lifecycle', 'nonce' );

		$scope = sanitize_key( wp_unslash( (string) ( $_POST['scope'] ?? 'audit' ) ) );
		if ( ! in_array( $scope, array( 'audit', 'trash', 'versions' ), true ) ) {
			$scope = 'audit';
		}

		$args = array(
			'page'       => max( 1, (int) ( $_POST['page'] ?? 1 ) ),
			'page_size'  => max( 1, min( 100, (int) ( $_POST['page_size'] ?? 25 ) ) ),
			'search'     => sanitize_text_field( wp_unslash( (string) ( $_POST['search'] ?? '' ) ) ),
			'module_key' => sanitize_key( wp_unslash( (string) ( $_POST['module_key'] ?? '' ) ) ),
			'status'     => sanitize_key( wp_unslash( (string) ( $_POST['status'] ?? 'deleted' ) ) ),
			'date_from'  => sanitize_text_field( wp_unslash( (string) ( $_POST['date_from'] ?? '' ) ) ),
			'date_to'    => sanitize_text_field( wp_unslash( (string) ( $_POST['date_to'] ?? '' ) ) ),
			'created_by' => max( 0, (int) ( $_POST['created_by'] ?? 0 ) ),
			'deleted_by' => max( 0, (int) ( $_POST['deleted_by'] ?? 0 ) ),
		);

		$result = Logistica_Entity_Lifecycle::query( $scope, $args );
		wp_send_json_success( $result );
	}

	/**
	 * @return void
	 */
	public function restore_lifecycle_trash() {
		if ( ! Logistica_Roles::user_can_view_lifecycle() ) {
			wp_send_json_error( array( 'message' => __( 'Sin permisos.', 'logistica' ) ), 403 );
		}

		check_ajax_referer( 'logistica_lifecycle', 'nonce' );

		$trash_id = max( 0, (int) ( $_POST['trash_id'] ?? 0 ) );
		$result   = Logistica_Entity_Lifecycle::restore_from_trash( $trash_id );

		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ) );
		}

		wp_send_json_success( array( 'item' => $result ) );
	}

	/**
	 * @return void
	 */
	public function restore_lifecycle_version() {
		if ( ! Logistica_Roles::user_can_view_lifecycle() ) {
			wp_send_json_error( array( 'message' => __( 'Sin permisos.', 'logistica' ) ), 403 );
		}

		check_ajax_referer( 'logistica_lifecycle', 'nonce' );

		$version_id = max( 0, (int) ( $_POST['version_id'] ?? 0 ) );
		$result     = Logistica_Entity_Lifecycle::restore_version( $version_id );

		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ) );
		}

		wp_send_json_success( array( 'item' => $result ) );
	}

	/**
	 * @return void
	 */
	public function purge_lifecycle_trash() {
		if ( ! Logistica_Roles::user_can_view_lifecycle() ) {
			wp_send_json_error( array( 'message' => __( 'Sin permisos.', 'logistica' ) ), 403 );
		}

		check_ajax_referer( 'logistica_lifecycle', 'nonce' );

		$trash_id = max( 0, (int) ( $_POST['trash_id'] ?? 0 ) );
		$result   = Logistica_Trash::purge( $trash_id );

		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ) );
		}

		wp_send_json_success();
	}

	/**
	 * @return void
	 */
	public function export_lifecycle() {
		if ( ! Logistica_Roles::user_can_view_lifecycle() ) {
			wp_die( esc_html__( 'Sin permisos para exportar trazabilidad.', 'logistica' ) );
		}

		check_admin_referer( 'logistica_lifecycle' );

		$scope = sanitize_key( wp_unslash( (string) ( $_GET['scope'] ?? 'audit' ) ) );
		if ( ! in_array( $scope, array( 'audit', 'trash', 'versions' ), true ) ) {
			$scope = 'audit';
		}

		$args = array(
			'search'     => sanitize_text_field( wp_unslash( (string) ( $_GET['search'] ?? '' ) ) ),
			'module_key' => sanitize_key( wp_unslash( (string) ( $_GET['module_key'] ?? '' ) ) ),
			'status'     => sanitize_key( wp_unslash( (string) ( $_GET['status'] ?? 'deleted' ) ) ),
			'date_from'  => sanitize_text_field( wp_unslash( (string) ( $_GET['date_from'] ?? '' ) ) ),
			'date_to'    => sanitize_text_field( wp_unslash( (string) ( $_GET['date_to'] ?? '' ) ) ),
		);

		Logistica_Entity_Lifecycle::export_csv( $scope, $args );
	}

	/**
	 * Redirects to a plugin page with a status flag.
	 *
	 * @param string $page Page slug.
	 * @param string $status Status value.
	 * @return void
	 */
	private function redirect_with_status( $page, $status ) {
		require_once LOGISTICA_PLUGIN_DIR . 'admin/views/partials.php';

		$referer = wp_get_referer();
		if ( $referer ) {
			wp_safe_redirect(
				add_query_arg(
					'logistica_status',
					$status,
					remove_query_arg( 'logistica_status', $referer )
				)
			);
			exit;
		}

		wp_safe_redirect(
			add_query_arg(
				array(
					'logistica_status' => $status,
				),
				logistica_tab_url( $page, 'admin' )
			)
		);
		exit;
	}
}
