<?php
/**
 * Admin plugin for BEZ Tax Calendar
 * Calendario Tributario DIAN Centralizado
 */

use dokuwiki\Extension\AdminPlugin;

if (file_exists(DOKU_PLUGIN . 'bez/meta/path_helper.php')) require_once(DOKU_PLUGIN . 'bez/meta/path_helper.php');

class admin_plugin_beztaxcalendar extends AdminPlugin {

    protected $helper;

    /**
     * Constructor
     */
    public function __construct() {
        $this->helper = plugin_load('helper', 'beztaxcalendar');
    }

    /**
     * Acceso para usuarios con nivel manager
     */
    public function forAdminOnly() {
        return false;
    }

    /**
     * Verifica si el usuario tiene permisos de manager, admin o director
     */
    protected function isManager() {
        global $USERINFO, $INPUT;

        $user = $INPUT->server->str('REMOTE_USER');
        $grps = $USERINFO['grps'] ?? [];

        // 1. Grupo "admin" NO tiene acceso a este plugin
        if (in_array('admin', $grps) && !in_array('director', $grps)) {
            // Si es SOLO admin (sin ser director), denegar acceso
            // A menos que sea superuser nativo de DokuWiki
            if (!auth_isadmin()) {
                return false;
            }
        }

        // 2. Grupo "director" tiene acceso total
        if (in_array('director', $grps)) {
            return true;
        }

        // 3. Superuser nativo de DokuWiki siempre tiene acceso
        if (auth_isadmin()) {
            return true;
        }

        // 4. Manager nativo de DokuWiki
        if (auth_ismanager()) {
            return true;
        }

        // 5. Verificar via niccadminmenu (manager virtual, grupo 'sie', etc.)
        $menuHelper = plugin_load('helper', 'niccadminmenu');
        if ($menuHelper && method_exists($menuHelper, 'getCurrentRole')) {
            $role = $menuHelper->getCurrentRole();
            if ($role === 'superuser' || $role === 'manager') {
                return true;
            }
        }

        return false;
    }

    /**
     * Ordenar en el menú después de otros plugins BEZ
     */
    public function getMenuSort() {
        return 15;
    }

    /**
     * Texto del menú
     */
    public function getMenuText($language) {
        return $this->getLang('menu');
    }

    /**
     * Icono del menú - DIAN
     */
    public function getMenuIcon() {
        $iconPath = (function_exists('bez_plugin_base') ? bez_plugin_base() : DOKU_PLUGIN) . 'beztaxcalendar/images/admin.png';
        if (file_exists($iconPath)) {
            return $iconPath;
        }
        // Fallback al icono de calendario por defecto
        return function_exists('bez_resolve_path') ? bez_resolve_path('lib/images/menu/calendar-clock.svg') : DOKU_INC . 'lib/images/menu/calendar-clock.svg';
    }

    /**
     * Handle request actions
     */
    public function handle() {
        global $INPUT;

        // Verificar permisos
        if (!$this->isManager()) {
            return;
        }

        $action = $INPUT->str('action');

        // Acciones de solo lectura (descargas de plantillas vacias) no requieren token
        $readOnlyActions = ['download_template', 'export_calendar'];

        if (!empty($action) && !in_array($action, $readOnlyActions) && !checkSecurityToken()) {
            return;
        }

        switch ($action) {
            case 'upload_calendar':
                $this->handleUploadCalendar();
                break;

            case 'verify_calendar':
                $this->handleVerifyCalendar();
                break;

            case 'save_client_obligations':
                $this->handleSaveClientObligations();
                break;

            case 'add_exception':
                $this->handleAddException();
                break;

            case 'delete_exception':
                $this->handleDeleteException();
                break;

            case 'generate_tasks':
                $this->handleGenerateTasks();
                break;

            case 'save_config':
                $this->handleSaveConfig();
                break;

            case 'download_template':
                $this->handleDownloadTemplate();
                break;

            case 'download_file':
                $this->handleDownloadFile();
                break;

            case 'export_pdf':
                $this->handleExportPDF();
                break;

            case 'reset_calendar':
                $this->handleResetCalendar();
                break;

            case 'save_responsible':
                $this->handleSaveResponsible();
                break;

            case 'add_obligation':
                $this->handleAddObligation();
                break;

            case 'update_obligation':
                $this->handleUpdateObligation();
                break;

            case 'delete_obligation':
                $this->handleDeleteObligation();
                break;

            case 'save_calendar_dates':
                $this->handleSaveCalendarDates();
                break;

            case 'add_calendar_row':
                $this->handleAddCalendarRow();
                break;

            case 'delete_calendar_row':
                $this->handleDeleteCalendarRow();
                break;

            case 'export_calendar':
                $this->handleExportCalendar();
                break;

            case 'toggle_row_active':
                $this->handleToggleRowActive();
                break;

            case 'toggle_obligation_active':
                $this->handleToggleObligationActive();
                break;
        }
    }

    /**
     * Descargar el archivo Excel que fue subido por el usuario
     */
    protected function handleDownloadFile() {
        global $INPUT;

        $year = $INPUT->int('year');
        $version = $INPUT->int('version', 0);

        if (!$year) {
            msg('Año no especificado', -1);
            return;
        }

        $file = $this->helper->getCalendarFile($year, $version ?: null);

        if (!$file || empty($file['file_content'])) {
            msg('No se encontró archivo para el año ' . $year, -1);
            return;
        }

        $filename = $file['filename'] ?: "calendario_tributario_{$year}.xlsx";

        header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
        header('Content-Disposition: attachment; filename="' . basename($filename) . '"');
        header('Content-Length: ' . strlen($file['file_content']));
        header('Cache-Control: no-cache, must-revalidate');
        header('Pragma: no-cache');

        echo $file['file_content'];
        exit;
    }

    /**
     * Descarga el template Excel para cargar calendarios
     * Genera dinamicamente con fechas reales (no texto)
     */
    protected function handleDownloadTemplate() {
        // Servir el archivo de plantilla estático
        $templatePath = (function_exists('bez_plugin_base') ? bez_plugin_base() : DOKU_PLUGIN)
            . 'beztaxcalendar/templates/calendario_tributario_template.xlsx';

        if (!file_exists($templatePath)) {
            msg('Plantilla no encontrada: calendario_tributario_template.xlsx', -1);
            return;
        }

        header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
        header('Content-Disposition: attachment; filename="calendario_tributario_template.xlsx"');
        header('Content-Length: ' . filesize($templatePath));
        header('Cache-Control: no-cache, must-revalidate');
        header('Pragma: no-cache');

        readfile($templatePath);
        exit;
    }

    /**
     * Render admin page
     */
    public function html() {
        global $INPUT;

        // Verificar permisos
        if (!$this->isManager()) {
            echo '<div class="error">';
            echo '<p>' . $this->getLang('error_no_permission') . '</p>';
            echo '</div>';
            return;
        }

        $tab = $INPUT->str('tab', 'overview');

        echo '<div class="bez-taxcalendar-admin">';
        echo '<h1>' . $this->getLang('admin_title') . '</h1>';

        // Navigation tabs
        $this->renderTabs($tab);

        // Content based on tab
        switch ($tab) {
            case 'upload':
                $this->renderUploadForm();
                break;

            case 'view':
                $this->renderCalendarView();
                break;

            case 'obligations':
                $this->renderObligations();
                break;

            case 'client':
                $this->renderClientConfig();
                break;

            case 'generate':
                $this->renderTaskGenerator();
                break;

            case 'config':
                $this->renderConfig();
                break;

            case 'overview':
            default:
                $this->renderOverview();
                break;
        }

        echo '</div>';

        // CSS Styles
        $this->renderStyles();
    }

    /**
     * Render navigation tabs
     */
    protected function renderTabs($currentTab) {
        global $ID;
        $tabs = [
            'overview' => $this->getLang('calendar_overview'),
            'upload' => $this->getLang('upload_calendar'),
            'obligations' => $this->getLang('obligations'),
            'view' => $this->getLang('view_calendar'),
            'client' => $this->getLang('client_config'),
            'generate' => $this->getLang('generate_tasks'),
            'config' => 'Instructivo'
        ];

        echo '<div class="taxcal-tabs">';
        foreach ($tabs as $key => $label) {
            $class = ($key === $currentTab) ? 'active' : '';
            $url = wl($ID, ['do' => 'admin', 'page' => 'beztaxcalendar', 'tab' => $key]);
            echo "<a href=\"{$url}\" class=\"taxcal-tab {$class}\">{$label}</a>";
        }
        echo '</div>';
    }

    /**
     * Render Overview tab
     */
    protected function renderOverview() {
        global $ID;
        $overview = $this->helper->getCalendarsOverview();

        // Alerts
        if (!empty($overview['alerts'])) {
            echo '<div class="taxcal-alerts">';
            foreach ($overview['alerts'] as $alert) {
                $alertClass = $alert['type'] === 'error' ? 'alert-error' : 'alert-warning';
                echo "<div class=\"taxcal-alert {$alertClass}\">";
                echo "<span class=\"alert-message\">" . hsc($alert['message']) . "</span>";
                if (!empty($alert['action_url'])) {
                    echo " <a href=\"" . hsc($alert['action_url']) . "\" class=\"btn-small\">" . $this->getLang('btn_' . $alert['action']) . "</a>";
                }
                echo "</div>";
            }
            echo '</div>';
        }

        // Calendar status table
        echo '<h2>' . $this->getLang('calendar_status') . '</h2>';
        echo '<table class="taxcal-table">';
        echo '<thead><tr>';
        echo '<th>' . $this->getLang('year') . '</th>';
        echo '<th>' . $this->getLang('status') . '</th>';
        echo '<th>' . $this->getLang('deadlines_count') . '</th>';
        echo '<th>' . $this->getLang('last_load') . '</th>';
        echo '<th>' . $this->getLang('actions') . '</th>';
        echo '</tr></thead>';
        echo '<tbody>';

        foreach ($overview['calendars'] as $year => $cal) {
            $rowClass = !empty($cal['is_current']) ? 'current-year' : '';
            echo "<tr class=\"{$rowClass}\">";

            // Year
            echo '<td><strong>' . $year . '</strong>';
            if (!empty($cal['is_current'])) {
                echo ' <span class="badge-current">' . $this->getLang('current_year') . '</span>';
            }
            echo '</td>';

            // Status
            echo '<td>' . $this->renderStatusBadge($cal['status']) . '</td>';

            // Deadlines count
            echo '<td>' . ($cal['total_deadlines'] ?: '-') . '</td>';

            // Last load
            echo '<td>';
            if (!empty($cal['loaded_at'])) {
                echo date('d/m/Y', strtotime($cal['loaded_at']));
                if (!empty($cal['version'])) {
                    echo '<br><small>v' . $cal['version'] . '</small>';
                }
            } else {
                echo '-';
            }
            echo '</td>';

            // Actions
            echo '<td class="actions">';
            $baseUrl = wl($ID, ['do' => 'admin', 'page' => 'beztaxcalendar']);

            if ($cal['status'] === 'not_loaded') {
                echo "<a href=\"{$baseUrl}&tab=upload&year={$year}\" class=\"btn-small btn-primary\">" . $this->getLang('btn_load') . "</a> ";
            } else {
                echo "<a href=\"{$baseUrl}&tab=view&year={$year}\" class=\"btn-small\">" . $this->getLang('btn_view') . "</a> ";
                echo "<a href=\"{$baseUrl}&tab=upload&year={$year}\" class=\"btn-small\">" . $this->getLang('btn_update') . "</a> ";

                // Boton descargar archivo Excel subido
                $fileRecord = $this->helper->getCalendarFile($year);
                if ($fileRecord && !empty($fileRecord['file_content'])) {
                    $dlUrl = wl($ID, ['do' => 'admin', 'page' => 'beztaxcalendar', 'action' => 'download_file', 'year' => $year, 'sectok' => getSecurityToken()]);
                    echo "<a href=\"{$dlUrl}\" class=\"btn-small\" title=\"Descargar Excel subido\">&#11015; Excel</a> ";
                }

                if ($cal['status'] === 'draft') {
                    echo "<a href=\"{$baseUrl}&action=verify_calendar&year={$year}&sectok=" . getSecurityToken() . "\" class=\"btn-small btn-success\">" . $this->getLang('btn_verify') . "</a>";
                }
            }
            echo '</td>';

            echo '</tr>';
        }

        echo '</tbody></table>';

        // Quick stats
        $this->renderQuickStats();
    }

    /**
     * Render upload form
     */
    protected function renderUploadForm() {
        global $ID;
        global $INPUT;
        $selectedYear = $INPUT->int('year', date('Y'));

        echo '<h2>' . $this->getLang('upload_title') . '</h2>';

        echo '<div class="taxcal-info-box">';
        echo '<p>' . $this->getLang('upload_info') . '</p>';
        echo '<a href="' . wl($ID, ['do' => 'admin', 'page' => 'beztaxcalendar', 'action' => 'download_template', 'sectok' => getSecurityToken()]) . '" class="btn-secondary">';
        echo '&#11015; Descargar calendario Excel';
        echo '</a>';

        $existingFile = $this->helper->getCalendarFile($selectedYear);
        echo '</div>';

        echo '<form method="post" enctype="multipart/form-data" class="taxcal-form">';
        echo '<input type="hidden" name="sectok" value="' . getSecurityToken() . '">';
        echo '<input type="hidden" name="action" value="upload_calendar">';

        echo '<div class="form-row">';
        echo '<label for="year">' . $this->getLang('select_year') . ':</label>';
        echo '<select name="year" id="year" required>';
        for ($y = date('Y'); $y <= date('Y') + 2; $y++) {
            $selected = ($y == $selectedYear) ? 'selected' : '';
            echo "<option value=\"{$y}\" {$selected}>{$y}</option>";
        }
        echo '</select>';
        echo '</div>';

        echo '<div class="form-row">';
        echo '<label for="excel_file">' . $this->getLang('select_file') . ':</label>';
        echo '<input type="file" name="excel_file" id="excel_file" accept=".xlsx,.xls,.csv" required>';
        echo '<small style="display:block;color:#666;margin-top:5px;">Formatos soportados: Excel (.xlsx, .xls) y CSV (.csv)</small>';
        echo '</div>';

        echo '<div class="form-row">';
        echo '<label class="checkbox-label">';
        echo '<input type="checkbox" name="preview_only" value="1">';
        echo $this->getLang('preview_only');
        echo '</label>';
        echo '</div>';

        echo '<div class="form-actions">';
        echo '<button type="submit" class="btn-primary">' . $this->getLang('btn_upload') . '</button>';
        echo '</div>';

        echo '</form>';

        // Boton Reiniciar Calendario (solo si existe un calendario cargado)
        if ($existingFile && !empty($existingFile['file_content'])) {
            echo '<div style="margin-top:20px;padding-top:15px;border-top:1px solid #e2e8f0;">';
            echo '<form method="post" onsubmit="return confirm(\'ATENCION: Esta accion eliminara completamente:\\n\\n- Calendario del ano ' . $selectedYear . '\\n- Todas las obligaciones tributarias\\n- Configuraciones de obligaciones por cliente\\n- Excepciones de fechas\\n- Registro de tareas generadas\\n\\nLos datos no se pueden recuperar.\\nLas tareas ya creadas en proyectos BEZ NO se eliminan.\\n\\nDesea continuar?\');">';
            echo '<input type="hidden" name="sectok" value="' . getSecurityToken() . '">';
            echo '<input type="hidden" name="do" value="admin">';
            echo '<input type="hidden" name="page" value="beztaxcalendar">';
            echo '<input type="hidden" name="tab" value="upload">';
            echo '<input type="hidden" name="action" value="reset_calendar">';
            echo '<input type="hidden" name="year" value="' . $selectedYear . '">';
            echo '<button type="submit" class="btn-danger" style="background:#dc2626;color:#fff;padding:8px 20px;border:none;border-radius:6px;cursor:pointer;font-weight:600;">';
            echo '&#9888; Reiniciar Calendario ' . $selectedYear;
            echo '</button>';
            echo '<small style="display:block;color:#666;margin-top:5px;">Elimina el calendario, obligaciones tributarias, configuraciones por cliente y excepciones. Las tareas ya creadas en proyectos BEZ no se eliminan.</small>';
            echo '</form>';
            echo '</div>';
        }

        // Show upload result at the bottom
        if (isset($_SESSION['taxcal_upload_result'])) {
            $this->renderUploadResult($_SESSION['taxcal_upload_result']);
            unset($_SESSION['taxcal_upload_result']);
        }
    }

    /**
     * Render calendar view
     */
    protected function renderCalendarView() {
        global $INPUT, $ID;
        $year = $INPUT->int('year', date('Y'));

        $years = $this->helper->getAvailableYearsWithStatus();

        echo '<div class="taxcal-header-row">';
        echo '<h2>' . sprintf($this->getLang('view_title'), $year) . '</h2>';
        echo '<div style="display:flex;gap:8px;align-items:center;">';
        $this->renderPdfButton('view', ['year' => $year]);
        $exportUrl = wl($ID, ['do' => 'admin', 'page' => 'beztaxcalendar', 'action' => 'export_calendar', 'year' => $year]);
        echo '<a href="' . $exportUrl . '" class="btn-secondary" style="display:inline-flex;align-items:center;gap:4px;padding:6px 14px;background:#1565c0;color:#fff;text-decoration:none;border-radius:6px;font-size:13px;" title="Exportar calendario editable">';
        echo '&#11015; Exportar Excel</a>';
        // Botón Guardar Todo general
        echo '<form method="post" id="taxcal_save_all_form" style="display:inline;margin:0;" onsubmit="return syncAllTables()">';
        echo '<input type="hidden" name="sectok" value="' . getSecurityToken() . '">';
        echo '<input type="hidden" name="do" value="admin">';
        echo '<input type="hidden" name="page" value="beztaxcalendar">';
        echo '<input type="hidden" name="tab" value="view">';
        echo '<input type="hidden" name="year" value="' . $year . '">';
        echo '<input type="hidden" name="action" value="save_calendar_dates">';
        echo '<div id="taxcal_save_all_data"></div>';
        echo '<button type="submit" style="display:inline-flex;align-items:center;gap:4px;padding:6px 14px;background:#38a169;color:#fff;border:none;border-radius:6px;font-size:13px;font-weight:600;cursor:pointer;" title="Guardar todos los cambios">&#128190; Guardar Todo</button>';
        echo '</form>';
        echo '</div>';
        echo '</div>';

        // Year selector
        echo '<div class="taxcal-filter">';
        echo '<form method="get">';
        echo '<input type="hidden" name="do" value="admin">';
        echo '<input type="hidden" name="page" value="beztaxcalendar">';
        echo '<input type="hidden" name="tab" value="view">';
        echo '<label>' . $this->getLang('year') . ': ';
        echo '<select name="year" onchange="this.form.submit()">';
        foreach ($years as $y => $info) {
            $selected = ($y == $year) ? 'selected' : '';
            $label = $y . ($info['loaded'] ? ' (' . $info['total_deadlines'] . ')' : ' - ' . $this->getLang('status_not_loaded'));
            echo "<option value=\"{$y}\" {$selected}>{$label}</option>";
        }
        echo '</select></label>';
        echo '</form>';
        echo '</div>';

        // Obtener obligaciones activas
        $obligations = $this->helper->getObligations();

        if (empty($obligations)) {
            echo '<p class="taxcal-empty">No hay obligaciones tributarias configuradas. <a href="' . wl($ID, ['do' => 'admin', 'page' => 'beztaxcalendar', 'tab' => 'obligations']) . '">Agregar obligaciones</a></p>';
            return;
        }

        echo '<div class="taxcal-calendar-view">';
        foreach ($obligations as $idx => $ob) {
            $code = $ob['code'];
            $rows = $this->helper->getCalendarRowsByObligation($year, $code);
            $sectionId = 'tcal_' . preg_replace('/[^a-zA-Z0-9]/', '_', $code);

            // Determinar si toda la obligación está activa
            $allActive = true;
            $allInactive = true;
            foreach ($rows as $r) {
                if (($r['is_active'] ?? 1) == 1) $allInactive = false;
                else $allActive = false;
            }
            // Ancla para mantener posición después de acciones
            echo '<a name="' . hsc($sectionId) . '"></a>';
            echo '<div class="obligation-section" id="' . hsc($sectionId) . '" style="margin-bottom:15px;">';

            // Botón desplegable estilo bez
            $rowCount = count($rows);
            $statusLabel = ($allInactive && !empty($rows)) ? ' (Desactivado)' : '';
            $btnExtraClass = ($allInactive && !empty($rows)) ? ' ob-inactive' : '';
            echo '<button class="toggle-obligation-btn' . $btnExtraClass . '" onclick="toggleObligationSection(\'' . hsc($sectionId) . '\')" id="btn_' . hsc($sectionId) . '" type="button">';
            echo '<span class="ob-toggle-name">' . hsc($ob['name']) . ' <small>(' . hsc($code) . ') - ' . $rowCount . ' periodos' . $statusLabel . '</small></span>';
            echo '<svg class="arrow-icon" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><path d="M7.41,8.58L12,13.17L16.59,8.58L18,10L12,16L6,10L7.41,8.58Z"/></svg>';
            echo '</button>';

            // Contenedor desplegable (oculto por defecto)
            echo '<div class="obligation-collapse-wrapper" id="wrap_' . hsc($sectionId) . '">';
            echo '<div class="obligation-collapse-inner">';

            // Tabla de edición (sin form - los valores se sincronizan al form de guardar via JS)
            echo '<div style="overflow-x:auto;max-width:100%;border:1px solid #e2e8f0;border-radius:6px;">';
            echo '<table class="taxcal-table taxcal-deadlines" style="font-size:12px;min-width:1200px;">';
            echo '<thead><tr>';
            echo '<th style="width:30px;">&#10003;</th>';
            echo '<th style="min-width:70px;">Per.</th>';
            echo '<th style="min-width:80px;">Nombre</th>';
            for ($d = 1; $d <= 9; $d++) {
                echo '<th style="min-width:95px;">Dig. ' . $d . '</th>';
            }
            echo '<th style="min-width:95px;">Dig. 0</th>';
            echo '<th style="min-width:100px;">Notas</th>';
            echo '</tr></thead>';
            echo '<tbody>';

            if (!empty($rows)) {
                foreach ($rows as $row) {
                    $rid = $row['id'];
                    $rowActive = ($row['is_active'] ?? 1) == 1;
                    $rowStyle = $rowActive ? '' : 'background:#f5f5f5;opacity:0.6;text-decoration:line-through;';
                    echo '<tr style="' . $rowStyle . '">';

                    // Checkbox toggle por fila
                    $toggleUrl = wl($ID, ['do' => 'admin', 'page' => 'beztaxcalendar', 'tab' => 'view', 'year' => $year,
                        'action' => 'toggle_row_active', 'row_id' => $rid, 'active' => ($rowActive ? '0' : '1'),
                        'sectok' => getSecurityToken()]) . '#' . $sectionId;
                    echo '<td>';
                    echo '<a href="' . $toggleUrl . '" style="font-size:16px;text-decoration:none;" title="' . ($rowActive ? 'Desactivar' : 'Activar') . '">';
                    echo $rowActive ? '&#9745;' : '&#9744;';
                    echo '</a>';
                    echo '</td>';
                    echo '<td><input type="text" name="cal_row[' . $rid . '][period]" value="' . hsc($row['period']) . '" style="width:65px;padding:3px;font-size:11px;"></td>';
                    echo '<td><input type="text" name="cal_row[' . $rid . '][period_name]" value="' . hsc($row['period_name']) . '" style="width:75px;padding:3px;font-size:11px;"></td>';
                    for ($d = 1; $d <= 9; $d++) {
                        $val = $row["digit_{$d}"] ?? '';
                        echo '<td><input type="date" name="cal_row[' . $rid . '][digit_' . $d . ']" value="' . hsc($val) . '" style="padding:2px;font-size:11px;width:90px;"></td>';
                    }
                    $val0 = $row['digit_0'] ?? '';
                    echo '<td><input type="date" name="cal_row[' . $rid . '][digit_0]" value="' . hsc($val0) . '" style="padding:2px;font-size:11px;width:90px;"></td>';
                    echo '<td><input type="text" name="cal_row[' . $rid . '][notes]" value="' . hsc($row['notes'] ?? '') . '" style="width:95px;padding:3px;font-size:11px;"></td>';
                    echo '</tr>';
                }
            } else {
                echo '<tr><td colspan="14" style="text-align:center;color:#999;">Sin datos. Agregue filas abajo.</td></tr>';
            }

            echo '</tbody></table>';
            echo '</div>'; // cierra div scroll horizontal

            // === BARRA DE ACCIONES EN UNA SOLA LINEA ===
            echo '<div class="taxcal-action-bar">';

            // 1. Desactivar/Activar impuesto
            echo '<div class="taxcal-action-item">';
            if (!empty($rows)) {
                if ($allInactive) {
                    echo '<a href="' . wl($ID, ['do' => 'admin', 'page' => 'beztaxcalendar', 'tab' => 'view', 'year' => $year,
                        'action' => 'toggle_obligation_active', 'obligation_code' => $code, 'active' => '1',
                        'sectok' => getSecurityToken()]) . '#' . $sectionId . '" class="taxcal-btn taxcal-btn-green" title="Activar impuesto">&#10003; Activar</a>';
                } else {
                    $confirmName = str_replace(["'", "\\"], ["", ""], $ob['name']);
                    echo '<a href="' . wl($ID, ['do' => 'admin', 'page' => 'beztaxcalendar', 'tab' => 'view', 'year' => $year,
                        'action' => 'toggle_obligation_active', 'obligation_code' => $code, 'active' => '0',
                        'sectok' => getSecurityToken()]) . '#' . $sectionId . '" class="taxcal-btn taxcal-btn-red" onclick="return confirm(\'Desactivar ' . hsc($confirmName) . '?\')" title="Desactivar impuesto">&#10007; Desactivar</a>';
                }
            }
            echo '</div>';

            // 2. Selector fila + Eliminar
            if (!empty($rows)) {
                echo '<div class="taxcal-action-item">';
                echo '<form method="post" style="display:flex;gap:4px;align-items:center;" class="taxcal-scroll-form" onsubmit="return confirm(\'Eliminar fila?\')">';
                echo '<input type="hidden" name="sectok" value="' . getSecurityToken() . '">';
                echo '<input type="hidden" name="do" value="admin"><input type="hidden" name="page" value="beztaxcalendar">';
                echo '<input type="hidden" name="tab" value="view"><input type="hidden" name="year" value="' . $year . '">';
                echo '<input type="hidden" name="action" value="delete_calendar_row">';
                echo '<input type="hidden" name="scroll_to" value="' . $sectionId . '">';
                echo '<select name="row_id" class="taxcal-select-sm" required>';
                echo '<option value="">-- Fila --</option>';
                foreach ($rows as $row) {
                    echo '<option value="' . $row['id'] . '">' . hsc($row['period']) . '-' . hsc($row['period_name']) . '</option>';
                }
                echo '</select>';
                echo '<button type="submit" class="taxcal-btn taxcal-btn-red">Eliminar</button>';
                echo '</form>';
                echo '</div>';
            }

            // 3. Agregar fila (periodo + nombre + botón)
            echo '<div class="taxcal-action-item">';
            echo '<form method="post" style="display:flex;gap:4px;align-items:center;" class="taxcal-scroll-form">';
            echo '<input type="hidden" name="sectok" value="' . getSecurityToken() . '">';
            echo '<input type="hidden" name="do" value="admin"><input type="hidden" name="page" value="beztaxcalendar">';
            echo '<input type="hidden" name="tab" value="view"><input type="hidden" name="year" value="' . $year . '">';
            echo '<input type="hidden" name="action" value="add_calendar_row">';
            echo '<input type="hidden" name="obligation_code" value="' . hsc($code) . '">';
            echo '<input type="hidden" name="scroll_to" value="' . $sectionId . '">';
            echo '<input type="text" name="period" placeholder="01" required class="taxcal-input-sm" style="width:45px;" title="Periodo">';
            echo '<input type="text" name="period_name" placeholder="Enero" class="taxcal-input-sm" style="width:70px;" title="Nombre">';
            echo '<button type="submit" class="taxcal-btn taxcal-btn-green">+ Fila</button>';
            echo '</form>';
            echo '</div>';

            // 4. Guardar (form independiente con hidden inputs sincronizados via JS)
            if (!empty($rows)) {
                echo '<div class="taxcal-action-item">';
                echo '<form method="post" class="taxcal-scroll-form" id="saveform_' . hsc($sectionId) . '" onsubmit="return syncSaveForm(\'' . hsc($sectionId) . '\')">';
                echo '<input type="hidden" name="sectok" value="' . getSecurityToken() . '">';
                echo '<input type="hidden" name="do" value="admin"><input type="hidden" name="page" value="beztaxcalendar">';
                echo '<input type="hidden" name="tab" value="view"><input type="hidden" name="year" value="' . $year . '">';
                echo '<input type="hidden" name="action" value="save_calendar_dates">';
                echo '<input type="hidden" name="scroll_to" value="' . $sectionId . '">';
                foreach ($rows as $row) {
                    $rid = $row['id'];
                    echo '<input type="hidden" name="cal_row[' . $rid . '][period]" value="' . hsc($row['period']) . '">';
                    echo '<input type="hidden" name="cal_row[' . $rid . '][period_name]" value="' . hsc($row['period_name']) . '">';
                    for ($d = 1; $d <= 9; $d++) {
                        echo '<input type="hidden" name="cal_row[' . $rid . '][digit_' . $d . ']" value="' . hsc($row["digit_{$d}"] ?? '') . '">';
                    }
                    echo '<input type="hidden" name="cal_row[' . $rid . '][digit_0]" value="' . hsc($row['digit_0'] ?? '') . '">';
                    echo '<input type="hidden" name="cal_row[' . $rid . '][notes]" value="' . hsc($row['notes'] ?? '') . '">';
                }
                echo '<button type="submit" class="taxcal-btn taxcal-btn-blue">Guardar</button>';
                echo '</form>';
                echo '</div>';
            }

            echo '</div>'; // taxcal-action-bar

            echo '</div>'; // obligation-collapse-inner
            echo '</div>'; // obligation-collapse-wrapper
            echo '</div>'; // obligation-section
        }
        echo '</div>';

        // JavaScript para toggle desplegable, scroll y sync
        echo '<script>
        // Toggle desplegable estilo bez
        function toggleObligationSection(id) {
            var wrapper = document.getElementById("wrap_" + id);
            var btn = document.getElementById("btn_" + id);
            if (wrapper.classList.contains("expanded")) {
                wrapper.classList.remove("expanded");
                btn.classList.remove("active");
            } else {
                wrapper.classList.add("expanded");
                btn.classList.add("active");
            }
        }

        // Sincronizar TODAS las tablas al form general de Guardar Todo
        function syncAllTables() {
            var container = document.getElementById("taxcal_save_all_data");
            if (!container) { alert("Error: contenedor no encontrado"); return false; }
            container.innerHTML = "";
            var count = 0;
            // Recorrer todas las tablas de obligaciones (incluso cerradas)
            var tables = document.querySelectorAll(".taxcal-calendar-view table.taxcal-deadlines");
            tables.forEach(function(table) {
                var inputs = table.querySelectorAll("input[name]");
                inputs.forEach(function(inp) {
                    if (!inp.name || inp.name.indexOf("cal_row") === -1) return;
                    var h = document.createElement("input");
                    h.type = "hidden";
                    h.name = inp.name;
                    h.value = inp.value;
                    container.appendChild(h);
                    count++;
                });
            });
            if (count === 0) {
                alert("No hay datos para guardar. Agregue filas primero.");
                return false;
            }
            return true;
        }

        // Sincronizar valores editados de la tabla al form de guardar individual
        function syncSaveForm(sectionId) {
            var section = document.getElementById(sectionId);
            var saveForm = document.getElementById("saveform_" + sectionId);
            if (!section || !saveForm) return false;

            var tableInputs = section.querySelectorAll("table input");
            tableInputs.forEach(function(inp) {
                var name = inp.name;
                if (!name || name.indexOf("cal_row") === -1) return;
                var hidden = saveForm.querySelector("input[name=\"" + name + "\"]");
                if (hidden) hidden.value = inp.value;
            });
            return true;
        }

        // Mantener posición de scroll después de POST
        document.addEventListener("DOMContentLoaded", function() {
            // Abrir la sección correspondiente si hay hash en URL
            var hash = window.location.hash.replace("#", "");
            if (hash && document.getElementById(hash)) {
                var wrapper = document.getElementById("wrap_" + hash);
                var btn = document.getElementById("btn_" + hash);
                if (wrapper) {
                    wrapper.classList.add("expanded");
                    if (btn) btn.classList.add("active");
                    setTimeout(function() {
                        document.getElementById(hash).scrollIntoView({behavior: "smooth", block: "start"});
                    }, 100);
                }
            }

            // Para formularios POST, agregar scroll_to como hash al action
            document.querySelectorAll(".taxcal-scroll-form").forEach(function(form) {
                form.addEventListener("submit", function() {
                    var scrollTo = form.querySelector("input[name=scroll_to]");
                    if (scrollTo && scrollTo.value) {
                        // Guardar en sessionStorage para restaurar después del POST
                        sessionStorage.setItem("taxcal_scroll_to", scrollTo.value);
                    }
                });
            });

            // Restaurar scroll después de POST (sessionStorage)
            var savedScroll = sessionStorage.getItem("taxcal_scroll_to");
            if (savedScroll && document.getElementById(savedScroll)) {
                var wrapper = document.getElementById("wrap_" + savedScroll);
                var btn = document.getElementById("btn_" + savedScroll);
                if (wrapper) {
                    wrapper.classList.add("expanded");
                    if (btn) btn.classList.add("active");
                    setTimeout(function() {
                        document.getElementById(savedScroll).scrollIntoView({behavior: "smooth", block: "start"});
                    }, 200);
                }
                sessionStorage.removeItem("taxcal_scroll_to");
            }
        });
        </script>';
    }

    /**
     * Render obligations management
     */
    protected function renderObligations() {
        global $INPUT, $ID;

        $editId = $INPUT->int('edit_obligation', 0);
        $frequencies = ['monthly' => 'Mensual', 'bimonthly' => 'Bimestral', 'quarterly' => 'Trimestral', 'annual' => 'Anual', 'custom' => 'Personalizada'];

        echo '<div class="taxcal-header-row">';
        echo '<h2>' . $this->getLang('obligations') . '</h2>';
        $this->renderPdfButton('obligations');
        echo '</div>';

        $obligations = $this->helper->getAllObligations();

        // Tabla de obligaciones existentes
        echo '<table class="taxcal-table">';
        echo '<thead><tr>';
        echo '<th>Codigo</th>';
        echo '<th>Nombre</th>';
        echo '<th>Descripcion</th>';
        echo '<th>Frecuencia</th>';
        echo '<th>Tipo Contribuyente</th>';
        echo '<th>Activa</th>';
        echo '<th style="width:120px;">Acciones</th>';
        echo '</tr></thead>';
        echo '<tbody>';

        foreach ($obligations as $ob) {
            if ($editId == $ob['id']) {
                // Fila resaltada (modo edición — formulario debajo de la tabla)
                echo '<tr style="background:#fffde7;">';
                echo '<td><code>' . hsc($ob['code']) . '</code></td>';
                echo '<td><em>' . hsc($ob['name']) . '</em></td>';
                echo '<td>' . hsc($ob['description'] ?? '') . '</td>';
                echo '<td>' . ($frequencies[$ob['frequency']] ?? $ob['frequency']) . '</td>';
                echo '<td>' . hsc($ob['taxpayer_type'] ?? '') . '</td>';
                echo '<td>' . ($ob['is_active'] ? '&#10003;' : '&#10007;') . '</td>';
                echo '<td><a href="' . wl($ID, ['do' => 'admin', 'page' => 'beztaxcalendar', 'tab' => 'obligations']) . '" style="color:#666;">Cancelar</a></td>';
                echo '</tr>';
                $editOb = $ob; // Guardar para formulario debajo
            } else {
                // Fila normal
                echo '<tr>';
                echo '<td><code>' . hsc($ob['code']) . '</code></td>';
                echo '<td>' . hsc($ob['name']) . '</td>';
                echo '<td>' . hsc($ob['description'] ?? '') . '</td>';
                echo '<td>' . ($frequencies[$ob['frequency']] ?? $ob['frequency']) . '</td>';
                echo '<td>' . hsc($ob['taxpayer_type'] ?? '') . '</td>';
                echo '<td>' . ($ob['is_active'] ? '&#10003;' : '&#10007;') . '</td>';
                echo '<td>';
                echo '<a href="' . wl($ID, ['do' => 'admin', 'page' => 'beztaxcalendar', 'tab' => 'obligations', 'edit_obligation' => $ob['id']]) . '" style="color:#1565c0;margin-right:8px;" title="Editar">&#9998;</a>';
                echo '<form method="post" style="display:inline;" onsubmit="return confirm(\'Eliminar obligacion ' . hsc($ob['code']) . '? Se eliminaran tambien las fechas del calendario y configuraciones de clientes asociadas.\');">';
                echo '<input type="hidden" name="sectok" value="' . getSecurityToken() . '">';
                echo '<input type="hidden" name="do" value="admin">';
                echo '<input type="hidden" name="page" value="beztaxcalendar">';
                echo '<input type="hidden" name="tab" value="obligations">';
                echo '<input type="hidden" name="action" value="delete_obligation">';
                echo '<input type="hidden" name="obligation_id" value="' . $ob['id'] . '">';
                echo '<button type="submit" style="background:none;border:none;color:#c62828;cursor:pointer;font-size:16px;" title="Eliminar">&#10060;</button>';
                echo '</form>';
                echo '</td>';
                echo '</tr>';
            }
        }
        echo '</tbody></table>';

        // Formulario de edición (fuera de la tabla — HTML válido)
        if (isset($editOb)) {
            echo '<div class="taxcal-section" style="margin-top:15px;background:#fffde7;padding:15px;border-radius:8px;border:1px solid #fbbf24;">';
            echo '<h4>Editar: ' . hsc($editOb['code']) . '</h4>';
            echo '<form method="post">';
            echo '<input type="hidden" name="sectok" value="' . getSecurityToken() . '">';
            echo '<input type="hidden" name="do" value="admin">';
            echo '<input type="hidden" name="page" value="beztaxcalendar">';
            echo '<input type="hidden" name="tab" value="obligations">';
            echo '<input type="hidden" name="action" value="update_obligation">';
            echo '<input type="hidden" name="obligation_id" value="' . $editOb['id'] . '">';
            echo '<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-bottom:10px;">';
            echo '<div><label>Nombre *</label><input type="text" name="name" value="' . hsc($editOb['name']) . '" required style="width:100%;padding:6px;"></div>';
            echo '<div><label>Descripcion</label><input type="text" name="description" value="' . hsc($editOb['description'] ?? '') . '" style="width:100%;padding:6px;"></div>';
            echo '<div><label>Frecuencia</label><select name="frequency" style="width:100%;padding:6px;">';
            foreach ($frequencies as $fk => $fl) {
                $sel = ($editOb['frequency'] == $fk) ? 'selected' : '';
                echo "<option value=\"{$fk}\" {$sel}>{$fl}</option>";
            }
            echo '</select></div>';
            echo '<div><label>Tipo Contribuyente</label><input type="text" name="taxpayer_type" value="' . hsc($editOb['taxpayer_type'] ?? '') . '" style="width:100%;padding:6px;"></div>';
            echo '<div><label>Activa</label><br><input type="checkbox" name="is_active" value="1"' . ($editOb['is_active'] ? ' checked' : '') . '> Si</div>';
            echo '</div>';
            echo '<button type="submit" class="btn-primary">Guardar cambios</button> ';
            echo '<a href="' . wl($ID, ['do' => 'admin', 'page' => 'beztaxcalendar', 'tab' => 'obligations']) . '" style="margin-left:10px;color:#666;">Cancelar</a>';
            echo '</form>';
            echo '</div>';
        }

        // Formulario para agregar nueva obligación
        echo '<div class="taxcal-section" style="margin-top:20px;">';
        echo '<h4>Agregar Nueva Obligacion</h4>';
        echo '<form method="post">';
        echo '<input type="hidden" name="sectok" value="' . getSecurityToken() . '">';
        echo '<input type="hidden" name="do" value="admin">';
        echo '<input type="hidden" name="page" value="beztaxcalendar">';
        echo '<input type="hidden" name="tab" value="obligations">';
        echo '<input type="hidden" name="action" value="add_obligation">';
        echo '<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-bottom:10px;">';
        echo '<div><label>Codigo *</label><input type="text" name="code" required placeholder="Ej: IVA_BIM" style="width:100%;padding:6px;"></div>';
        echo '<div><label>Nombre *</label><input type="text" name="name" required placeholder="Ej: IVA Bimestral" style="width:100%;padding:6px;"></div>';
        echo '<div><label>Descripcion</label><input type="text" name="description" placeholder="Descripcion breve" style="width:100%;padding:6px;"></div>';
        echo '<div><label>Frecuencia</label><select name="frequency" style="width:100%;padding:6px;">';
        foreach ($frequencies as $fk => $fl) { echo "<option value=\"{$fk}\">{$fl}</option>"; }
        echo '</select></div>';
        echo '<div><label>Tipo Contribuyente</label><input type="text" name="taxpayer_type" placeholder="Ej: responsable_iva" style="width:100%;padding:6px;"></div>';
        echo '</div>';
        echo '<button type="submit" class="btn-primary">Agregar Obligacion</button>';
        echo '</form>';
        echo '</div>';
    }

    /**
     * Render client configuration
     */
    protected function renderClientConfig() {
        global $INPUT;

        $clients = $this->helper->getClients();
        $selectedClient = $INPUT->int('client_id', 0);
        $selectedYear = $INPUT->int('year', date('Y'));

        echo '<div class="taxcal-header-row">';
        echo '<h2>' . $this->getLang('client_config') . '</h2>';
        if ($selectedClient) {
            $this->renderPdfButton('client', ['client_id' => $selectedClient, 'year' => $selectedYear]);
        }
        echo '</div>';

        // Client selector
        echo '<div class="taxcal-filter">';
        echo '<form method="get">';
        echo '<input type="hidden" name="do" value="admin">';
        echo '<input type="hidden" name="page" value="beztaxcalendar">';
        echo '<input type="hidden" name="tab" value="client">';

        echo '<label>' . $this->getLang('select_client') . ': ';
        echo '<select name="client_id" onchange="this.form.submit()">';
        echo '<option value="0">-- ' . $this->getLang('select_client') . ' --</option>';
        foreach ($clients as $client) {
            $selected = ($client['id'] == $selectedClient) ? 'selected' : '';
            echo "<option value=\"{$client['id']}\" {$selected}>" . hsc($client['name']) . "</option>";
        }
        echo '</select></label>';

        echo '<label>' . $this->getLang('year') . ': ';
        echo '<select name="year" onchange="this.form.submit()">';
        for ($y = date('Y') - 1; $y <= date('Y') + 1; $y++) {
            $selected = ($y == $selectedYear) ? 'selected' : '';
            echo "<option value=\"{$y}\" {$selected}>{$y}</option>";
        }
        echo '</select></label>';

        echo '</form>';
        echo '</div>';

        if ($selectedClient) {
            $client = $this->helper->getClient($selectedClient);

            echo '<div class="client-info">';
            echo '<h3>' . hsc($client['name']) . '</h3>';
            if (!empty($client['nit'])) {
                $nitLastDigit = $this->helper->extractNitLastDigit($client['nit']);
                echo '<p>NIT: ' . hsc($client['nit']) . ' (Ultimo digito: <strong>' . $nitLastDigit . '</strong>)</p>';
            }
            echo '</div>';

            // Responsable del calendario tributario
            $this->renderClientResponsible($selectedClient);

            // Client obligations
            $this->renderClientObligations($selectedClient);

            // Client exceptions
            $this->renderClientExceptions($selectedClient, $selectedYear);

            // Effective calendar
            $this->renderClientEffectiveCalendar($selectedClient, $selectedYear);
        }
    }

    /**
     * Render client obligations assignment
     */
    protected function renderClientObligations($clientId) {
        $obligations = $this->helper->getClientObligations($clientId);

        echo '<div class="taxcal-section">';
        echo '<h4>' . $this->getLang('client_obligations_title') . '</h4>';

        echo '<form method="post">';
        echo '<input type="hidden" name="sectok" value="' . getSecurityToken() . '">';
        echo '<input type="hidden" name="do" value="admin">';
        echo '<input type="hidden" name="page" value="beztaxcalendar">';
        echo '<input type="hidden" name="tab" value="client">';
        echo '<input type="hidden" name="action" value="save_client_obligations">';
        echo '<input type="hidden" name="client_id" value="' . $clientId . '">';

        echo '<div class="obligations-grid">';
        foreach ($obligations as $ob) {
            $checked = $ob['is_assigned'] ? 'checked' : '';
            echo '<label class="obligation-checkbox">';
            echo "<input type=\"checkbox\" name=\"obligations[]\" value=\"{$ob['code']}\" {$checked}>";
            echo ' ' . hsc($ob['name']);
            echo '</label>';
        }
        echo '</div>';

        echo '<button type="submit" class="btn-primary">' . $this->getLang('btn_save') . '</button>';
        echo '</form>';
        echo '</div>';
    }

    /**
     * Render campo Responsable del calendario tributario por cliente
     */
    protected function renderClientResponsible($clientId) {
        $currentResponsible = $this->helper->getClientResponsible($clientId);
        $users = $this->helper->getSystemUsers();

        // Obtener coordinadores de proyectos del cliente para referencia
        $projects = $this->helper->getClientProjects($clientId);
        $coordinators = [];
        foreach ($projects as $p) {
            if (!empty($p['coordinator']) && !in_array($p['coordinator'], $coordinators)) {
                $coordinators[] = $p['coordinator'];
            }
        }

        echo '<div class="taxcal-section">';
        echo '<h4>Responsable del Calendario Tributario</h4>';
        echo '<p style="color:#666;font-size:13px;">Seleccione quien sera el responsable de las tareas tributarias de este cliente. Por defecto se asigna al coordinador del proyecto.</p>';

        echo '<form method="post">';
        echo '<input type="hidden" name="sectok" value="' . getSecurityToken() . '">';
        echo '<input type="hidden" name="do" value="admin">';
        echo '<input type="hidden" name="page" value="beztaxcalendar">';
        echo '<input type="hidden" name="tab" value="client">';
        echo '<input type="hidden" name="action" value="save_responsible">';
        echo '<input type="hidden" name="client_id" value="' . $clientId . '">';
        echo '<input type="hidden" name="year" value="' . (int)($GLOBALS['INPUT']->int('year', date('Y'))) . '">';

        echo '<div style="display:flex;gap:15px;align-items:center;margin:10px 0;">';
        echo '<label style="font-weight:600;">Responsable:</label>';
        echo '<select name="responsible" style="padding:6px 12px;border:1px solid #d1d5db;border-radius:6px;min-width:250px;">';

        // Opcion por defecto: coordinador del proyecto
        $coordLabel = !empty($coordinators) ? ' (' . implode(', ', $coordinators) . ')' : '';
        echo '<option value="">-- Coordinador del proyecto' . hsc($coordLabel) . ' --</option>';

        foreach ($users as $user) {
            $selected = ($currentResponsible === $user['username']) ? 'selected' : '';
            $label = hsc($user['fullname']) . ' (' . hsc($user['username']) . ')';
            echo '<option value="' . hsc($user['username']) . '" ' . $selected . '>' . $label . '</option>';
        }
        echo '</select>';

        echo '<button type="submit" class="btn-primary" style="padding:6px 16px;">Guardar</button>';
        echo '</div>';

        if ($currentResponsible) {
            echo '<p style="color:#059669;font-size:13px;">&#10003; Responsable actual: <strong>' . hsc($currentResponsible) . '</strong> (override activo)</p>';
        } else {
            echo '<p style="color:#6b7280;font-size:13px;">Sin override — se usara el coordinador del proyecto.</p>';
        }

        echo '</form>';
        echo '</div>';
    }

    /**
     * Render client exceptions
     */
    protected function renderClientExceptions($clientId, $year) {
        global $ID;
        $exceptions = $this->helper->getClientExceptions($clientId, $year);

        echo '<div class="taxcal-section">';
        echo '<h4>' . $this->getLang('client_exceptions_title') . ' ' . $year . '</h4>';

        if (!empty($exceptions)) {
            echo '<table class="taxcal-table">';
            echo '<thead><tr>';
            echo '<th>' . $this->getLang('obligation') . '</th>';
            echo '<th>' . $this->getLang('period') . '</th>';
            echo '<th>' . $this->getLang('exception_type') . '</th>';
            echo '<th>' . $this->getLang('custom_date') . '</th>';
            echo '<th>' . $this->getLang('reason') . '</th>';
            echo '<th>' . $this->getLang('actions') . '</th>';
            echo '</tr></thead>';
            echo '<tbody>';

            foreach ($exceptions as $ex) {
                echo '<tr>';
                echo '<td>' . hsc($ex['obligation_name']) . '</td>';
                echo '<td>' . hsc($ex['period']) . '</td>';
                echo '<td>' . $this->getLang('exception_' . $ex['exception_type']) . '</td>';
                echo '<td>' . ($ex['custom_date'] ? date('d/m/Y', strtotime($ex['custom_date'])) : '-') . '</td>';
                echo '<td>' . hsc($ex['reason'] ?? '') . '</td>';
                echo '<td>';
                $deleteUrl = wl($ID, [
                    'do' => 'admin',
                    'page' => 'beztaxcalendar',
                    'tab' => 'client',
                    'client_id' => $clientId,
                    'year' => $year,
                    'action' => 'delete_exception',
                    'exception_id' => $ex['id'],
                    'sectok' => getSecurityToken()
                ]);
                echo "<a href=\"{$deleteUrl}\" class=\"btn-small btn-danger\" onclick=\"return confirm('" . $this->getLang('confirm_delete') . "')\">" . $this->getLang('btn_delete') . "</a>";
                echo '</td>';
                echo '</tr>';
            }

            echo '</tbody></table>';
        }

        // Add exception form
        $this->renderAddExceptionForm($clientId, $year);

        echo '</div>';
    }

    /**
     * Render add exception form
     */
    protected function renderAddExceptionForm($clientId, $year) {
        $obligations = $this->helper->getClientObligations($clientId);
        $assignedObligations = array_filter($obligations, function($o) { return $o['is_assigned']; });

        echo '<div class="add-exception-form">';
        echo '<h5>' . $this->getLang('add_exception') . '</h5>';

        echo '<form method="post">';
        echo '<input type="hidden" name="sectok" value="' . getSecurityToken() . '">';
        echo '<input type="hidden" name="do" value="admin">';
        echo '<input type="hidden" name="page" value="beztaxcalendar">';
        echo '<input type="hidden" name="tab" value="client">';
        echo '<input type="hidden" name="action" value="add_exception">';
        echo '<input type="hidden" name="client_id" value="' . $clientId . '">';
        echo '<input type="hidden" name="year" value="' . $year . '">';

        echo '<div class="form-row">';
        echo '<label>' . $this->getLang('obligation') . ':';
        echo '<select name="obligation_code" required>';
        echo '<option value="">--</option>';
        foreach ($assignedObligations as $ob) {
            echo "<option value=\"{$ob['code']}\">" . hsc($ob['name']) . "</option>";
        }
        echo '</select></label>';
        echo '</div>';

        echo '<div class="form-row">';
        echo '<label>' . $this->getLang('period') . ':';
        echo '<input type="text" name="period" placeholder="01, 01-02, C1..." required>';
        echo '</label>';
        echo '</div>';

        echo '<div class="form-row">';
        echo '<label>' . $this->getLang('exception_type') . ':';
        echo '<select name="exception_type" required onchange="toggleCustomDate(this)">';
        echo '<option value="change_date">' . $this->getLang('exception_change_date') . '</option>';
        echo '<option value="exclude">' . $this->getLang('exception_exclude') . '</option>';
        echo '</select></label>';
        echo '</div>';

        echo '<div class="form-row" id="custom-date-row">';
        echo '<label>' . $this->getLang('custom_date') . ':';
        echo '<input type="date" name="custom_date">';
        echo '</label>';
        echo '</div>';

        echo '<div class="form-row">';
        echo '<label>' . $this->getLang('reason') . ':';
        echo '<input type="text" name="reason" placeholder="Ej: Prorroga DIAN Res. 123">';
        echo '</label>';
        echo '</div>';

        echo '<button type="submit" class="btn-primary">' . $this->getLang('add_exception') . '</button>';
        echo '</form>';
        echo '</div>';

        echo '<script>
        function toggleCustomDate(select) {
            var row = document.getElementById("custom-date-row");
            row.style.display = select.value === "change_date" ? "block" : "none";
        }
        </script>';
    }

    /**
     * Render client effective calendar
     */
    protected function renderClientEffectiveCalendar($clientId, $year) {
        $calendar = $this->helper->getClientEffectiveCalendar($clientId, $year);

        echo '<div class="taxcal-section">';
        echo '<h4>' . $this->getLang('effective_calendar') . ' ' . $year . '</h4>';

        if (empty($calendar)) {
            echo '<p class="taxcal-empty">' . $this->getLang('no_data') . '</p>';
            echo '</div>';
            return;
        }

        echo '<table class="taxcal-table">';
        echo '<thead><tr>';
        echo '<th>' . $this->getLang('deadline_date') . '</th>';
        echo '<th>' . $this->getLang('obligation') . '</th>';
        echo '<th>' . $this->getLang('period') . '</th>';
        echo '<th>Fuente</th>';
        echo '</tr></thead>';
        echo '<tbody>';

        foreach ($calendar as $item) {
            $sourceClass = $item['source'] === 'exception' ? 'source-exception' : 'source-master';
            echo "<tr class=\"{$sourceClass}\">";
            echo '<td>' . ($item['effective_date'] ? date('d/m/Y', strtotime($item['effective_date'])) : '-') . '</td>';
            echo '<td>' . hsc($item['obligation_name']) . '</td>';
            echo '<td>' . hsc($item['period_name']) . '</td>';
            echo '<td>' . ($item['source'] === 'exception' ? 'Excepcion' : 'Maestro') . '</td>';
            echo '</tr>';
        }

        echo '</tbody></table>';
        echo '</div>';
    }

    /**
     * Render task generator
     */
    protected function renderTaskGenerator() {
        global $INPUT, $ID;

        echo '<h2>' . $this->getLang('generate_title') . '</h2>';

        $clients = $this->helper->getClients();
        $selectedClient = $INPUT->int('client_id', 0);
        $selectedYear = $INPUT->int('year', date('Y'));

        // Validation
        $validation = $this->helper->validateCalendarForGeneration($selectedYear);

        if (!$validation['can_proceed']) {
            echo '<div class="taxcal-alerts">';
            foreach ($validation['alerts'] as $alert) {
                echo '<div class="taxcal-alert alert-error">';
                echo '<span>' . hsc($alert['message']) . '</span>';
                if (!empty($alert['action'])) {
                    $url = wl($ID, ['do' => 'admin', 'page' => 'beztaxcalendar', 'tab' => $alert['action'], 'year' => $selectedYear]);
                    echo " <a href=\"{$url}\" class=\"btn-small\">" . hsc($alert['action_label']) . "</a>";
                }
                echo '</div>';
            }
            echo '</div>';
        }

        if (!empty($validation['warnings'])) {
            echo '<div class="taxcal-alerts">';
            foreach ($validation['warnings'] as $warning) {
                echo '<div class="taxcal-alert alert-warning">';
                echo '<span>' . hsc($warning['message']) . '</span>';
                echo '</div>';
            }
            echo '</div>';
        }

        // Client & year selector
        echo '<div class="taxcal-filter">';
        echo '<form method="get">';
        echo '<input type="hidden" name="do" value="admin">';
        echo '<input type="hidden" name="page" value="beztaxcalendar">';
        echo '<input type="hidden" name="tab" value="generate">';

        echo '<label>' . $this->getLang('select_client') . ': ';
        echo '<select name="client_id" onchange="this.form.submit()">';
        echo '<option value="0">-- ' . $this->getLang('select_client') . ' --</option>';
        foreach ($clients as $client) {
            $selected = ($client['id'] == $selectedClient) ? 'selected' : '';
            echo "<option value=\"{$client['id']}\" {$selected}>" . hsc($client['name']) . "</option>";
        }
        echo '</select></label>';

        echo '<label>' . $this->getLang('year') . ': ';
        echo '<select name="year" onchange="this.form.submit()">';
        for ($y = date('Y') - 1; $y <= date('Y') + 1; $y++) {
            $selected = ($y == $selectedYear) ? 'selected' : '';
            echo "<option value=\"{$y}\" {$selected}>{$y}</option>";
        }
        echo '</select></label>';

        echo '</form>';
        echo '</div>';

        if ($selectedClient && $validation['can_proceed']) {
            $client = $this->helper->getClient($selectedClient);
            $projects = $this->helper->getClientProjects($selectedClient);
            $obligations = $this->helper->getClientObligations($selectedClient);
            $assignedObligations = array_filter($obligations, function($o) { return $o['is_assigned']; });

            if (empty($projects)) {
                echo '<p class="taxcal-empty">El cliente no tiene proyectos abiertos.</p>';
                return;
            }

            if (empty($assignedObligations)) {
                echo '<p class="taxcal-empty">El cliente no tiene obligaciones asignadas. <a href="' . wl($ID, ['do' => 'admin', 'page' => 'beztaxcalendar', 'tab' => 'client', 'client_id' => $selectedClient]) . '">Configurar</a></p>';
                return;
            }

            echo '<form method="post">';
            echo '<input type="hidden" name="sectok" value="' . getSecurityToken() . '">';
            echo '<input type="hidden" name="do" value="admin">';
            echo '<input type="hidden" name="page" value="beztaxcalendar">';
            echo '<input type="hidden" name="tab" value="generate">';
            echo '<input type="hidden" name="action" value="generate_tasks">';
            echo '<input type="hidden" name="client_id" value="' . $selectedClient . '">';
            echo '<input type="hidden" name="year" value="' . $selectedYear . '">';

            // Project selector
            echo '<div class="form-row">';
            echo '<label>' . $this->getLang('select_project') . ':';
            echo '<select name="project_id" required>';
            echo '<option value="">--</option>';
            foreach ($projects as $proj) {
                echo "<option value=\"{$proj['id']}\">#" . $proj['id'] . ' - ' . hsc(substr($proj['title'] ?? 'Proyecto', 0, 50)) . "</option>";
            }
            echo '</select></label>';
            echo '</div>';

            // Obligations selector
            echo '<div class="form-row">';
            echo '<label>' . $this->getLang('select_obligations') . ':</label>';
            echo '<div class="obligations-grid">';
            foreach ($assignedObligations as $ob) {
                echo '<label class="obligation-checkbox">';
                echo "<input type=\"checkbox\" name=\"obligations[]\" value=\"{$ob['code']}\" checked>";
                echo ' ' . hsc($ob['name']);
                echo '</label>';
            }
            echo '</div>';
            echo '</div>';

            // Periods (simplified - select all available)
            echo '<input type="hidden" name="all_periods" value="1">';

            echo '<div class="form-actions">';
            echo '<button type="submit" class="btn-primary">' . $this->getLang('btn_generate') . '</button>';
            echo '</div>';

            echo '</form>';

            // Show generation result
            if (isset($_SESSION['taxcal_generation_result'])) {
                $this->renderGenerationResult($_SESSION['taxcal_generation_result']);
                unset($_SESSION['taxcal_generation_result']);
            }
        }
    }

    /**
     * Render configuration
     */
    protected function renderConfig() {
        $config = $this->helper->getAllConfig();

        // ============================================================
        // PARTE 1: INSTRUCTIVO COMPLETO
        // ============================================================
        echo '<h2>Instructivo del Calendario Tributario</h2>';
        echo '<p style="color:#666;">Guia completa para el uso del modulo de Calendario Tributario DIAN.</p>';

        // 1. Carga del Calendario
        echo '<div class="taxcal-section" style="margin-bottom:20px;">';
        echo '<h3 style="color:#1565c0;">1. Carga del Calendario Tributario</h3>';
        echo '<ol style="line-height:2;">';
        echo '<li>Ingrese a la pestana <strong>Cargar Calendario</strong>.</li>';
        echo '<li>Descargue la plantilla Excel haciendo clic en <strong>"Descargar calendario Excel"</strong>.</li>';
        echo '<li>Complete la plantilla con las fechas de vencimiento para cada obligacion y periodo:</li>';
        echo '<ul>';
        echo '<li><strong>Hoja "Vencimientos":</strong> columnas obligation_code, period, period_name, digit_1 a digit_0, notes.</li>';
        echo '<li><strong>Hoja "Obligaciones":</strong> columnas code, name, description, frequency, taxpayer_type.</li>';
        echo '<li>Las fechas deben estar en formato <code>AAAA-MM-DD</code> (ejemplo: 2026-03-15).</li>';
        echo '<li>Los codigos de obligacion deben ser unicos y en mayusculas (ejemplo: RETEFUENTE, IVA_BIM).</li>';
        echo '</ul>';
        echo '<li>Seleccione el <strong>ano fiscal</strong> y suba el archivo.</li>';
        echo '<li>Use la opcion <strong>"Solo vista previa"</strong> para verificar antes de guardar.</li>';
        echo '<li>Si aparecen errores, corrija el Excel y vuelva a cargar.</li>';
        echo '<li>Para reemplazar un calendario existente, use el boton <strong>"Reiniciar Calendario"</strong> que borra todos los datos del ano y permite cargar uno nuevo.</li>';
        echo '</ol>';
        echo '<div style="background:#fff3cd;padding:10px;border-radius:6px;border-left:4px solid #ffc107;margin-top:10px;">';
        echo '<strong>&#9888; Importante:</strong> El boton "Reiniciar Calendario" elimina permanentemente todas las obligaciones, fechas, configuraciones de clientes y excepciones. Las tareas ya generadas en proyectos BEZ no se eliminan.';
        echo '</div>';
        echo '</div>';

        // 2. Correcciones del Calendario
        echo '<div class="taxcal-section" style="margin-bottom:20px;">';
        echo '<h3 style="color:#1565c0;">2. Correcciones del Calendario</h3>';
        echo '<p>Despues de cargar el calendario, puede realizar correcciones desde las pestanas <strong>Obligaciones Tributarias</strong> y <strong>Ver Calendario</strong>:</p>';
        echo '<table class="taxcal-table" style="font-size:13px;">';
        echo '<thead><tr><th style="text-align:left;">Accion</th><th style="text-align:left;">Como hacerlo</th><th style="text-align:left;">Donde</th></tr></thead>';
        echo '<tbody>';
        echo '<tr><td>Editar fechas</td><td>Modifique directamente los campos de fecha en la tabla y haga clic en "Guardar cambios"</td><td>Ver Calendario</td></tr>';
        echo '<tr><td>Agregar fila (periodo)</td><td>Use el formulario "Agregar fila" debajo de cada impuesto. Ingrese periodo y nombre.</td><td>Ver Calendario</td></tr>';
        echo '<tr><td>Eliminar fila</td><td>Seleccione la fila del listado desplegable y haga clic en "Eliminar seleccionado"</td><td>Ver Calendario</td></tr>';
        echo '<tr><td>Desactivar fila (ignorar)</td><td>Haga clic en el check &#9745; de la fila. La fila queda en gris y no genera tareas, pero se mantiene visible.</td><td>Ver Calendario</td></tr>';
        echo '<tr><td>Desactivar impuesto completo</td><td>Haga clic en el boton rojo <strong>"Desactivar Impuesto"</strong> junto al titulo de la obligacion.</td><td>Ver Calendario</td></tr>';
        echo '<tr><td>Agregar obligacion</td><td>Use el formulario "Agregar Nueva Obligacion" al final de la tabla.</td><td>Obligaciones Tributarias</td></tr>';
        echo '<tr><td>Editar obligacion</td><td>Haga clic en el icono &#9998; (lapiz). Se abre un formulario de edicion debajo de la tabla.</td><td>Obligaciones Tributarias</td></tr>';
        echo '<tr><td>Eliminar obligacion</td><td>Haga clic en &#10060;. Elimina la obligacion y todos sus datos asociados (fechas, configuraciones, excepciones).</td><td>Obligaciones Tributarias</td></tr>';
        echo '</tbody></table>';
        echo '<div style="background:#e8f5e9;padding:10px;border-radius:6px;border-left:4px solid #4caf50;margin-top:10px;">';
        echo '<strong>Diferencia entre Ignorar y Eliminar:</strong><br>';
        echo '&#9745; <strong>Ignorar (desactivar):</strong> La fila o impuesto queda visible en gris pero NO genera tareas. Se mantiene el registro para trazabilidad. Se puede reactivar en cualquier momento.<br>';
        echo '&#10060; <strong>Eliminar:</strong> Borra permanentemente la fila o impuesto y todos sus datos asociados. No se puede recuperar.';
        echo '</div>';
        echo '</div>';

        // 3. Proposito de cada boton/pestana
        echo '<div class="taxcal-section" style="margin-bottom:20px;">';
        echo '<h3 style="color:#1565c0;">3. Descripcion de cada Pestana</h3>';
        echo '<table class="taxcal-table" style="font-size:13px;">';
        echo '<thead><tr><th style="text-align:left;">Pestana</th><th style="text-align:left;">Funcion</th></tr></thead>';
        echo '<tbody>';
        echo '<tr><td><strong>Estado del Calendario</strong></td><td>Resumen general: ano cargado, total obligaciones, total fechas, estado de verificacion. Punto de partida para evaluar el estado del sistema.</td></tr>';
        echo '<tr><td><strong>Cargar Calendario</strong></td><td>Subir el archivo Excel con las fechas de vencimiento. Permite descargar la plantilla, cargar archivo nuevo y reiniciar el calendario completo.</td></tr>';
        echo '<tr><td><strong>Obligaciones Tributarias</strong></td><td>Editor de las obligaciones (impuestos). Permite agregar, editar y eliminar obligaciones con su codigo, nombre, frecuencia y tipo de contribuyente.</td></tr>';
        echo '<tr><td><strong>Ver Calendario</strong></td><td>Editor de fechas del calendario maestro. Muestra cada impuesto con su tabla de periodos y digitos. Permite editar fechas, agregar/eliminar filas y activar/desactivar periodos o impuestos completos.</td></tr>';
        echo '<tr><td><strong>Configuracion por Cliente</strong></td><td>Asignar obligaciones a cada cliente, definir el responsable tributario, configurar excepciones de fechas y ver el calendario efectivo del cliente.</td></tr>';
        echo '<tr><td><strong>Generar Tareas</strong></td><td>Crear tareas BEZ automaticamente a partir del calendario configurado. Las tareas se asignan al responsable tributario o al coordinador del proyecto.</td></tr>';
        echo '<tr><td><strong>Instructivo</strong></td><td>Esta guia de uso y la configuracion avanzada del sistema.</td></tr>';
        echo '</tbody></table>';
        echo '</div>';

        // 4. Flujo completo
        echo '<div class="taxcal-section" style="margin-bottom:20px;">';
        echo '<h3 style="color:#1565c0;">4. Flujo Completo: Desde la Carga hasta el Seguimiento</h3>';
        echo '<div style="background:#f0f4f8;padding:15px;border-radius:8px;">';
        echo '<ol style="line-height:2.2;font-size:13px;">';
        echo '<li><strong>Cargar Calendario:</strong> Suba el Excel con las fechas de vencimiento del ano fiscal.</li>';
        echo '<li><strong>Verificar Obligaciones:</strong> Revise en "Obligaciones Tributarias" que todos los impuestos esten correctos.</li>';
        echo '<li><strong>Ajustar Calendario:</strong> En "Ver Calendario", corrija fechas, desactive periodos que no aplican o agregue filas faltantes.</li>';
        echo '<li><strong>Configurar Clientes:</strong> En "Configuracion por Cliente", asigne las obligaciones que aplican a cada cliente y defina el responsable tributario.</li>';
        echo '<li><strong>Generar Tareas:</strong> En "Generar Tareas", seleccione cliente, proyecto, obligaciones y periodos. Las tareas se crean automaticamente en el proyecto BEZ.</li>';
        echo '<li><strong>Seguimiento:</strong> Las tareas aparecen en el proyecto BEZ del cliente con la fecha de vencimiento como fecha limite. El responsable puede marcar cada tarea como completada desde el modulo de proyectos.</li>';
        echo '<li><strong>Auditoria:</strong> El sistema registra quien cargo el calendario, quien genero las tareas, y el estado de cada tarea en el modulo BEZ.</li>';
        echo '</ol>';
        echo '</div>';
        echo '<div style="background:#e3f2fd;padding:10px;border-radius:6px;border-left:4px solid #1565c0;margin-top:10px;">';
        echo '<strong>Sincronizacion:</strong> Los cambios realizados en "Obligaciones Tributarias" se reflejan automaticamente en "Ver Calendario". Si agrega una nueva obligacion, aparecera como una seccion vacia en el calendario donde puede agregar filas con las fechas correspondientes.';
        echo '</div>';
        echo '</div>';

        // 5. Exportar datos
        echo '<div class="taxcal-section" style="margin-bottom:20px;">';
        echo '<h3 style="color:#1565c0;">5. Exportar e Importar</h3>';
        echo '<ul style="line-height:2;">';
        echo '<li><strong>Exportar Excel:</strong> En "Ver Calendario", use el boton "Exportar Excel" para descargar el calendario actual en formato editable.</li>';
        echo '<li><strong>Re-importar:</strong> El archivo exportado tiene la misma estructura que la plantilla de carga. Puede editarlo en Excel y volver a cargarlo en "Cargar Calendario".</li>';
        echo '<li><strong>Exportar PDF:</strong> Cada pestana tiene un boton PDF (rojo) que genera una version imprimible de la informacion actual.</li>';
        echo '</ul>';
        echo '</div>';

        // ============================================================
        // PARTE 2: CONFIGURACIÓN (contenido original)
        // ============================================================
        echo '<hr style="margin:30px 0;border-color:#e2e8f0;">';
        echo '<h2>Configuracion Avanzada</h2>';
        echo '<p style="color:#666;">Parametros tecnicos del sistema de calendario tributario.</p>';

        echo '<form method="post" class="taxcal-form">';
        echo '<input type="hidden" name="sectok" value="' . getSecurityToken() . '">';
        echo '<input type="hidden" name="do" value="admin">';
        echo '<input type="hidden" name="page" value="beztaxcalendar">';
        echo '<input type="hidden" name="tab" value="config">';
        echo '<input type="hidden" name="action" value="save_config">';

        // Alert settings
        echo '<div class="config-section">';
        echo '<h3>' . $this->getLang('config_alerts') . '</h3>';

        echo '<div class="form-row">';
        echo '<label>' . $this->getLang('config_alert_days') . ':';
        echo '<input type="number" name="alert_days_before_year_end" value="' . hsc($config['alert_days_before_year_end']['value'] ?? '60') . '" min="0" max="365">';
        echo '</label></div>';

        echo '</div>';

        echo '<div class="form-actions">';
        echo '<button type="submit" class="btn-primary">' . $this->getLang('btn_save_config') . '</button>';
        echo '</div>';

        echo '</form>';
    }

    // ================================================================
    // HANDLERS
    // ================================================================

    // ================================================================
    // HANDLERS: OBLIGACIONES TRIBUTARIAS CRUD
    // ================================================================

    protected function handleAddObligation() {
        global $INPUT;
        $code = strtoupper(trim($INPUT->str('code')));
        $name = trim($INPUT->str('name'));
        $description = trim($INPUT->str('description'));
        $frequency = $INPUT->str('frequency');
        $taxpayerType = trim($INPUT->str('taxpayer_type'));
        $allowedFrequencies = ['monthly', 'bimonthly', 'quarterly', 'annual', 'custom'];

        if (empty($code) || empty($name)) {
            msg('Codigo y nombre son obligatorios.', -1);
            return;
        }
        if (strlen($code) > 30) {
            msg('El codigo no puede tener mas de 30 caracteres.', -1);
            return;
        }
        if (!preg_match('/^[A-Z0-9_]+$/', $code)) {
            msg('El codigo solo puede contener letras mayusculas, numeros y guion bajo.', -1);
            return;
        }
        if (!in_array($frequency, $allowedFrequencies)) {
            msg('Frecuencia no valida.', -1);
            return;
        }

        $result = $this->helper->addObligation($code, $name, $description, $frequency, $taxpayerType);
        if (is_array($result) && !$result['ok']) {
            msg($result['error'], -1);
        } else {
            msg('Obligacion ' . hsc($code) . ' agregada correctamente.', 1);
        }
    }

    protected function handleUpdateObligation() {
        global $INPUT;
        $id = $INPUT->int('obligation_id');
        $name = trim($INPUT->str('name'));
        $description = trim($INPUT->str('description'));
        $frequency = $INPUT->str('frequency');
        $taxpayerType = trim($INPUT->str('taxpayer_type'));
        $isActive = $INPUT->bool('is_active');
        $allowedFrequencies = ['monthly', 'bimonthly', 'quarterly', 'annual', 'custom'];

        if (!$id || empty($name)) {
            msg('ID y nombre son obligatorios.', -1);
            return;
        }
        $existing = $this->helper->getObligationById($id);
        if (!$existing) {
            msg('Obligacion no encontrada.', -1);
            return;
        }
        if (!in_array($frequency, $allowedFrequencies)) {
            msg('Frecuencia no valida.', -1);
            return;
        }

        $this->helper->updateObligation($id, $name, $description, $frequency, $taxpayerType, $isActive);
        msg('Obligacion actualizada correctamente.', 1);
    }

    protected function handleDeleteObligation() {
        global $INPUT;
        $id = $INPUT->int('obligation_id');
        if (!$id) {
            msg('ID de obligacion no valido.', -1);
            return;
        }

        $ob = $this->helper->getObligationById($id);
        if (!$ob) {
            msg('Obligacion no encontrada.', -1);
            return;
        }

        $this->helper->deleteObligation($id);
        msg('Obligacion ' . hsc($ob['code']) . ' eliminada con todos sus datos asociados.', 1);
    }

    // ================================================================
    // HANDLERS: CALENDARIO MAESTRO CRUD
    // ================================================================

    protected function handleSaveCalendarDates() {
        global $INPUT;
        $rows = $INPUT->arr('cal_row');
        $updated = 0;

        if (!is_array($rows)) {
            msg('No se recibieron datos para guardar.', -1);
            return;
        }

        foreach ($rows as $id => $data) {
            $id = (int)$id;
            if (!$id) continue;

            $period = trim($data['period'] ?? '');
            $periodName = trim($data['period_name'] ?? '');
            $notes = trim($data['notes'] ?? '');
            $dates = [];
            $dateError = false;
            for ($d = 0; $d <= 9; $d++) {
                $val = trim($data['digit_' . ($d == 0 ? '0' : $d)] ?? '');
                if (!empty($val) && !preg_match('/^\d{4}-\d{2}-\d{2}$/', $val)) {
                    msg('Fecha invalida en digito ' . ($d == 0 ? '0' : $d) . ' de fila ' . $id . ': ' . hsc($val), -1);
                    $dateError = true;
                    break;
                }
                $dates[$d] = $val;
            }
            if ($dateError) continue;

            // Pasar fechas en orden: digit_1..digit_9, digit_0
            // updateCalendarRow espera indices 0-9 mapeados a digit_1..digit_9, digit_0
            $datesOrdered = [
                $dates[1] ?? '', $dates[2] ?? '', $dates[3] ?? '', $dates[4] ?? '', $dates[5] ?? '',
                $dates[6] ?? '', $dates[7] ?? '', $dates[8] ?? '', $dates[9] ?? '', $dates[0] ?? ''
            ];

            $this->helper->updateCalendarRow($id, $period, $periodName, $datesOrdered, $notes);
            $updated++;
        }

        msg($updated . ' filas del calendario actualizadas.', 1);
    }

    protected function handleAddCalendarRow() {
        global $INPUT;
        $year = $INPUT->int('year');
        $obligationCode = $INPUT->str('obligation_code');
        $period = trim($INPUT->str('period'));
        $periodName = trim($INPUT->str('period_name'));

        if (!$year || empty($obligationCode) || empty($period)) {
            msg('Ano, codigo de obligacion y periodo son obligatorios.', -1);
            return;
        }
        if ($year < 2020 || $year > 2040) {
            msg('Ano no valido.', -1);
            return;
        }
        $ob = $this->helper->getObligation($obligationCode);
        if (!$ob) {
            msg('Codigo de obligacion no valido: ' . hsc($obligationCode), -1);
            return;
        }
        // Duplicado check
        $db = $this->helper->getDB();
        if ($db) {
            $dupRes = $db->query("SELECT id FROM tax_calendar_master WHERE year = ? AND obligation_code = ? AND period = ?", $year, $obligationCode, $period);
            if ($db->res2row($dupRes)) {
                msg('Ya existe una fila para ' . hsc($obligationCode) . ' periodo ' . hsc($period) . ' en el ano ' . $year . '.', -1);
                return;
            }
        }

        $dates = array_fill(0, 10, '');
        $this->helper->addCalendarRow($year, $obligationCode, $period, $periodName, $dates);
        msg('Fila agregada al calendario de ' . hsc($obligationCode) . '.', 1);
    }

    protected function handleDeleteCalendarRow() {
        global $INPUT;
        $id = $INPUT->int('row_id');
        if (!$id) {
            msg('ID de fila no valido.', -1);
            return;
        }

        $this->helper->deleteCalendarRow($id);
        msg('Fila eliminada del calendario.', 1);
    }

    /**
     * Exportar calendario actual como Excel editable
     * Genera un .xlsx con la misma estructura que el template de importación
     * para que el usuario pueda editar offline y re-importar.
     */
    protected function handleExportCalendar() {
        global $INPUT;

        $year = $INPUT->int('year', date('Y'));

        require_once (function_exists('bez_plugin_base') ? bez_plugin_base() : DOKU_PLUGIN) . 'beztaxcalendar/lib/XLSXWriter.php';

        $writer = new \dokuwiki\plugin\beztaxcalendar\lib\XLSXWriter();

        // === Hoja 1: Vencimientos ===
        // Estructura EXACTA que processUploadedFile() espera al re-importar
        $writer->addSheet('Vencimientos');
        $writer->addRow([
            'obligation_code', 'period', 'period_name',
            'digit_1', 'digit_2', 'digit_3', 'digit_4', 'digit_5',
            'digit_6', 'digit_7', 'digit_8', 'digit_9', 'digit_0',
            'notes'
        ]);

        // Datos actuales del calendario maestro
        $calendar = $this->helper->getMasterCalendar($year);
        if (!empty($calendar)) {
            foreach ($calendar as $row) {
                $excelRow = [];
                $excelRow[] = $row['obligation_code'];
                $excelRow[] = $row['period'];
                $excelRow[] = $row['period_name'];
                // Fechas como formato date para Excel
                for ($d = 1; $d <= 9; $d++) {
                    $val = $row["digit_{$d}"] ?? '';
                    $excelRow[] = !empty($val) ? ['date' => $val] : '';
                }
                $val0 = $row['digit_0'] ?? '';
                $excelRow[] = !empty($val0) ? ['date' => $val0] : '';
                $excelRow[] = $row['notes'] ?? '';
                $writer->addRow($excelRow);
            }
        }

        // === Hoja 2: Obligaciones ===
        // Estructura EXACTA que processUploadedFile() espera
        $writer->addSheet('Obligaciones');
        $writer->addRow(['code', 'name', 'description', 'frequency', 'taxpayer_type']);

        $obligations = $this->helper->getAllObligations();
        foreach ($obligations as $ob) {
            $writer->addRow([
                $ob['code'],
                $ob['name'],
                $ob['description'] ?? '',
                $ob['frequency'] ?? '',
                $ob['taxpayer_type'] ?? ''
            ]);
        }

        // Generar y enviar
        $content = $writer->output();
        $filename = 'calendario_tributario_' . $year . '_exportado.xlsx';

        header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
        header('Content-Disposition: attachment; filename="' . $filename . '"');
        header('Content-Length: ' . strlen($content));
        header('Cache-Control: no-cache, must-revalidate');
        header('Pragma: no-cache');

        echo $content;
        exit;
    }

    // ================================================================
    // HANDLERS: VALIDACIÓN SELECTIVA (TOGGLE ACTIVE)
    // ================================================================

    protected function handleToggleRowActive() {
        global $INPUT;
        $rowId = $INPUT->int('row_id');
        $active = $INPUT->bool('active');

        if (!$rowId) {
            msg('ID de fila no valido.', -1);
            return;
        }

        $this->helper->toggleCalendarRowActive($rowId, $active);
        msg('Fila ' . ($active ? 'activada' : 'desactivada') . ' correctamente.', 1);
    }

    protected function handleToggleObligationActive() {
        global $INPUT;
        $year = $INPUT->int('year');
        $obligationCode = $INPUT->str('obligation_code');
        $active = $INPUT->bool('active');

        if (!$year || empty($obligationCode)) {
            msg('Ano y codigo de obligacion son obligatorios.', -1);
            return;
        }

        $this->helper->toggleObligationActive($year, $obligationCode, $active);
        $ob = $this->helper->getObligation($obligationCode);
        $name = $ob ? $ob['name'] : $obligationCode;
        msg(hsc($name) . ' ' . ($active ? 'activada' : 'desactivada') . ' para el ano ' . $year . '.', 1);
    }

    // ================================================================
    // HANDLERS: RESPONSABLE Y RESET
    // ================================================================

    protected function handleSaveResponsible() {
        global $INPUT, $auth;

        $clientId = $INPUT->int('client_id');
        $responsible = trim($INPUT->str('responsible'));

        if (!$clientId) {
            msg('Cliente no valido', -1);
            return;
        }

        // Validar que el usuario existe (si no es vacío)
        if (!empty($responsible) && $auth) {
            $userData = $auth->getUserData($responsible);
            if (!$userData) {
                msg('El usuario "' . hsc($responsible) . '" no existe en el sistema.', -1);
                return;
            }
        }

        $userId = $INPUT->server->str('REMOTE_USER');
        $result = $this->helper->setClientResponsible($clientId, $responsible, $userId);

        if ($result) {
            if (empty($responsible)) {
                msg('Responsable reiniciado al coordinador del proyecto.', 1);
            } else {
                msg('Responsable actualizado a: ' . hsc($responsible), 1);
            }
        } else {
            msg('Error al guardar responsable', -1);
        }
    }

    /**
     * Reiniciar calendario: borra todos los datos del año seleccionado
     * para permitir cargar un calendario nuevo desde cero.
     * NO elimina tareas ya generadas en proyectos BEZ.
     */
    protected function handleResetCalendar() {
        global $INPUT;

        $year = $INPUT->int('year');
        if ($year < 2020 || $year > 2040) {
            msg('Ano no valido', -1);
            return;
        }

        $db = $this->helper->getDB();
        if (!$db) {
            msg('Error de conexion a base de datos', -1);
            return;
        }

        $user = $INPUT->server->str('REMOTE_USER');

        try {
            $db->query("BEGIN TRANSACTION");

            // 1. Eliminar datos del calendario maestro para este año
            $db->query("DELETE FROM tax_calendar_master WHERE year = ?", $year);

            // 2. Eliminar estado del calendario (verificacion, etc.)
            $db->query("DELETE FROM tax_calendar_status WHERE year = ?", $year);

            // 3. Eliminar archivo almacenado
            $db->query("DELETE FROM tax_calendar_files WHERE year = ?", $year);

            // 4. Eliminar log de tareas generadas (permite regenerar)
            $db->query("DELETE FROM tax_task_log WHERE year = ?", $year);

            // 5. Eliminar excepciones de clientes para este año
            $db->query("DELETE FROM client_deadline_exceptions WHERE year = ?", $year);

            // 6. Eliminar obligaciones tributarias que NO tienen datos en otros años
            $db->query("DELETE FROM tax_obligations WHERE code NOT IN (
                SELECT DISTINCT obligation_code FROM tax_calendar_master WHERE year != ?
            )", $year);

            // 7. Eliminar asignaciones de obligaciones cuya obligación ya no existe
            $db->query("DELETE FROM client_obligations WHERE obligation_code NOT IN (
                SELECT code FROM tax_obligations
            )");

            $db->query("COMMIT");

            msg('Calendario ' . $year . ' reiniciado correctamente. Las obligaciones compartidas con otros años se mantuvieron. Puede cargar un nuevo calendario.', 1);
            dbglog('beztaxcalendar: Calendario ' . $year . ' reiniciado por ' . $user);

        } catch (\Exception $e) {
            $db->query("ROLLBACK");
            msg('Error al reiniciar calendario: ' . hsc($e->getMessage()), -1);
        }
    }

    /**
     * Handle calendar upload
     */
    protected function handleUploadCalendar() {
        global $INPUT;

        $year = $INPUT->int('year');
        $previewOnly = $INPUT->bool('preview_only');
        $userId = $INPUT->server->str('REMOTE_USER');

        if ($year < 2020 || $year > 2040) {
            msg('Ano no valido.', -1);
            return;
        }
        if (!isset($_FILES['excel_file']) || $_FILES['excel_file']['error'] !== UPLOAD_ERR_OK) {
            msg('Error al subir el archivo (codigo: ' . ($_FILES['excel_file']['error'] ?? 'desconocido') . ').', -1);
            return;
        }
        if (empty($_FILES['excel_file']['tmp_name'])) {
            msg($this->getLang('validation_file_required'), -1);
            return;
        }
        // Limite de 5MB
        if ($_FILES['excel_file']['size'] > 5 * 1024 * 1024) {
            msg('El archivo excede el tamano maximo permitido (5MB).', -1);
            return;
        }

        $result = $this->processUploadedFile($_FILES['excel_file'], $year, $previewOnly, $userId);

        $_SESSION['taxcal_upload_result'] = $result;
    }

    /**
     * Process uploaded file - Supports CSV and Excel XLSX
     */
    protected function processUploadedFile($file, $year, $previewOnly, $userId) {
        $db = $this->helper->getDB();

        if (!$db) {
            return [
                'success' => false,
                'preview' => $previewOnly,
                'stats' => ['deadlines_new' => 0, 'deadlines_updated' => 0, 'errors' => 1],
                'errors' => ['No se pudo conectar a la base de datos. Verifique que el plugin SQLite este instalado y funcionando.'],
                'warnings' => []
            ];
        }

        // Usar CalendarImporter para procesar el archivo
        require_once (function_exists('bez_plugin_base') ? bez_plugin_base() : DOKU_PLUGIN) . 'beztaxcalendar/lib/CalendarImporter.php';

        $importer = new \dokuwiki\plugin\beztaxcalendar\lib\CalendarImporter($db);
        // Pasar el nombre original del archivo para detectar la extension correctamente
        $result = $importer->import($file['tmp_name'], $year, 'deadlines', $previewOnly, $file['name']);

        // Update calendar status if successful and not preview
        if ($result['success'] && !$previewOnly && ($result['stats']['errors'] ?? 0) == 0) {
            // Obtener version actual para incrementar
            $currentStatus = $this->helper->getCalendarStatus($year);
            $version = ($currentStatus && !empty($currentStatus['version'])) ? $currentStatus['version'] + 1 : 1;

            $this->helper->updateCalendarStatus($year, [
                'status' => 'draft',
                'filename' => $file['name'],
                'file_hash' => md5_file($file['tmp_name'])
            ], $userId);

            // Guardar copia del archivo Excel en la BD para descarga posterior
            $fileContent = file_get_contents($file['tmp_name']);
            if ($fileContent !== false) {
                $this->helper->saveCalendarFile($year, $version, $file['name'], $fileContent, $userId);
            }

            // Log upload
            $this->helper->logCalendarUpload($year, $file['name'], $result['stats'], $userId);
        }

        return $result;
    }

    /**
     * Handle verify calendar
     */
    protected function handleVerifyCalendar() {
        global $INPUT;

        $year = $INPUT->int('year');
        if ($year) {
            $userId = $INPUT->server->str('REMOTE_USER');
            $this->helper->verifyCalendar($year, $userId);
            msg($this->getLang('success'), 1);
        }
    }

    /**
     * Handle save client obligations
     */
    protected function handleSaveClientObligations() {
        global $INPUT;

        $clientId = $INPUT->int('client_id');
        $selectedObligations = $INPUT->arr('obligations');
        $userId = $INPUT->server->str('REMOTE_USER');

        if (!$clientId) return;

        // Get all obligations
        $allObligations = $this->helper->getObligations();

        foreach ($allObligations as $ob) {
            $isSelected = in_array($ob['code'], $selectedObligations);
            $this->helper->setClientObligation($clientId, $ob['code'], $isSelected, '', $userId);
        }

        msg($this->getLang('success'), 1);
    }

    /**
     * Handle add exception
     */
    protected function handleAddException() {
        global $INPUT;

        $clientId = $INPUT->int('client_id');
        $year = $INPUT->int('year');
        $obligationCode = $INPUT->str('obligation_code');
        $period = $INPUT->str('period');
        $exceptionType = $INPUT->str('exception_type');
        $customDate = $INPUT->str('custom_date');
        $reason = $INPUT->str('reason');
        $userId = $INPUT->server->str('REMOTE_USER');

        if (!$clientId || !$obligationCode || !$period) {
            msg('Cliente, obligacion y periodo son obligatorios.', -1);
            return;
        }
        if (!in_array($exceptionType, ['change_date', 'exclude'])) {
            msg('Tipo de excepcion no valido.', -1);
            return;
        }
        if ($exceptionType === 'change_date' && empty($customDate)) {
            msg('Debe especificar una fecha personalizada para cambiar el vencimiento.', -1);
            return;
        }

        $this->helper->addClientException($clientId, $year, $obligationCode, $period, $exceptionType, $customDate ?: null, $reason, $userId);

        msg($this->getLang('success'), 1);
    }

    /**
     * Handle delete exception
     */
    protected function handleDeleteException() {
        global $INPUT;

        $exceptionId = $INPUT->int('exception_id');
        if ($exceptionId) {
            $this->helper->removeClientException($exceptionId);
            msg($this->getLang('success'), 1);
        }
    }

    /**
     * Handle generate tasks
     */
    protected function handleGenerateTasks() {
        global $INPUT;

        $clientId = $INPUT->int('client_id');
        $projectId = $INPUT->int('project_id');
        $year = $INPUT->int('year');
        $obligations = $INPUT->arr('obligations');

        if (!$clientId || !$projectId || !$year || empty($obligations)) {
            msg($this->getLang('error'), -1);
            return;
        }

        // Get periods from calendar master (only periods that actually have dates loaded)
        // This avoids trying to generate tasks for periods that don't exist in the calendar
        $periods = [];
        foreach ($obligations as $code) {
            // Obtener periodos disponibles del calendario maestro
            $calendarPeriods = $this->helper->getAvailablePeriodsFromCalendar($code, $year);
            if (!empty($calendarPeriods)) {
                $periods = array_merge($periods, array_keys($calendarPeriods));
            }
        }
        $periods = array_unique($periods);

        if (empty($periods)) {
            msg('No hay periodos cargados en el calendario para las obligaciones seleccionadas en el ano ' . $year, -1);
            return;
        }

        try {
            $userId = $INPUT->server->str('REMOTE_USER');
            $result = $this->helper->generateTasks($clientId, $projectId, $obligations, $year, $periods, $userId);
            $_SESSION['taxcal_generation_result'] = $result;

            msg(sprintf($this->getLang('tasks_generated') . ': %d', $result['total_generated']), 1);
        } catch (\Exception $e) {
            msg($this->getLang('error') . ': ' . $e->getMessage(), -1);
        }
    }

    /**
     * Handle save config
     */
    protected function handleSaveConfig() {
        global $INPUT;

        $configKeys = [
            'alert_days_before_year_end',
        ];

        foreach ($configKeys as $key) {
            $value = $INPUT->str($key);
            $this->helper->setConfig($key, $value);
        }

        msg($this->getLang('config_saved'), 1);
    }

    // ================================================================
    // RENDER HELPERS
    // ================================================================

    /**
     * Render status badge
     */
    protected function renderStatusBadge($status) {
        $badges = [
            'active' => '<span class="status-badge status-active">&#10003; ' . $this->getLang('status_active') . '</span>',
            'draft' => '<span class="status-badge status-draft">&#9998; ' . $this->getLang('status_draft') . '</span>',
            'archived' => '<span class="status-badge status-archived">&#128230; ' . $this->getLang('status_archived') . '</span>',
            'not_loaded' => '<span class="status-badge status-missing">&#10007; ' . $this->getLang('status_not_loaded') . '</span>'
        ];
        return $badges[$status] ?? $status;
    }

    /**
     * Render upload result
     */
    protected function renderUploadResult($result) {
        $class = $result['success'] ? 'result-success' : 'result-error';
        $title = $result['preview'] ? $this->getLang('upload_preview') : $this->getLang('upload_result');

        echo "<div class=\"taxcal-result {$class}\">";
        echo "<h3>{$title}</h3>";

        echo '<div class="stats-grid">';

        // Mostrar estadisticas de obligaciones si existen
        $obNew = $result['stats']['obligations_new'] ?? 0;
        $obUpdated = $result['stats']['obligations_updated'] ?? 0;
        if ($obNew > 0 || $obUpdated > 0) {
            echo '<div class="stat"><span class="number">' . $obNew . '</span><span class="label">Obligaciones nuevas</span></div>';
            echo '<div class="stat"><span class="number">' . $obUpdated . '</span><span class="label">Obligaciones actualizadas</span></div>';
        }

        // Estadisticas de vencimientos
        $dlNew = $result['stats']['deadlines_new'] ?? 0;
        $dlUpdated = $result['stats']['deadlines_updated'] ?? 0;
        echo '<div class="stat"><span class="number">' . $dlNew . '</span><span class="label">Vencimientos nuevos</span></div>';
        echo '<div class="stat"><span class="number">' . $dlUpdated . '</span><span class="label">Vencimientos actualizados</span></div>';
        echo '<div class="stat stat-error"><span class="number">' . ($result['stats']['errors'] ?? 0) . '</span><span class="label">' . $this->getLang('records_errors') . '</span></div>';
        echo '</div>';

        if (!empty($result['errors'])) {
            echo '<div class="errors-list"><h4>' . $this->getLang('errors_found') . ':</h4><ul>';
            foreach ($result['errors'] as $error) {
                echo '<li>' . hsc($error) . '</li>';
            }
            echo '</ul></div>';
        }

        if (!empty($result['warnings'])) {
            echo '<div class="warnings-list"><h4>Advertencias:</h4><ul>';
            foreach ($result['warnings'] as $warning) {
                echo '<li>' . hsc($warning) . '</li>';
            }
            echo '</ul></div>';
        }

        // Si es preview exitoso, mostrar boton para confirmar
        if ($result['preview'] && $result['success'] && ($result['stats']['errors'] ?? 0) == 0) {
            echo '<div class="preview-actions" style="margin-top:15px; padding-top:15px; border-top:1px solid #ccc;">';
            echo '<p><strong>¿Desea importar estos datos?</strong></p>';
            echo '<p>Desmarque la opcion "Solo vista previa" y vuelva a cargar el archivo para guardar los cambios.</p>';
            echo '</div>';
        }

        echo '</div>';
    }

    /**
     * Render generation result
     */
    protected function renderGenerationResult($result) {
        echo '<div class="taxcal-result result-success">';
        echo '<h3>' . $this->getLang('generation_result') . '</h3>';

        echo '<div class="stats-grid">';
        echo '<div class="stat"><span class="number">' . $result['total_generated'] . '</span><span class="label">' . $this->getLang('tasks_generated') . '</span></div>';
        echo '<div class="stat"><span class="number">' . $result['total_skipped'] . '</span><span class="label">' . $this->getLang('tasks_skipped') . '</span></div>';
        echo '</div>';

        if (!empty($result['generated'])) {
            echo '<h4>Tareas creadas:</h4><ul>';
            foreach ($result['generated'] as $task) {
                echo '<li>' . hsc($task['obligation']) . ' - ' . hsc($task['period']) . ' (' . $task['deadline'] . ') - Task #' . $task['task_id'] . '</li>';
            }
            echo '</ul>';
        }

        // Mostrar detalles de tareas omitidas
        if (!empty($result['skipped'])) {
            echo '<h4 style="color: #b45309; margin-top: 20px;">Tareas omitidas (' . count($result['skipped']) . '):</h4>';

            // Agrupar por razon
            $byReason = [];
            foreach ($result['skipped'] as $skip) {
                $reason = $skip['reason'] ?? 'unknown';
                if (!isset($byReason[$reason])) {
                    $byReason[$reason] = [];
                }
                $byReason[$reason][] = $skip;
            }

            // Explicacion de razones
            $reasonLabels = [
                'already_exists' => 'Ya generada anteriormente (existe en log)',
                'excluded' => 'Excluida por configuracion del cliente',
                'no_date' => 'Sin fecha en calendario maestro (periodo no cargado)'
            ];

            foreach ($byReason as $reason => $items) {
                $label = $reasonLabels[$reason] ?? $reason;
                echo '<div style="margin: 10px 0; padding: 10px; background: #fef3c7; border: 1px solid #fcd34d; border-radius: 6px;">';
                echo '<strong>' . hsc($label) . ' (' . count($items) . '):</strong>';
                echo '<ul style="margin: 5px 0 0 20px; font-size: 0.9em;">';
                foreach (array_slice($items, 0, 10) as $item) {
                    echo '<li>' . hsc($item['code']) . ' - Periodo: ' . hsc($item['period']) . '</li>';
                }
                if (count($items) > 10) {
                    echo '<li>... y ' . (count($items) - 10) . ' mas</li>';
                }
                echo '</ul>';
                echo '</div>';
            }

            // Sugerencia si hay muchos no_date
            if (isset($byReason['no_date']) && count($byReason['no_date']) > 5) {
                echo '<div style="margin-top: 15px; padding: 12px; background: #dbeafe; border: 1px solid #93c5fd; border-radius: 6px;">';
                echo '<strong>Sugerencia:</strong> Hay muchos periodos sin fecha. Verifique que el calendario tributario tiene cargados todos los periodos necesarios para el anio seleccionado.';
                echo '<br><small>Vaya a la pestania "Ver Calendario" para verificar que periodos estan disponibles.</small>';
                echo '</div>';
            }
        }

        echo '</div>';
    }

    /**
     * Render quick stats
     */
    protected function renderQuickStats() {
        $obligations = $this->helper->getObligations();
        $clients = $this->helper->getClients();

        echo '<div class="taxcal-quick-stats">';
        echo '<div class="quick-stat"><span class="number">' . count($obligations) . '</span><span class="label">Obligaciones configuradas</span></div>';
        echo '<div class="quick-stat"><span class="number">' . count($clients) . '</span><span class="label">Clientes</span></div>';
        echo '</div>';
    }

    // ================================================================
    // PDF EXPORT
    // ================================================================

    /**
     * Render PDF export button
     *
     * @param string $tabType view|obligations|client
     * @param array $params Parametros adicionales para la URL
     */
    protected function renderPdfButton($tabType, $params = []) {
        global $ID;

        $urlParams = array_merge([
            'do' => 'admin',
            'page' => 'beztaxcalendar',
            'action' => 'export_pdf',
            'pdf_tab' => $tabType,
            'sectok' => getSecurityToken(),
        ], $params);

        $url = wl($ID, $urlParams);

        echo '<a href="' . $url . '" class="btn-pdf-export" target="_blank" title="Exportar a PDF">';
        echo '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align: middle; margin-right: 4px;">';
        echo '<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>';
        echo '<polyline points="14 2 14 8 20 8"/>';
        echo '<line x1="16" y1="13" x2="8" y2="13"/>';
        echo '<line x1="16" y1="17" x2="8" y2="17"/>';
        echo '<polyline points="10 9 9 9 8 9"/>';
        echo '</svg>';
        echo 'PDF';
        echo '</a>';
    }

    /**
     * Handle PDF export - Genera pagina HTML limpia para imprimir/guardar como PDF
     * No requiere libreria mPDF ni dependencias externas.
     * El navegador abre el dialogo de impresion automaticamente.
     */
    protected function handleExportPDF() {
        global $INPUT;

        $tabType = $INPUT->str('pdf_tab', '');

        if (!in_array($tabType, ['view', 'obligations', 'client'])) {
            msg('Tipo de exportacion PDF no valido', -1);
            return;
        }

        $htmlContent = '';
        $title = 'Calendario Tributario DIAN';
        $subtitle = '';
        $orientation = 'landscape';

        switch ($tabType) {
            case 'view':
                $year = $INPUT->int('year', date('Y'));
                $title = 'Calendario Tributario DIAN ' . $year;
                $subtitle = 'Vencimientos por ultimo digito NIT';
                $htmlContent = $this->generatePdfCalendarView($year);
                break;

            case 'obligations':
                $title = 'Obligaciones Tributarias';
                $subtitle = 'Catalogo de obligaciones configuradas';
                $orientation = 'portrait';
                $htmlContent = $this->generatePdfObligations();
                break;

            case 'client':
                $clientId = $INPUT->int('client_id', 0);
                $year = $INPUT->int('year', date('Y'));
                if (!$clientId) {
                    msg('Seleccione un cliente para exportar', -1);
                    return;
                }
                $client = $this->helper->getClient($clientId);
                $title = 'Configuracion Tributaria - ' . ($client['name'] ?? 'Cliente');
                $subtitle = 'Ano fiscal ' . $year;
                $htmlContent = $this->generatePdfClientConfig($clientId, $year);
                break;
        }

        if (empty($htmlContent)) {
            msg('No hay datos para exportar', -1);
            return;
        }

        // Generar PDF con mPDF via TaxCalendarPDF
        try {
            require_once __DIR__ . '/lib/TaxCalendarPDF.php';
            $pdf = new \dokuwiki\plugin\beztaxcalendar\lib\TaxCalendarPDF($title, $subtitle, $orientation);
            $pdfContent = $pdf->generate($htmlContent, $tabType);

            $filename = 'calendario_tributario_' . date('Y-m-d') . '.pdf';

            header('Content-Type: application/pdf');
            header('Content-Disposition: attachment; filename="' . $filename . '"');
            header('Content-Length: ' . strlen($pdfContent));
            header('Cache-Control: private, max-age=0, must-revalidate');
            header('Pragma: public');

            echo $pdfContent;
            exit;

        } catch (\Exception $e) {
            msg('Error al generar PDF: ' . hsc($e->getMessage()), -1);
        }
    }

    /**
     * Genera HTML del calendario maestro para PDF
     */
    protected function generatePdfCalendarView($year) {
        $calendar = $this->helper->getMasterCalendar($year);

        if (empty($calendar)) {
            return '<p>No hay datos de calendario para el ano ' . intval($year) . '</p>';
        }

        // Agrupar por obligacion
        $grouped = [];
        foreach ($calendar as $row) {
            $code = $row['obligation_code'];
            if (!isset($grouped[$code])) {
                $grouped[$code] = [
                    'name' => $row['obligation_name'],
                    'frequency' => $row['frequency'],
                    'periods' => []
                ];
            }
            $grouped[$code]['periods'][] = $row;
        }

        $html = '';
        foreach ($grouped as $code => $data) {
            $html .= '<div class="obligation-section">';
            $html .= '<h3>' . htmlspecialchars($data['name']) . ' <small>(' . htmlspecialchars($code) . ')</small></h3>';

            $html .= '<table>';
            $html .= '<thead><tr>';
            $html .= '<th style="text-align:left; min-width:100px;">Periodo</th>';
            for ($d = 1; $d <= 9; $d++) {
                $html .= '<th>Dig. ' . $d . '</th>';
            }
            $html .= '<th>Dig. 0</th>';
            $html .= '<th>Notas</th>';
            $html .= '</tr></thead>';
            $html .= '<tbody>';

            foreach ($data['periods'] as $period) {
                $html .= '<tr>';
                $html .= '<td style="text-align:left;"><strong>' . htmlspecialchars($period['period_name']) . '</strong><br><small>' . htmlspecialchars($period['period']) . '</small></td>';
                for ($d = 1; $d <= 9; $d++) {
                    $date = $period["digit_{$d}"] ?? '';
                    $html .= '<td>' . ($date ? date('d/m', strtotime($date)) : '-') . '</td>';
                }
                $date0 = $period['digit_0'] ?? '';
                $html .= '<td>' . ($date0 ? date('d/m', strtotime($date0)) : '-') . '</td>';
                $html .= '<td style="text-align:left; font-size:7pt;">' . htmlspecialchars($period['notes'] ?? '') . '</td>';
                $html .= '</tr>';
            }

            $html .= '</tbody></table>';
            $html .= '</div>';
        }

        return $html;
    }

    /**
     * Genera HTML de obligaciones para PDF
     */
    protected function generatePdfObligations() {
        $obligations = $this->helper->getObligations();

        if (empty($obligations)) {
            return '<p>No hay obligaciones configuradas</p>';
        }

        $freqLabels = [
            'monthly' => 'Mensual',
            'bimonthly' => 'Bimestral',
            'quarterly' => 'Trimestral',
            'annual' => 'Anual',
            'custom' => 'Personalizada'
        ];

        $html = '<table>';
        $html .= '<thead><tr>';
        $html .= '<th style="text-align:left;">Codigo</th>';
        $html .= '<th style="text-align:left;">Nombre</th>';
        $html .= '<th style="text-align:left;">Descripcion</th>';
        $html .= '<th>Frecuencia</th>';
        $html .= '<th>Tipo Contribuyente</th>';
        $html .= '<th>Activa</th>';
        $html .= '</tr></thead>';
        $html .= '<tbody>';

        foreach ($obligations as $ob) {
            $html .= '<tr>';
            $html .= '<td style="text-align:left;"><code>' . htmlspecialchars($ob['code']) . '</code></td>';
            $html .= '<td style="text-align:left;">' . htmlspecialchars($ob['name']) . '</td>';
            $html .= '<td style="text-align:left; font-size:7pt;">' . htmlspecialchars($ob['description'] ?? '') . '</td>';
            $html .= '<td>' . ($freqLabels[$ob['frequency']] ?? $ob['frequency']) . '</td>';
            $html .= '<td style="font-size:7pt;">' . htmlspecialchars($ob['taxpayer_type'] ?? '') . '</td>';
            $html .= '<td>' . ($ob['is_active'] ? '&#10003;' : '-') . '</td>';
            $html .= '</tr>';
        }

        $html .= '</tbody></table>';

        return $html;
    }

    /**
     * Genera HTML de configuracion por cliente para PDF
     */
    protected function generatePdfClientConfig($clientId, $year) {
        $client = $this->helper->getClient($clientId);

        if (!$client) {
            return '<p>Cliente no encontrado</p>';
        }

        $html = '';

        // Info del cliente — más completa
        $html .= '<div class="client-info" style="background:#f0f4f8;padding:15px;border-radius:8px;margin-bottom:20px;">';
        $html .= '<h3 style="margin:0 0 10px 0;color:#1a365d;">' . htmlspecialchars($client['name']) . '</h3>';
        $html .= '<table style="width:100%;border:none;">';
        if (!empty($client['nit'])) {
            $nitLastDigit = $this->helper->extractNitLastDigit($client['nit']);
            $html .= '<tr><td style="border:none;width:200px;"><strong>NIT:</strong></td><td style="border:none;">' . htmlspecialchars($client['nit']) . '</td></tr>';
            $html .= '<tr><td style="border:none;"><strong>Ultimo digito NIT:</strong></td><td style="border:none;"><strong style="color:#c62828;font-size:18px;">' . $nitLastDigit . '</strong></td></tr>';
        }
        if (!empty($client['codigo_verificacion'])) {
            $html .= '<tr><td style="border:none;"><strong>Digito verificacion:</strong></td><td style="border:none;">' . htmlspecialchars($client['codigo_verificacion']) . '</td></tr>';
        }
        // Responsable del calendario
        $responsible = $this->helper->getClientResponsible($clientId);
        if ($responsible) {
            $html .= '<tr><td style="border:none;"><strong>Responsable tributario:</strong></td><td style="border:none;">' . htmlspecialchars($responsible) . '</td></tr>';
        } else {
            // Buscar coordinador de proyectos del cliente
            $projects = $this->helper->getClientProjects($clientId);
            $coordinators = [];
            foreach ($projects as $p) {
                if (!empty($p['coordinator']) && !in_array($p['coordinator'], $coordinators)) {
                    $coordinators[] = $p['coordinator'];
                }
            }
            $html .= '<tr><td style="border:none;"><strong>Responsable tributario:</strong></td><td style="border:none;">' . (!empty($coordinators) ? htmlspecialchars(implode(', ', $coordinators)) . ' (coordinador)' : 'Sin asignar') . '</td></tr>';
        }
        $html .= '<tr><td style="border:none;"><strong>Ano fiscal:</strong></td><td style="border:none;">' . intval($year) . '</td></tr>';
        $html .= '</table>';
        $html .= '</div>';

        // Obligaciones asignadas — como tabla
        $obligations = $this->helper->getClientObligations($clientId);
        $assignedObs = array_filter($obligations, function($o) { return $o['is_assigned']; });
        $html .= '<h4>Obligaciones Tributarias Asignadas (' . count($assignedObs) . ')</h4>';
        if (!empty($assignedObs)) {
            $html .= '<table>';
            $html .= '<thead><tr><th style="text-align:left;">Obligacion</th><th>Codigo</th><th>Frecuencia</th></tr></thead>';
            $html .= '<tbody>';
            $freqLabels = ['monthly' => 'Mensual', 'bimonthly' => 'Bimestral', 'quarterly' => 'Trimestral', 'annual' => 'Anual', 'custom' => 'Personalizada'];
            foreach ($assignedObs as $ob) {
                $html .= '<tr>';
                $html .= '<td style="text-align:left;">' . htmlspecialchars($ob['name']) . '</td>';
                $html .= '<td><code>' . htmlspecialchars($ob['code']) . '</code></td>';
                $html .= '<td>' . ($freqLabels[$ob['frequency']] ?? $ob['frequency']) . '</td>';
                $html .= '</tr>';
            }
            $html .= '</tbody></table>';
        } else {
            $html .= '<p style="color:#666;">Sin obligaciones asignadas</p>';
        }

        // Excepciones
        $exceptions = $this->helper->getClientExceptions($clientId, $year);
        if (!empty($exceptions)) {
            $html .= '<h4>Excepciones ' . intval($year) . '</h4>';
            $html .= '<table>';
            $html .= '<thead><tr>';
            $html .= '<th style="text-align:left;">Obligacion</th>';
            $html .= '<th>Periodo</th>';
            $html .= '<th>Tipo</th>';
            $html .= '<th>Fecha Personalizada</th>';
            $html .= '<th style="text-align:left;">Razon</th>';
            $html .= '</tr></thead>';
            $html .= '<tbody>';

            $exTypeLabels = ['change_date' => 'Cambio de fecha', 'exclude' => 'Excluir'];

            foreach ($exceptions as $ex) {
                $html .= '<tr>';
                $html .= '<td style="text-align:left;">' . htmlspecialchars($ex['obligation_name']) . '</td>';
                $html .= '<td>' . htmlspecialchars($ex['period']) . '</td>';
                $html .= '<td>' . ($exTypeLabels[$ex['exception_type']] ?? $ex['exception_type']) . '</td>';
                $html .= '<td>' . ($ex['custom_date'] ? date('d/m/Y', strtotime($ex['custom_date'])) : '-') . '</td>';
                $html .= '<td style="text-align:left;">' . htmlspecialchars($ex['reason'] ?? '') . '</td>';
                $html .= '</tr>';
            }

            $html .= '</tbody></table>';
        }

        // Calendario efectivo
        $calendar = $this->helper->getClientEffectiveCalendar($clientId, $year);
        if (!empty($calendar)) {
            $html .= '<h4>Calendario Efectivo ' . intval($year) . '</h4>';
            $html .= '<table>';
            $html .= '<thead><tr>';
            $html .= '<th>Fecha Vencimiento</th>';
            $html .= '<th style="text-align:left;">Obligacion</th>';
            $html .= '<th>Periodo</th>';
            $html .= '<th>Fuente</th>';
            $html .= '</tr></thead>';
            $html .= '<tbody>';

            foreach ($calendar as $item) {
                $trClass = $item['source'] === 'exception' ? ' class="source-exception"' : '';
                $html .= '<tr' . $trClass . '>';
                $html .= '<td>' . ($item['effective_date'] ? date('d/m/Y', strtotime($item['effective_date'])) : '-') . '</td>';
                $html .= '<td style="text-align:left;">' . htmlspecialchars($item['obligation_name']) . '</td>';
                $html .= '<td>' . htmlspecialchars($item['period_name']) . '</td>';
                $html .= '<td>' . ($item['source'] === 'exception' ? 'Excepcion' : 'Maestro') . '</td>';
                $html .= '</tr>';
            }

            $html .= '</tbody></table>';
        }

        return $html;
    }

    /**
     * Render CSS styles
     */
    protected function renderStyles() {
        echo '<style>
.bez-taxcalendar-admin { max-width: 1200px; margin: 0 auto; }
.bez-taxcalendar-admin h1 { border-bottom: 2px solid #333; padding-bottom: 10px; }
.bez-taxcalendar-admin h2 { color: #333; margin-top: 30px; }

/* Tabs */
.taxcal-tabs { display: flex; gap: 5px; margin: 20px 0; flex-wrap: wrap; border-bottom: 2px solid #ddd; padding-bottom: 10px; }
.taxcal-tab { padding: 8px 16px; background: #f5f5f5; border: 1px solid #ddd; border-radius: 4px 4px 0 0; text-decoration: none; color: #333; }
.taxcal-tab:hover { background: #e9e9e9; }
.taxcal-tab.active { background: #2b73b7; color: white; border-color: #2b73b7; }

/* Alerts */
.taxcal-alerts { margin: 20px 0; }
.taxcal-alert { padding: 12px 16px; border-radius: 6px; margin-bottom: 10px; display: flex; justify-content: space-between; align-items: center; }
.alert-error { background: #fee; border-left: 4px solid #c00; }
.alert-warning { background: #fff8e6; border-left: 4px solid #f90; }

/* Tables */
.taxcal-table { width: 100%; border-collapse: collapse; margin: 15px 0; }
.taxcal-table th, .taxcal-table td { padding: 10px 12px; border: 1px solid #ddd; text-align: left; }
.taxcal-table th { background: #f5f5f5; font-weight: 600; }
.taxcal-table tr:hover { background: #fafafa; }
.taxcal-table tr.current-year { background: #e7f3ff; }
.taxcal-table .actions { white-space: nowrap; }

/* Calendar deadlines table */
.taxcal-deadlines th, .taxcal-deadlines td { padding: 6px 8px; font-size: 13px; text-align: center; }
.taxcal-deadlines td:first-child { text-align: left; }

/* Status badges */
.status-badge { padding: 3px 8px; border-radius: 12px; font-size: 12px; font-weight: 500; }
.status-active { background: #d4edda; color: #155724; }
.status-draft { background: #fff3cd; color: #856404; }
.status-archived { background: #e2e3e5; color: #383d41; }
.status-missing { background: #f8d7da; color: #721c24; }
.badge-current { background: #2b73b7; color: white; font-size: 10px; padding: 2px 6px; border-radius: 10px; margin-left: 5px; }

/* Buttons */
.btn-small { padding: 4px 10px; font-size: 12px; border-radius: 4px; text-decoration: none; display: inline-block; margin: 2px; border: none; cursor: pointer; }
.btn-primary, .bez-taxcalendar-admin button.btn-primary { background: #2b73b7; color: white; padding: 8px 16px; border: none; border-radius: 4px; cursor: pointer; }
.btn-primary:hover { background: #1e5a8a; }
.btn-secondary { background: #6c757d; color: white; padding: 8px 16px; border: none; border-radius: 4px; cursor: pointer; text-decoration: none; display: inline-block; }
.btn-success { background: #28a745; color: white; }
.btn-danger { background: #dc3545; color: white; }

/* Forms */
.taxcal-form { max-width: 600px; }
.form-row { margin-bottom: 15px; }
.form-row label { display: block; margin-bottom: 5px; font-weight: 500; }
.form-row input[type="text"], .form-row input[type="number"], .form-row input[type="date"],
.form-row input[type="file"], .form-row select { width: 100%; max-width: 400px; padding: 8px; border: 1px solid #ddd; border-radius: 4px; }
.form-row select { max-width: 420px; }
.checkbox-label { display: flex !important; align-items: center; gap: 8px; font-weight: normal !important; }
.checkbox-label input { width: auto !important; }
.form-actions { margin-top: 20px; padding-top: 20px; border-top: 1px solid #ddd; }

/* Info box */
.taxcal-info-box { background: #e7f3ff; border: 1px solid #b3d7ff; border-radius: 8px; padding: 15px; margin-bottom: 20px; }

/* Filter */
.taxcal-filter { background: #f9f9f9; padding: 15px; border-radius: 8px; margin-bottom: 20px; display: flex; gap: 20px; flex-wrap: wrap; }
.taxcal-filter label { display: flex; align-items: center; gap: 8px; }
.taxcal-filter select { padding: 6px 10px; }

/* Sections */
.taxcal-section { background: #fff; border: 1px solid #ddd; border-radius: 8px; padding: 20px; margin: 20px 0; }
.taxcal-section h4 { margin-top: 0; border-bottom: 1px solid #eee; padding-bottom: 10px; }

/* Obligations grid */
.obligations-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 10px; margin: 10px 0; }
.obligation-checkbox { display: flex; align-items: center; gap: 8px; padding: 8px; background: #f9f9f9; border-radius: 4px; cursor: pointer; }
.obligation-checkbox:hover { background: #f0f0f0; }

/* Client info */
.client-info { background: #f0f7ff; padding: 15px; border-radius: 8px; margin-bottom: 20px; }
.client-info h3 { margin: 0 0 5px; }
.client-info p { margin: 0; color: #666; }

/* Results */
.taxcal-result { border-radius: 8px; padding: 20px; margin: 20px 0; }
.result-success { background: #d4edda; border: 1px solid #c3e6cb; }
.result-error { background: #f8d7da; border: 1px solid #f5c6cb; }
.taxcal-result h3 { margin-top: 0; }

/* Stats grid */
.stats-grid { display: flex; gap: 20px; margin: 15px 0; }
.stat { text-align: center; padding: 15px 25px; background: #fff; border-radius: 8px; }
.stat .number { display: block; font-size: 28px; font-weight: bold; color: #2b73b7; }
.stat .label { display: block; font-size: 12px; color: #666; }
.stat.stat-error .number { color: #dc3545; }

/* Quick stats */
.taxcal-quick-stats { display: flex; gap: 20px; margin-top: 30px; }
.quick-stat { background: #f9f9f9; border: 1px solid #ddd; border-radius: 8px; padding: 20px; text-align: center; flex: 1; }
.quick-stat .number { display: block; font-size: 36px; font-weight: bold; color: #2b73b7; }
.quick-stat .label { display: block; font-size: 14px; color: #666; margin-top: 5px; }

/* Empty state */
.taxcal-empty { color: #666; font-style: italic; padding: 20px; text-align: center; background: #f9f9f9; border-radius: 8px; }

/* Add exception form */
.add-exception-form { margin-top: 20px; padding-top: 20px; border-top: 1px solid #eee; }
.add-exception-form h5 { margin-bottom: 15px; }
.add-exception-form .form-row { display: inline-block; margin-right: 15px; margin-bottom: 10px; }
.add-exception-form input, .add-exception-form select { padding: 6px 10px; }

/* Source styles */
.source-exception { background: #fff3cd; }

/* Errors/warnings list */
.errors-list, .warnings-list { margin-top: 15px; }
.errors-list { color: #721c24; }
.warnings-list { color: #856404; }

/* Obligation section */
.obligation-section { margin-bottom: 30px; }
.obligation-section h3 { background: #f5f5f5; padding: 10px 15px; margin: 0 0 10px; border-radius: 4px; }
.obligation-section h3 small { color: #666; font-weight: normal; }

/* Config section */
.config-section { background: #f9f9f9; border: 1px solid #eee; border-radius: 8px; padding: 20px; margin-bottom: 20px; }
.config-section h3 { margin-top: 0; padding-bottom: 10px; border-bottom: 1px solid #ddd; }

/* PDF button and header row */
.taxcal-header-row { display: flex; justify-content: space-between; align-items: center; margin-top: 30px; }
.taxcal-header-row h2 { margin-top: 0; margin-bottom: 0; flex: 1; }
.btn-pdf-export { display: inline-flex; align-items: center; padding: 6px 14px; background: #dc3545; color: white !important; border: none; border-radius: 4px; text-decoration: none !important; font-size: 13px; font-weight: 500; cursor: pointer; transition: background 0.2s; white-space: nowrap; }
.btn-pdf-export:hover { background: #c82333; text-decoration: none !important; }
.btn-pdf-export:visited { color: white !important; }
.btn-pdf-export svg { stroke: white; }

/* Toggle desplegable estilo bez */
.toggle-obligation-btn {
    width: 100%;
    margin: 0 auto 5px auto;
    background: #3d4852;
    color: #ffffff;
    border: none;
    padding: 12px 20px;
    border-radius: 10px;
    font-size: 14px;
    font-weight: 600;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    transition: all 0.3s ease;
    box-shadow: 0 3px 10px rgba(61, 72, 82, 0.25);
    text-align: left;
}
.toggle-obligation-btn:hover {
    background: #4a5568;
    box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
}
.toggle-obligation-btn.active {
    background: #1e3a5f;
    border-radius: 10px 10px 0 0;
    box-shadow: 0 3px 10px rgba(30, 58, 95, 0.35);
}
.toggle-obligation-btn.active:hover { background: #2a4a73; }
.toggle-obligation-btn .arrow-icon { transition: transform 0.3s ease; flex-shrink: 0; }
.toggle-obligation-btn.active .arrow-icon { transform: rotate(180deg); }
.toggle-obligation-btn svg path { fill: #ffffff; }
.ob-toggle-name { flex: 1; }
.ob-toggle-name small { font-weight: 400; opacity: 0.8; }
.toggle-obligation-btn.ob-inactive {
    background: #7a8a9a;
    border-left: 4px solid #e53e3e;
}
.toggle-obligation-btn.ob-inactive:hover { background: #8a9aaa; }

/* Contenedor desplegable */
.obligation-collapse-wrapper {
    max-height: 0;
    overflow: hidden;
    transition: max-height 0.4s ease-out;
}
.obligation-collapse-wrapper.expanded {
    max-height: 3000px;
    transition: max-height 0.6s ease-in;
}
.obligation-collapse-inner {
    background: #fff;
    border-radius: 0 0 10px 10px;
    box-shadow: 0 3px 12px rgba(0,0,0,0.08);
    padding: 10px 8px;
    margin-bottom: 8px;
}

/* Barra de acciones unificada */
.taxcal-action-bar {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    align-items: center;
    margin-top: 8px;
    padding: 8px 4px;
    border-top: 1px solid #e2e8f0;
}
.taxcal-action-item { display: flex; align-items: center; gap: 4px; }

/* Botones compactos */
.taxcal-btn {
    display: inline-block;
    padding: 4px 10px;
    border: none;
    border-radius: 4px;
    cursor: pointer;
    font-size: 11px;
    font-weight: 600;
    text-decoration: none;
    white-space: nowrap;
    line-height: 1.4;
}
.taxcal-btn-red { background: #dc2626; color: #fff; }
.taxcal-btn-red:hover { background: #b91c1c; color: #fff; }
.taxcal-btn-green { background: #059669; color: #fff; }
.taxcal-btn-green:hover { background: #047857; color: #fff; }
.taxcal-btn-blue { background: #2b73b7; color: #fff; }
.taxcal-btn-blue:hover { background: #1e5a8a; color: #fff; }
.taxcal-select-sm { padding: 3px 6px; border: 1px solid #d1d5db; border-radius: 4px; font-size: 11px; max-width: 140px; }
.taxcal-input-sm { padding: 3px 6px; border: 1px solid #d1d5db; border-radius: 4px; font-size: 11px; }
</style>';
    }
}
