<?php
require_once '../../core/config.php';
require_once '../../core/session.php';
require_once '../../core/database.php';
require_once '../../core/permissions.php';
require_once '../../core/format.php';
require_once __DIR__ . '/../customers/_customer_quick_fields.php';

require_login();
require_permission('module.crm');

$page_title = 'Cotizaciones';
$pdo = db();
$billingConfiguredEnvironment = billing_runtime_environment(null);
$billingConfiguredConfirmationText = billing_issue_confirmation_text($billingConfiguredEnvironment);
$hasInvoiceIssueDate = q_col_exists($pdo, 'crm_quotes', 'invoice_issue_date');
$hasInvoiceNumber = q_col_exists($pdo, 'crm_quotes', 'invoice_number');
$hasPaymentDate = q_col_exists($pdo, 'crm_quotes', 'payment_date');
$hasFactoDocumentId = q_col_exists($pdo, 'crm_quotes', 'facto_document_id');
$hasQuoteDeletedAt = q_col_exists($pdo, 'crm_quotes', 'deleted_at');
$hasQuoteDeletedBy = q_col_exists($pdo, 'crm_quotes', 'deleted_by');
$hasQuoteDeletedReason = q_col_exists($pdo, 'crm_quotes', 'deleted_reason');
$hasCustomerDeletedAt = q_col_exists($pdo, 'crm_customers', 'deleted_at');
$showDeletedQuotes = $hasQuoteDeletedAt && isset($_GET['view']) && $_GET['view'] === 'deleted';

$allowedStatuses = array('Borrador', 'Enviada', 'Aceptada', 'Facturada', 'Esperando pago', 'Pagada', 'Rechazada', 'Vencida', 'Anulada');
$systemManagedStatuses = array('Facturada', 'Esperando pago', 'Pagada');
$editableQuoteStatuses = array('Borrador', 'Enviada', 'Aceptada', 'Rechazada', 'Vencida', 'Anulada');

function q_col_exists($pdo, $table, $column)
{
    static $cache = array();
    $key = $table . '.' . $column;

    if (array_key_exists($key, $cache)) {
        return $cache[$key];
    }

    $stmt = $pdo->prepare("SHOW COLUMNS FROM `" . str_replace('`', '', $table) . "` LIKE ?");
    $stmt->execute(array($column));
    $cache[$key] = (bool)$stmt->fetch(PDO::FETCH_ASSOC);

    return $cache[$key];
}

function q_table_exists($pdo, $table)
{
    static $cache = array();
    $table = str_replace('`', '', (string)$table);

    if (array_key_exists($table, $cache)) {
        return $cache[$table];
    }

    $stmt = $pdo->prepare("SHOW TABLES LIKE ?");
    $stmt->execute(array($table));
    $cache[$table] = (bool)$stmt->fetchColumn();

    return $cache[$table];
}

function q_date($date)
{
    return $date ? date('d-m-Y', strtotime($date)) : '-';
}

function q_compact_quote_number($quoteNumber)
{
    $quoteNumber = (string)$quoteNumber;
    if (preg_match('/^([A-Z]+)-\d{4}-(\d+)$/', $quoteNumber, $m)) {
        return $m[1] . '-' . $m[2];
    }
    return $quoteNumber;
}

function q_badge($status)
{
    if (in_array($status, array('Aceptada', 'Pagada'), true)) return 'badge-success';
    if (in_array($status, array('Enviada', 'Facturada', 'Esperando pago'), true)) return 'badge-warning';
    if (in_array($status, array('Rechazada', 'Vencida', 'Anulada'), true)) return 'badge-error';
    return '';
}

function q_status_options($statuses, $current, $systemManagedStatuses = array())
{
    $html = '';

    foreach ($statuses as $status) {
        $selected = $status === $current ? ' selected' : '';
        $disabled = in_array($status, $systemManagedStatuses, true) && $status !== $current ? ' disabled' : '';
        $label = in_array($status, $systemManagedStatuses, true) ? $status . ' · automático' : $status;
        $html .= '<option value="' . safe_text($status) . '"' . $selected . $disabled . '>' . safe_text($label) . '</option>';
    }

    return $html;
}

function q_phone($phone)
{
    $phone = preg_replace('/[^0-9]/', '', (string)$phone);
    if ($phone === '') return '';
    if (strlen($phone) === 9) return '56' . $phone;
    if (strlen($phone) === 8) return '569' . $phone;
    return $phone;
}

function q_display_customer($row)
{
    if (!empty($row['commercial_name'])) return $row['commercial_name'];
    if (!empty($row['customer_name'])) return $row['customer_name'];
    return $row['legal_name'] ?? '';
}

function q_legal_customer($row)
{
    if (!empty($row['legal_name'])) return $row['legal_name'];
    return $row['customer_name'] ?? '';
}

function q_split_item($item)
{
    $name = (string)($item['name'] ?? '');
    $sku = '';

    if (preg_match('/\s*\(SKU:\s*([^)]+)\)\s*$/i', $name, $m)) {
        $sku = trim($m[1]);
        $name = preg_replace('/\s*\(SKU:\s*([^)]+)\)\s*$/i', '', $name);
    }

    if ($sku === '' && !empty($item['description'])) {
        if (preg_match('/SKU:\s*(.+)$/i', (string)$item['description'], $m)) {
            $sku = trim($m[1]);
        } else {
            $sku = trim((string)$item['description']);
        }
    }

    $item['display_name'] = trim($name);
    $item['sku'] = $sku;
    $allowedUnits = array('unidad', 'mL', 'L', 'g', 'kg', 'pack', 'caja', 'servicio');
    $item['unit'] = in_array(($item['unit'] ?? 'unidad'), $allowedUnits, true) ? $item['unit'] : 'unidad';
    return $item;
}

$stmt = $pdo->prepare("
    SELECT
        q.*,
        " . ($hasInvoiceIssueDate ? "q.invoice_issue_date" : "NULL AS invoice_issue_date") . ",
        " . ($hasInvoiceNumber ? "q.invoice_number" : "NULL AS invoice_number") . ",
        " . ($hasPaymentDate ? "q.payment_date" : "NULL AS payment_date") . ",
        " . ($hasFactoDocumentId ? "q.facto_document_id" : "NULL AS facto_document_id") . ",
        c.customer_name,
        c.commercial_name,
        c.legal_name,
        c.customer_tax_id,
        c.export_tax_id_code,
        c.export_tax_id_type,
        c.tax_id_type,
        c.email,
        c.email_dte,
        c.phone,
        c.address,
        c.district,
        c.city,
        c.country,
        c.country_id,
        c.export_country_id,
        c.business_activity,
        o.title AS opportunity_title,
        " . ($hasQuoteDeletedAt ? "q.deleted_at" : "NULL AS deleted_at") . ",
        " . ($hasQuoteDeletedBy ? "q.deleted_by" : "NULL AS deleted_by") . ",
        " . ($hasQuoteDeletedReason ? "q.deleted_reason" : "NULL AS deleted_reason") . "
    FROM crm_quotes q
    JOIN crm_customers c ON q.customer_id = c.id
    LEFT JOIN crm_opportunities o ON q.opportunity_id = o.id
    " . ($hasQuoteDeletedAt
        ? ($showDeletedQuotes
            ? "WHERE q.deleted_at IS NOT NULL"
            : "WHERE q.deleted_at IS NULL" . ($hasCustomerDeletedAt ? " AND c.deleted_at IS NULL" : ""))
        : ($hasCustomerDeletedAt ? "WHERE c.deleted_at IS NULL" : "")) . "
    ORDER BY q.created_at DESC, q.id DESC
");
$stmt->execute();
$quotes = $stmt->fetchAll(PDO::FETCH_ASSOC);

$quoteIds = array_map(function ($q) {
    return (int)$q['id'];
}, $quotes);

$itemsByQuote = array();

if (!empty($quoteIds)) {
    $ph = implode(',', array_fill(0, count($quoteIds), '?'));
    $stmtItems = $pdo->prepare("SELECT * FROM crm_quote_items WHERE quote_id IN ($ph) ORDER BY id ASC");
    $stmtItems->execute($quoteIds);

    foreach ($stmtItems->fetchAll(PDO::FETCH_ASSOC) as $item) {
        $qid = (int)$item['quote_id'];
        if (!isset($itemsByQuote[$qid])) $itemsByQuote[$qid] = array();
        $itemsByQuote[$qid][] = q_split_item($item);
    }
}


$billingIssueByQuote = array();

if (!empty($quoteIds) && q_table_exists($pdo, 'billing_issue_logs')) {
    $ph = implode(',', array_fill(0, count($quoteIds), '?'));
    $stmtBillingIssues = $pdo->prepare("
        SELECT id, quote_id, environment, mode, status, facto_document_id, document_number, pdf_file_path, xml_file_path,
               response_payload, error_code, error_message, created_at, updated_at
        FROM billing_issue_logs
        WHERE quote_id IN ($ph)
          AND (
              (mode = 'issue' AND status IN ('success', 'sent', 'pending_reconcile', 'error'))
              OR (mode = 'draft_preview' AND status IN ('validated', 'error'))
          )
        ORDER BY id DESC
    ");
    $stmtBillingIssues->execute($quoteIds);

    foreach ($stmtBillingIssues->fetchAll(PDO::FETCH_ASSOC) as $issueRow) {
        $qid = (int)$issueRow['quote_id'];
        $recoverableDocumentId = trim((string)($issueRow['facto_document_id'] ?? ''));
        if ($recoverableDocumentId === '' && !empty($issueRow['response_payload'])) {
            $issueResponse = json_decode((string)$issueRow['response_payload'], true);
            if (is_array($issueResponse)) {
                $recoverableDocumentId = trim((string)(
                    ($issueResponse['document_id'] ?? null)
                    ?: ($issueResponse['header']['document_id'] ?? null)
                    ?: ($issueResponse['document']['document_id'] ?? '')
                ));
            }
        }
        $issueRow['_recoverable_document_id'] = $recoverableDocumentId;
        if (($issueRow['mode'] ?? '') !== 'issue' && $recoverableDocumentId === '') {
            continue;
        }
        if ($issueRow['status'] === 'error' && $recoverableDocumentId === '') {
            continue;
        }
        if (!isset($billingIssueByQuote[$qid])) {
            $billingIssueByQuote[$qid] = $issueRow;
        }
    }
}

$billingDraftByQuote = array();

if (!empty($quoteIds) && q_table_exists($pdo, 'billing_issue_logs')) {
    $ph = implode(',', array_fill(0, count($quoteIds), '?'));
    $stmtBillingDrafts = $pdo->prepare("
        SELECT id, quote_id, document_type_key, environment, mode, status, facto_document_id, document_number, pdf_file_path, xml_file_path, request_payload, error_message, created_at, updated_at
        FROM billing_issue_logs
        WHERE quote_id IN ($ph)
          AND mode IN ('dry_run', 'draft_preview')
          AND status IN ('draft', 'validated', 'error', 'blocked')
        ORDER BY updated_at DESC, id DESC
    ");
    $stmtBillingDrafts->execute($quoteIds);

    foreach ($stmtBillingDrafts->fetchAll(PDO::FETCH_ASSOC) as $draftRow) {
        $qid = (int)$draftRow['quote_id'];
        if (!isset($billingDraftByQuote[$qid])) {
            $meta = array();
            if (!empty($draftRow['request_payload'])) {
                $decodedDraft = json_decode($draftRow['request_payload'], true);
                if (is_array($decodedDraft) && !empty($decodedDraft['_icylab_draft']) && is_array($decodedDraft['_icylab_draft'])) {
                    $meta = $decodedDraft['_icylab_draft'];
                } elseif (is_array($decodedDraft)) {
                    if (!empty($decodedDraft['header']['issue_date'])) $meta['issue_date'] = $decodedDraft['header']['issue_date'];
                    if (!empty($decodedDraft['header']['payment_conditions'])) $meta['payment_conditions'] = $decodedDraft['header']['payment_conditions'];
                }
            }
            $draftRow['_meta'] = $meta;
            $billingDraftByQuote[$qid] = $draftRow;
        }
    }
}

$billingRecoveryByQuote = array();
if (!empty($quoteIds) && q_table_exists($pdo, 'facto_documents')) {
    $ph = implode(',', array_fill(0, count($quoteIds), '?'));
    $recoveryStmt = $pdo->prepare("
        SELECT q.id AS quote_id, d.document_id
        FROM crm_quotes q
        INNER JOIN crm_customers c ON c.id=q.customer_id
        INNER JOIN facto_documents d
            ON d.environment=?
           AND d.received_issued_flag=1
           AND d.document_status IN (1,2,3)
           AND d.document_type_id IN (2,32,46)
           AND COALESCE(d.payment_status,'')<>'anulado'
           AND (d.crm_customer_id=q.customer_id OR REPLACE(REPLACE(REPLACE(UPPER(d.receiver_tax_id_code),'.',''),'-',''),' ','')=REPLACE(REPLACE(REPLACE(UPPER(c.customer_tax_id),'.',''),'-',''),' ',''))
           AND ABS(COALESCE(d.net_amount_clp,d.net_amount)-((q.subtotal)-COALESCE(q.discount_amount,0)))<=1
           AND ABS(COALESCE(d.taxes_amount_clp,d.taxes_amount)-COALESCE(q.iva,0))<=1
           AND ABS(COALESCE(d.total_amount_clp,d.total_amount)-q.total)<=1
           AND d.issue_date>=DATE(q.created_at)
           AND (d.crm_quote_id IS NULL OR d.crm_quote_id=q.id)
        WHERE q.id IN ($ph)
          AND q.status IN ('Aceptada','Facturada','Esperando pago','Pagada')
          AND (q.facto_document_id IS NULL OR q.facto_document_id='' OR BINARY q.facto_document_id=BINARY d.document_id)
        ORDER BY q.id,d.id DESC
    ");
    $recoveryStmt->execute(array_merge(array($billingConfiguredEnvironment), $quoteIds));
    $candidatesByQuote = array();
    $candidateQuoteCount = array();
    foreach ($recoveryStmt->fetchAll(PDO::FETCH_ASSOC) as $candidate) {
        $candidateQuoteId = (int)$candidate['quote_id'];
        $candidateDocumentId = trim((string)$candidate['document_id']);
        if ($candidateQuoteId <= 0 || $candidateDocumentId === '') continue;
        if (!isset($candidatesByQuote[$candidateQuoteId])) $candidatesByQuote[$candidateQuoteId] = array();
        $candidatesByQuote[$candidateQuoteId][$candidateDocumentId] = true;
        if (!isset($candidateQuoteCount[$candidateDocumentId])) $candidateQuoteCount[$candidateDocumentId] = array();
        $candidateQuoteCount[$candidateDocumentId][$candidateQuoteId] = true;
    }
    foreach ($candidatesByQuote as $candidateQuoteId => $documents) {
        $documentIds = array_keys($documents);
        if (count($documentIds) !== 1) continue;
        $candidateDocumentId = $documentIds[0];
        if (count($candidateQuoteCount[$candidateDocumentId] ?? array()) !== 1) continue;
        $billingRecoveryByQuote[$candidateQuoteId] = $candidateDocumentId;
    }
}


$billingPaymentMethodsPayload = array();
if (q_table_exists($pdo, 'billing_payment_methods')) {
    $stmtPayMethods = $pdo->query("SELECT id, method_key, method_name, method_type, is_default, sort_order FROM billing_payment_methods WHERE is_active = 1 ORDER BY sort_order ASC, method_name ASC");
    if ($stmtPayMethods) {
        $billingPaymentMethodsPayload = $stmtPayMethods->fetchAll(PDO::FETCH_ASSOC);
    }
}
if (empty($billingPaymentMethodsPayload)) {
    $billingPaymentMethodsPayload = array(
        array('id' => 0, 'method_key' => 'transferencia', 'method_name' => 'Transferencia bancaria', 'method_type' => 'bank_transfer', 'is_default' => 1, 'sort_order' => 10),
        array('id' => 0, 'method_key' => 'efectivo', 'method_name' => 'Efectivo', 'method_type' => 'cash', 'is_default' => 0, 'sort_order' => 20),
        array('id' => 0, 'method_key' => 'tarjeta_debito', 'method_name' => 'Tarjeta de débito', 'method_type' => 'card', 'is_default' => 0, 'sort_order' => 30),
        array('id' => 0, 'method_key' => 'tarjeta_credito', 'method_name' => 'Tarjeta de crédito', 'method_type' => 'card', 'is_default' => 0, 'sort_order' => 40),
        array('id' => 0, 'method_key' => 'webpay', 'method_name' => 'Webpay / pasarela de pago', 'method_type' => 'gateway', 'is_default' => 0, 'sort_order' => 50),
        array('id' => 0, 'method_key' => 'cheque', 'method_name' => 'Cheque', 'method_type' => 'check', 'is_default' => 0, 'sort_order' => 60),
        array('id' => 0, 'method_key' => 'otro', 'method_name' => 'Otro', 'method_type' => 'other', 'is_default' => 0, 'sort_order' => 90)
    );
}

$billingCashAccountsPayload = array();
if (q_table_exists($pdo, 'billing_cash_accounts')) {
    $stmtCashAccounts = $pdo->query("SELECT id, account_key, account_name, account_type, currency_code, bank_name, account_number, is_default, sort_order FROM billing_cash_accounts WHERE is_active = 1 ORDER BY sort_order ASC, account_name ASC");
    if ($stmtCashAccounts) {
        $billingCashAccountsPayload = $stmtCashAccounts->fetchAll(PDO::FETCH_ASSOC);
    }
}
if (empty($billingCashAccountsPayload)) {
    $billingCashAccountsPayload = array(
        array('id' => 0, 'account_key' => 'banco_cuenta_corriente', 'account_name' => 'Banco / cuenta corriente', 'account_type' => 'bank', 'currency_code' => 'CLP', 'bank_name' => null, 'account_number' => null, 'is_default' => 1, 'sort_order' => 10),
        array('id' => 0, 'account_key' => 'caja_efectivo', 'account_name' => 'Caja efectivo', 'account_type' => 'cash', 'currency_code' => 'CLP', 'bank_name' => null, 'account_number' => null, 'is_default' => 0, 'sort_order' => 20),
        array('id' => 0, 'account_key' => 'pasarela_webpay', 'account_name' => 'Pasarela Webpay', 'account_type' => 'gateway', 'currency_code' => 'CLP', 'bank_name' => null, 'account_number' => null, 'is_default' => 0, 'sort_order' => 30),
        array('id' => 0, 'account_key' => 'otra_cuenta', 'account_name' => 'Otra cuenta/caja', 'account_type' => 'other', 'currency_code' => 'CLP', 'bank_name' => null, 'account_number' => null, 'is_default' => 0, 'sort_order' => 90)
    );
}


$billingCountriesPayload = array();
if (q_table_exists($pdo, 'billing_countries')) {
    $stmtCountries = $pdo->query("SELECT country_name, facto_country_id FROM billing_countries WHERE is_active=1 ORDER BY country_name ASC");
    if ($stmtCountries) $billingCountriesPayload = $stmtCountries->fetchAll(PDO::FETCH_ASSOC);
}
$billingCurrenciesPayload = array();
if (q_table_exists($pdo, 'billing_currencies')) {
    $stmtCurrencies = $pdo->query("SELECT currency_code, currency_name, facto_currency_id FROM billing_currencies WHERE is_active=1 ORDER BY currency_code ASC");
    if ($stmtCurrencies) $billingCurrenciesPayload = $stmtCurrencies->fetchAll(PDO::FETCH_ASSOC);
}

$stmtOpps = $pdo->query("
    SELECT
        o.id,
        o.customer_id,
        o.title,
        o.customer_name AS opportunity_customer_name,
        o.stage,
        c.customer_name,
        c.commercial_name,
        c.legal_name,
        c.customer_tax_id,
        c.email,
        c.phone,
        c.address,
        c.district,
        c.city
    FROM crm_opportunities o
    JOIN crm_customers c ON c.id = o.customer_id
    WHERE o.deleted_at IS NULL
      AND c.deleted_at IS NULL
    ORDER BY o.updated_at DESC, o.created_at DESC
    LIMIT 400
");
$opportunities = $stmtOpps->fetchAll(PDO::FETCH_ASSOC);

$quotesPayload = array();

foreach ($quotes as $q) {
    $qid = (int)$q['id'];
    $quotesPayload[] = array(
        'id' => $qid,
        'opportunity_id' => (int)($q['opportunity_id'] ?? 0),
        'customer_id' => (int)$q['customer_id'],
        'quote_number' => $q['quote_number'],
        'subtotal' => (float)$q['subtotal'],
        'discount_percent' => (float)$q['discount_percent'],
        'discount_amount' => (float)$q['discount_amount'],
        'iva' => (float)$q['iva'],
        'total' => (float)$q['total'],
        'notes' => $q['notes'] ?? '',
        'status' => $q['status'],
        'invoice_issue_date' => q_date($q['invoice_issue_date'] ?? null),
        'invoice_issue_date_raw' => $q['invoice_issue_date'] ?? '',
        'invoice_number' => $q['invoice_number'] ?? '',
        'facto_document_id' => $q['facto_document_id'] ?? '',
        'billing_issue_log_id' => !empty($billingIssueByQuote[$qid]['id']) ? (int)$billingIssueByQuote[$qid]['id'] : 0,
        'billing_pdf_url' => !empty($billingIssueByQuote[$qid]['pdf_file_path']) ? '/modules/billing/preview_pdf.php?log_id=' . (int)$billingIssueByQuote[$qid]['id'] : '',
        'billing_xml_available' => !empty($billingIssueByQuote[$qid]['xml_file_path']),
        'billing_issue_environment' => $billingIssueByQuote[$qid]['environment'] ?? '',
        'billing_issue_status' => $billingIssueByQuote[$qid]['status'] ?? '',
        'billing_recovery_document_id' => ((in_array(($billingIssueByQuote[$qid]['status'] ?? ''), array('error', 'pending_reconcile'), true) || ($billingIssueByQuote[$qid]['mode'] ?? '') === 'draft_preview') ? ($billingIssueByQuote[$qid]['_recoverable_document_id'] ?? '') : '') ?: ($billingRecoveryByQuote[$qid] ?? ''),
        'billing_recovery_message' => ($billingIssueByQuote[$qid]['error_message'] ?? '') ?: (isset($billingRecoveryByQuote[$qid]) ? 'Se encontro un unico DTE emitido que coincide por cliente, fecha y montos. Verificalo y concilialo; no vuelvas a emitir.' : ''),
        'billing_draft_log_id' => !empty($billingDraftByQuote[$qid]['id']) ? (int)$billingDraftByQuote[$qid]['id'] : 0,
        'billing_draft_document_key' => $billingDraftByQuote[$qid]['document_type_key'] ?? '',
        'billing_draft_environment' => $billingDraftByQuote[$qid]['environment'] ?? '',
        'billing_draft_mode' => $billingDraftByQuote[$qid]['mode'] ?? '',
        'billing_draft_status' => $billingDraftByQuote[$qid]['status'] ?? '',
        'billing_draft_document_number' => $billingDraftByQuote[$qid]['document_number'] ?? '',
        'billing_draft_facto_document_id' => $billingDraftByQuote[$qid]['facto_document_id'] ?? '',
        'billing_draft_pdf_url' => !empty($billingDraftByQuote[$qid]['pdf_file_path']) ? '/modules/billing/preview_pdf.php?log_id=' . (int)$billingDraftByQuote[$qid]['id'] : '',
        'billing_draft_has_xml' => !empty($billingDraftByQuote[$qid]['xml_file_path']),
        'billing_draft_updated_at' => !empty($billingDraftByQuote[$qid]['updated_at']) ? q_date($billingDraftByQuote[$qid]['updated_at']) : '',
        'billing_draft_issue_date' => $billingDraftByQuote[$qid]['_meta']['issue_date'] ?? '',
        'billing_draft_payment_terms' => $billingDraftByQuote[$qid]['_meta']['payment_terms_key'] ?? '',
        'billing_draft_error_message' => $billingDraftByQuote[$qid]['error_message'] ?? '',
        'payment_date' => q_date($q['payment_date'] ?? null),
        'payment_date_raw' => $q['payment_date'] ?? '',
        'created_at' => q_date($q['created_at']),
        'created_at_raw' => $q['created_at'],
        'customer_name' => q_legal_customer($q),
        'customer_display_name' => q_display_customer($q),
        'customer_legal_name' => q_legal_customer($q),
        'customer_commercial_name' => $q['commercial_name'] ?? '',
        'customer_base_name' => $q['customer_name'] ?? '',
        'customer_tax_id' => $q['customer_tax_id'] ?? '',
        'export_tax_id_code' => $q['export_tax_id_code'] ?? '',
        'export_tax_id_type' => $q['export_tax_id_type'] ?? '',
        'tax_id_type' => $q['tax_id_type'] ?? 'CL-RUT',
        'email' => $q['email_dte'] ?: ($q['email'] ?? ''),
        'phone' => $q['phone'] ?? '',
        'whatsapp_phone' => q_phone($q['phone'] ?? ''),
        'address' => $q['address'] ?? '',
        'district' => $q['district'] ?? '',
        'city' => $q['city'] ?? '',
        'country' => $q['country'] ?? '',
        'country_id' => $q['country_id'] ?? null,
        'export_country_id' => $q['export_country_id'] ?? null,
        'business_activity' => $q['business_activity'] ?? '',
        'opportunity_title' => !empty($q['opportunity_title']) ? $q['opportunity_title'] : 'Venta directa',
        'deleted_at' => $q['deleted_at'] ?? '',
        'deleted_by' => $q['deleted_by'] ?? '',
        'deleted_reason' => $q['deleted_reason'] ?? '',
        'is_deleted' => !empty($q['deleted_at']),
        'search_text' => mb_strtolower(trim(implode(' ', array_filter(array(
            $q['quote_number'] ?? '',
            q_display_customer($q),
            $q['customer_name'] ?? '',
            $q['commercial_name'] ?? '',
            $q['legal_name'] ?? '',
            $q['customer_tax_id'] ?? '',
            $q['email'] ?? '',
            $q['phone'] ?? '',
            !empty($q['opportunity_title']) ? $q['opportunity_title'] : 'Venta directa',
            $q['status'] ?? '',
            $q['invoice_number'] ?? ''
        )))), 'UTF-8'),
        'items' => $itemsByQuote[$qid] ?? array()
    );
}

$opportunitiesPayload = array();

foreach ($opportunities as $op) {
    $opportunitiesPayload[] = array(
        'id' => (int)$op['id'],
        'customer_id' => (int)$op['customer_id'],
        'title' => $op['title'],
        'stage' => $op['stage'],
        'customer' => array(
            'id' => (int)$op['customer_id'],
            'customer_name' => $op['customer_name'],
            'commercial_name' => $op['commercial_name'] ?? '',
            'legal_name' => $op['legal_name'] ?? '',
            'display_name' => q_display_customer($op),
            'customer_tax_id' => $op['customer_tax_id'] ?? '',
            'email' => $op['email'] ?? '',
            'phone' => $op['phone'] ?? '',
            'address' => $op['address'] ?? '',
            'district' => $op['district'] ?? '',
            'city' => $op['city'] ?? ''
        )
    );
}

$totalQuotes = count($quotes);
$totalAmount = 0;
$acceptedAmount = 0;
$sentAmount = 0;

foreach ($quotes as $q) {
    $totalAmount += (float)$q['total'];
    if ($q['status'] === 'Aceptada') $acceptedAmount += (float)$q['total'];
    if ($q['status'] === 'Enviada') $sentAmount += (float)$q['total'];
}

$sessionUser = current_user();
$currentUserName = !empty($sessionUser['name']) ? $sessionUser['name'] : (!empty($sessionUser['email']) ? $sessionUser['email'] : 'Sistema');

require_once '../../layouts/header.php';
require_once '../../layouts/sidebar.php';
?>

<main class="app-main crm-ui-main <?php echo $showDeletedQuotes ? 'quotes-deleted-view' : ''; ?>" id="quotesMain" data-module="quotes">
    <?php require_once '../../layouts/topbar.php'; ?>

    <section class="content quotes-page-shell crm-ui-page-content crm-ui-page">
        <div class="page-header header-responsive crm-ui-page-header">
            <div class="page-header-content crm-ui-page-header-copy">
                <span class="label">CRM</span>
                <h2>Cotizaciones</h2>
                <p class="text-muted"><?php echo $showDeletedQuotes ? 'Papelera lógica de cotizaciones eliminadas. Desde aquí puedes revisar y restaurar documentos.' : 'Cotizador comercial con flujo nativo de creación, edición y vista previa.'; ?></p>
            </div>

            <div class="cluster top-actions-cluster crm-ui-page-actions">
                <a href="/modules/crm/" class="btn btn-outline btn-icon-mobile quote-header-crm" title="Volver a CRM">
                    <i data-lucide="arrow-left"></i>
                    <span class="mobile-hidden">CRM</span>
                </a>
            
                <a href="/modules/crm/embudo.php" class="btn btn-outline btn-icon-mobile quote-header-embudo" title="Ir a Embudo">
                    <i data-lucide="kanban-square"></i>
                    <span class="mobile-hidden">Embudo</span>
                </a>

                <?php if ($hasQuoteDeletedAt): ?>
                    <?php if ($showDeletedQuotes): ?>
                        <a href="/modules/crm/quotes.php" class="btn btn-outline btn-icon-mobile" title="Volver a cotizaciones activas" data-no-spa="true">
                            <i data-lucide="list"></i>
                            <span class="mobile-hidden">Activas</span>
                        </a>
                    <?php else: ?>
                        <a href="/modules/crm/quotes.php?view=deleted" class="btn btn-outline btn-icon-mobile" title="Ver cotizaciones eliminadas" data-no-spa="true">
                            <i data-lucide="archive-restore"></i>
                            <span class="mobile-hidden">Eliminadas</span>
                        </a>
                    <?php endif; ?>
                <?php endif; ?>
            
                <?php if (!$showDeletedQuotes): ?>
                <button type="button" class="btn btn-primary quote-open-create quote-header-create btn-icon-mobile" title="Nueva cotización">
                    <i data-lucide="plus"></i>
                    <span class="mobile-hidden">Nueva cotización</span>
                </button>
                <?php endif; ?>
            </div>
        </div>

        <section class="filter-bar-container quotes-filter-bar crm-ui-filter">
            <input id="quoteSearch" type="search" class="input" placeholder="Buscar folio, cliente, RUT, nombre comercial o razón social" autocomplete="off">

            <select id="quoteStatusFilter" class="input quotes-status-select">
                <option value="">Todos</option>
                <?php foreach ($allowedStatuses as $status): ?>
                    <option value="<?php echo safe_text($status); ?>"><?php echo safe_text($status); ?></option>
                <?php endforeach; ?>
            </select>

            <div class="quote-filter-date-group crm-ui-date-group" aria-label="Filtrar por fecha de creación de cotización">
                <label class="quote-date-filter-field crm-ui-date-field">
                    <span>Desde</span>
                    <input id="quoteCreatedFrom" type="date" class="input" title="Cotización creada desde" aria-label="Cotización creada desde">
                    <em>dd/mm/aaaa</em>
                </label>

                <label class="quote-date-filter-field crm-ui-date-field">
                    <span>Hasta</span>
                    <input id="quoteCreatedTo" type="date" class="input" title="Cotización creada hasta" aria-label="Cotización creada hasta">
                    <em>dd/mm/aaaa</em>
                </label>
            </div>
        </section>

        <section class="grid grid-4 responsive-kpis quotes-kpis-row crm-ui-kpis" style="--crm-kpi-columns: 4;">
            <article class="kpi-card">
                <div class="kpi-label">Cotizaciones</div>
                <div class="kpi-main"><div class="kpi-value" id="kpiCount"><?php echo (int)$totalQuotes; ?></div></div>
            </article>

            <article class="kpi-card">
                <div class="kpi-label">Monto total</div>
                <div class="kpi-main"><div class="kpi-value" id="kpiTotal"><?php echo clp($totalAmount); ?></div></div>
            </article>

            <article class="kpi-card">
                <div class="kpi-label">Aceptadas</div>
                <div class="kpi-main"><div class="kpi-value" id="kpiAccepted"><?php echo clp($acceptedAmount); ?></div></div>
            </article>

            <article class="kpi-card">
                <div class="kpi-label">Enviadas</div>
                <div class="kpi-main"><div class="kpi-value" id="kpiSent"><?php echo clp($sentAmount); ?></div></div>
            </article>
        </section>

        <section class="card quotes-table-card crm-ui-table-card">
            <div class="table-wrap quotes-table-wrap crm-ui-table-scroll">
                <table class="table quotes-table crm-ui-table crm-ui-mobile-table icy-sortable-table" style="--crm-mobile-columns: 82px minmax(0, 1fr) 104px 92px;">
                    <colgroup>
                        <col class="col-folio">
                        <col class="col-client">
                        <col class="col-origin">
                        <col class="col-status">
                        <col class="col-total">
                        <col class="col-date">
                        <col class="col-action">
                    </colgroup>

                    <thead>
                        <tr>
                            <th class="crm-mobile-primary" aria-sort="none"><button type="button" class="icy-table-sort-button" data-sort-type="text"><span>Folio</span><span class="icy-table-sort-indicator" aria-hidden="true"></span></button></th>
                            <th class="crm-mobile-primary" aria-sort="none"><button type="button" class="icy-table-sort-button" data-sort-type="text"><span>Cliente</span><span class="icy-table-sort-indicator" aria-hidden="true"></span></button></th>
                            <th class="crm-mobile-hidden" aria-sort="none"><button type="button" class="icy-table-sort-button" data-sort-type="text"><span>Origen</span><span class="icy-table-sort-indicator" aria-hidden="true"></span></button></th>
                            <th aria-sort="none"><button type="button" class="icy-table-sort-button" data-sort-type="text"><span>Estado</span><span class="icy-table-sort-indicator" aria-hidden="true"></span></button></th>
                            <th class="crm-mobile-hidden" aria-sort="none"><button type="button" class="icy-table-sort-button" data-sort-type="number"><span>Total</span><span class="icy-table-sort-indicator" aria-hidden="true"></span></button></th>
                            <th class="crm-mobile-hidden" aria-sort="none"><button type="button" class="icy-table-sort-button" data-sort-type="date"><span>Fecha</span><span class="icy-table-sort-indicator" aria-hidden="true"></span></button></th>
                            <th class="text-center crm-ui-action-cell">Acción</th>
                        </tr>
                    </thead>

                    <tbody id="quotesTableBody">
                        <?php if (empty($quotes)): ?>
                            <tr class="empty-state-row">
                                <td colspan="7" class="text-muted"><?php echo $showDeletedQuotes ? 'No hay cotizaciones eliminadas.' : 'No hay cotizaciones registradas.'; ?></td>
                            </tr>
                        <?php else: ?>
                            <?php foreach ($quotes as $q): ?>
                                <?php
                                    $origin = !empty($q['opportunity_title']) ? $q['opportunity_title'] : 'Venta directa';
                                    $searchData = mb_strtolower(trim(implode(' ', array_filter(array(
                                        $q['quote_number'] ?? '',
                                        q_display_customer($q),
                                        $q['customer_name'] ?? '',
                                        $q['commercial_name'] ?? '',
                                        $q['legal_name'] ?? '',
                                        $q['customer_tax_id'] ?? '',
                                        $q['email'] ?? '',
                                        $q['phone'] ?? '',
                                        $origin,
                                        $q['status'] ?? '',
                                        $q['invoice_number'] ?? ''
                                    )))), 'UTF-8');
                                    $createdDateRaw = !empty($q['created_at']) ? substr((string)$q['created_at'], 0, 10) : '';
                                ?>
                                <tr class="quote-row <?php echo !empty($q['deleted_at']) ? 'quote-row-deleted' : ''; ?>" data-quote-id="<?php echo (int)$q['id']; ?>" data-search="<?php echo safe_text($searchData); ?>" data-status="<?php echo safe_text($q['status']); ?>" data-created-date="<?php echo safe_text($createdDateRaw); ?>" data-deleted="<?php echo !empty($q['deleted_at']) ? '1' : '0'; ?>">
                                    <td data-label="Folio" class="crm-mobile-primary" data-sort-value="<?php echo safe_text($q['quote_number']); ?>">
                                        <button type="button" class="quote-link quote-open-preview" data-quote-id="<?php echo (int)$q['id']; ?>" title="<?php echo safe_text($q['quote_number']); ?>">
                                            <span class="quote-folio-full"><?php echo safe_text($q['quote_number']); ?></span>
                                            <span class="quote-folio-mobile"><?php echo safe_text(q_compact_quote_number($q['quote_number'])); ?></span>
                                        </button>
                                    </td>
                                    <td data-label="Cliente" class="crm-mobile-primary" data-sort-value="<?php echo safe_text(q_display_customer($q)); ?>">
                                        <div class="quote-client-cell">
                                            <strong><?php echo safe_text(q_display_customer($q)); ?></strong>
                                            <span><?php echo safe_text($q['customer_tax_id'] ?? ''); ?></span>
                                        </div>
                                    </td>
                                    <td data-label="Origen" class="crm-mobile-hidden" data-sort-value="<?php echo safe_text($origin); ?>">
                                        <div class="quote-origin-cell">
                                            <span><?php echo safe_text($origin); ?></span>
                                        </div>
                                    </td>
                                    <td data-label="Estado" data-sort-value="<?php echo safe_text($q['status']); ?>">
                                        <select class="quote-status-quick <?php echo q_badge($q['status']); ?>" data-quote-id="<?php echo (int)$q['id']; ?>" data-current="<?php echo safe_text($q['status']); ?>" aria-label="Actualizar estado de cotización" <?php echo (!empty($q['deleted_at']) || in_array($q['status'], $systemManagedStatuses, true)) ? 'disabled' : ''; ?>>
                                            <?php echo q_status_options($allowedStatuses, $q['status'], $systemManagedStatuses); ?>
                                        </select>
                                    </td>
                                    <td data-label="Total" class="crm-mobile-hidden" data-sort-value="<?php echo safe_text((string)(float)$q['total']); ?>">
                                        <strong class="metric-value"><?php echo clp($q['total']); ?></strong>
                                    </td>
                                    <td data-label="Fecha" class="crm-mobile-hidden" data-sort-value="<?php echo safe_text($createdDateRaw); ?>"><?php echo safe_text(q_date($q['created_at'])); ?></td>
                                    <td data-label="Acción" class="actions-cell-align crm-ui-action-cell">
                                        <div class="actions-btn-group">
                                            <button type="button" class="btn btn-sm btn-outline action-icon-btn quote-open-preview crm-ui-action-btn" data-quote-id="<?php echo (int)$q['id']; ?>" title="Ver cotización">
                                                <i data-lucide="eye"></i>
                                            </button>
                                            <?php if (!empty($q['deleted_at'])): ?>
                                                <form method="POST" action="/modules/crm/quote_restore.php" data-no-spa="true" class="quote-restore-inline-form" style="margin:0;">
                                                    <input type="hidden" name="id" value="<?php echo (int)$q['id']; ?>">
                                                    <input type="hidden" name="redirect" value="/modules/crm/quotes.php?view=deleted">
                                                    <button type="submit" class="btn btn-sm btn-outline action-icon-btn quote-restore-btn" title="Restaurar cotización">
                                                        <i data-lucide="rotate-ccw"></i>
                                                    </button>
                                                </form>
                                            <?php endif; ?>
                                        </div>
                                    </td>
                                </tr>
                            <?php endforeach; ?>
                        <?php endif; ?>
                    </tbody>
                </table>
            </div>
        </section>
    </section>

    <div id="quotesModalBackdrop" class="product-modal-backdrop crm-ui-modal-backdrop" aria-hidden="true">
        <div class="product-modal-box quotes-modal-box crm-ui-modal-box">

            <div id="quoteLayerPreview" class="quote-view-layer crm-ui-modal-layer active">
                <div class="product-modal-header quote-modal-header crm-ui-modal-header">
                    <button type="button" class="btn btn-outline action-square-btn quote-modal-close crm-ui-modal-icon-btn" title="Volver">
                        <i data-lucide="arrow-left"></i>
                    </button>

                    <div class="quote-modal-title crm-ui-modal-heading">
                        <h2 class="h3">Detalle de Cotización</h2>
                        <span class="text-secondary text-sm">Visualiza y comparte el documento</span>
                    </div>

                    <button type="button" class="btn btn-outline action-square-btn quote-modal-close crm-ui-modal-icon-btn" title="Cerrar">
                        <i data-lucide="x"></i>
                    </button>
                </div>

                <div id="quoteSaveNotice" class="quote-save-notice hidden"></div>

                <div class="product-modal-body quote-document-body crm-ui-modal-body">
                    <div id="quotePreviewDocument"></div>
                </div>

                <div class="product-modal-footer quote-modal-footer crm-ui-modal-footer">
                    <div class="modal-footer-actions-left"></div>

                    <div class="modal-footer-actions-right">
                        <a href="#" id="quotePdfBtn" class="btn btn-outline" download>
                            <i data-lucide="download"></i>
                            <span>Descargar PDF</span>
                        </a>

                        <a href="#" id="quoteWhatsAppBtn" target="_blank" rel="noopener" class="btn btn-outline quote-wa-btn">
                            <i data-lucide="message-circle"></i>
                            <span>WhatsApp</span>
                        </a>

                        <a href="#" id="quoteMailBtn" class="btn btn-outline">
                            <i data-lucide="mail"></i>
                            <span>Email</span>
                        </a>

                        <button type="button" class="btn btn-outline quote-open-edit">
                            <i data-lucide="pencil"></i>
                            <span>Editar</span>
                        </button>

                        <button type="button" id="quoteDispatchGuideBtn" class="btn btn-outline hidden">
                            <i data-lucide="truck"></i>
                            <span>Guía de despacho</span>
                        </button>

                        <button type="button" id="quoteBillingBtn" class="btn btn-primary quote-billing-btn hidden">
                            <i data-lucide="file-text"></i>
                            <span>Facturar</span>
                        </button>
                    </div>
                </div>
            </div>

            <div id="quoteLayerForm" class="quote-view-layer crm-ui-modal-layer quote-opportunity-skin">
                <div class="product-modal-header quote-modal-header crm-ui-modal-header">
                    <button type="button" class="btn btn-outline action-square-btn quote-form-back crm-ui-modal-icon-btn" title="Volver">
                        <i data-lucide="arrow-left"></i>
                    </button>

                    <div class="quote-modal-title crm-ui-modal-heading">
                        <h2 class="h3" id="quoteFormTitle">Nueva Cotización</h2>
                        <span class="text-secondary text-sm" id="quoteFormSubtitle">Completa cliente, productos y condiciones</span>
                    </div>

                    <button type="button" class="btn btn-outline action-square-btn quote-modal-close crm-ui-modal-icon-btn" title="Cerrar">
                        <i data-lucide="x"></i>
                    </button>
                </div>

                <form id="quoteForm" autocomplete="off" data-no-spa="true">
                    <input type="hidden" name="quote_id" id="formQuoteId" value="0">
                    <input type="hidden" name="customer_id" id="formCustomerId" value="0">
                    <input type="hidden" name="opportunity_id" id="formOpportunityId" value="0">

                    <div class="product-modal-body crm-ui-modal-body">
                        <div class="quote-form-grid-main">
                            <div class="form-field quote-search-field" id="customerSelectorBlock">
                                <span class="label">Cliente</span>

                                <div class="quote-inline-search">
                                    <input type="search" class="input" id="customerSearch" placeholder="Buscar cliente por nombre o RUT" autocomplete="off">

                                    <button type="button" class="btn btn-outline btn-icon-mobile quote-open-customer-create" title="Crear cliente">
                                        <i data-lucide="user-plus"></i>
                                        <span class="mobile-hidden">Crear cliente</span>
                                    </button>
                                </div>

                                <div id="customerResults" class="search-results-box"></div>
                                <div id="selectedCustomerChip" class="selected-chip hidden"></div>
                            </div>

                            <label class="form-field">
                                <span class="label">Oportunidad</span>
                                <select class="select" id="opportunitySelect">
                                    <option value="0" data-customer-id="0">Venta directa</option>
                                    <?php foreach ($opportunities as $op): ?>
                                        <option value="<?php echo (int)$op['id']; ?>" data-customer-id="<?php echo (int)$op['customer_id']; ?>">
                                            <?php echo safe_text(($op['customer_name'] ?: $op['opportunity_customer_name']) . ' · ' . $op['title']); ?>
                                        </option>
                                    <?php endforeach; ?>
                                </select>
                            </label>

                            <label class="form-field">
                                <span class="label">Estado</span>
                                <select class="select" name="status" id="formStatus">
                                    <?php foreach ($editableQuoteStatuses as $status): ?>
                                        <option value="<?php echo safe_text($status); ?>"><?php echo safe_text($status); ?></option>
                                    <?php endforeach; ?>
                                </select>
                            </label>

                            <label class="form-field">
                                <span class="label">Descuento %</span>
                                <input class="input" type="number" min="0" max="100" step="0.1" name="discount_percent" id="formDiscount" value="0">
                            </label>
                        </div>

                        <div class="form-field quote-search-field mt-4">
                            <span class="label">Productos</span>

                            <div class="quote-inline-search">
                                <input type="search" class="input" id="productSearch" placeholder="Buscar producto por nombre o SKU" autocomplete="off">

                                <button type="button" class="btn btn-outline btn-icon-mobile quote-add-manual" title="Agregar producto manual">
                                    <i data-lucide="plus"></i>
                                    <span class="mobile-hidden">Manual</span>
                                </button>
                            </div>

                            <div id="productResults" class="search-results-box"></div>
                        </div>

                        <div class="quote-builder-head mobile-hidden">
                            <div>Producto</div>
                            <div>SKU</div>
                            <div>Cant.</div>
                            <div>Unidad</div>
                            <div>Precio U. Neto</div>
                            <div>Total fila</div>
                            <div></div>
                        </div>

                        <div id="quoteItems" class="quote-items-box mt-2"></div>

                        <div class="quote-form-bottom mt-4">
                            <label class="form-field">
                                <span class="label">Condiciones comerciales</span>
                                <textarea class="textarea" name="notes" id="formNotes" rows="4" placeholder="Sin condiciones comerciales adicionales especificadas."></textarea>
                            </label>

                            <div class="quote-summary-card">
                                <div><span>Subtotal Bruto:</span><strong id="sumSubtotal">$0</strong></div>
                                <div><span>Monto Neto:</span><strong id="sumNet">$0</strong></div>
                                <div><span>IVA (19%):</span><strong id="sumIva">$0</strong></div>
                                <hr>
                                <div class="total"><span>Total General:</span><strong id="sumTotal">$0</strong></div>
                            </div>
                        </div>
                    </div>

                    <div class="product-modal-footer quote-modal-footer crm-ui-modal-footer">
                        <div class="modal-footer-actions-left">
                            <button type="button" class="btn btn-outline quote-danger-btn quote-delete-btn hidden" id="quoteFormDeleteBtn" title="Eliminar cotización" aria-label="Eliminar cotización">
                                <i data-lucide="trash-2"></i>
                                <span>Eliminar</span>
                            </button>
                        </div>

                        <div class="modal-footer-actions-right">
                            <button type="button" class="btn btn-outline quote-form-back" id="quoteFormCancelBtn" title="Cancelar" aria-label="Cancelar edición">
                                <i data-lucide="x"></i>
                                <span>Cancelar</span>
                            </button>

                            <button type="submit" class="btn btn-primary" id="quoteSaveBtn">
                                <i data-lucide="save"></i>
                                <span>Guardar <span class="mobile-hidden">cotización</span></span>
                            </button>
                        </div>
                    </div>
                </form>
            </div>

            <div id="quoteLayerCustomer" class="quote-view-layer crm-ui-modal-layer">
                <div class="product-modal-header quote-modal-header crm-ui-modal-header">
                    <button type="button" class="btn btn-outline action-square-btn quote-back-form crm-ui-modal-icon-btn" title="Volver">
                        <i data-lucide="arrow-left"></i>
                    </button>

                    <div class="quote-modal-title crm-ui-modal-heading">
                        <h2 class="h3">Crear Cliente</h2>
                        <span class="text-secondary text-sm">Registro rápido para cotización</span>
                    </div>

                    <button type="button" class="btn btn-outline action-square-btn quote-modal-close crm-ui-modal-icon-btn" title="Cerrar">
                        <i data-lucide="x"></i>
                    </button>
                </div>

                <form id="customerQuickForm" autocomplete="off" data-no-spa="true">
                    <div class="product-modal-body crm-ui-modal-body">
                        <?php icylab_render_customer_quick_fields(array(
                            'name_field' => 'customer_name',
                            'tax_field' => 'customer_tax_id',
                            'tax_id' => 'quickCustomerTaxId',
                            'phone_prefix_id' => 'quickCustomerPhonePrefix',
                            'phone_number_id' => 'quickCustomerPhoneNumber',
                            'phone_hidden_id' => 'quickCustomerPhone',
                            'show_commercial_name' => true,
                            'values' => array('country' => 'Chile'),
                            'required' => array('customer_name', 'country', 'address', 'district', 'city', 'region')
                        )); ?>
                    </div>

                    <div class="product-modal-footer quote-modal-footer crm-ui-modal-footer">
                        <div class="modal-footer-actions-left"></div>
                        <div class="modal-footer-actions-right">
                            <button type="button" class="btn btn-outline quote-back-form" title="Cancelar" aria-label="Cancelar creación de cliente">
                                <i data-lucide="x"></i>
                                <span>Cancelar</span>
                            </button>
                            <button class="btn btn-primary" type="submit">
                                <i data-lucide="save"></i>
                                <span>Crear <span class="mobile-hidden">cliente</span></span>
                            </button>
                        </div>
                    </div>
                </form>
            </div>

            <div id="quoteLayerBilling" class="quote-view-layer crm-ui-modal-layer">
                <div class="product-modal-header quote-modal-header crm-ui-modal-header">
                    <button type="button" class="btn btn-outline action-square-btn quote-billing-back crm-ui-modal-icon-btn" title="Volver a cotización">
                        <i data-lucide="arrow-left"></i>
                    </button>

                    <div class="quote-modal-title crm-ui-modal-heading">
                        <h2 class="h3" id="quoteBillingTitle">Facturación tributaria</h2>
                        <span class="text-secondary text-sm" id="quoteBillingSubtitle">Emisión vinculada a cotización</span>
                    </div>

                    <button type="button" class="btn btn-outline action-square-btn quote-modal-close crm-ui-modal-icon-btn" title="Cerrar">
                        <i data-lucide="x"></i>
                    </button>
                </div>

                <div class="product-modal-body quote-billing-body crm-ui-modal-body">
                    <div id="quoteBillingContent" class="quote-billing-content"></div>
                </div>

                <div class="product-modal-footer quote-modal-footer quote-billing-footer crm-ui-modal-footer">
                    <div class="modal-footer-actions-left"></div>
                    <div class="modal-footer-actions-right" id="quoteBillingActions"></div>
                </div>
            </div>

            <div id="quoteLayerDispatchGuide" class="quote-view-layer crm-ui-modal-layer">
                <div class="product-modal-header quote-modal-header crm-ui-modal-header">
                    <button type="button" class="btn btn-outline action-square-btn quote-guide-back crm-ui-modal-icon-btn" title="Volver">
                        <i data-lucide="arrow-left"></i>
                    </button>

                    <div class="quote-modal-title crm-ui-modal-heading">
                        <h2 class="h3" id="quoteGuideTitle">Guía de despacho</h2>
                        <span class="text-secondary text-sm" id="quoteGuideSubtitle">Emisión vinculada a cotización</span>
                    </div>

                    <button type="button" class="btn btn-outline action-square-btn quote-modal-close crm-ui-modal-icon-btn" title="Cerrar">
                        <i data-lucide="x"></i>
                    </button>
                </div>

                <div class="product-modal-body quote-guide-body crm-ui-modal-body">
                    <div id="quoteGuideContent" class="quote-guide-content"></div>
                </div>

                <div class="product-modal-footer quote-modal-footer quote-guide-footer crm-ui-modal-footer">
                    <div class="modal-footer-actions-left"></div>
                    <div class="modal-footer-actions-right" id="quoteGuideActions"></div>
                </div>
            </div>

        </div>
    </div>

    <?php
    $sortJsPath = dirname(__DIR__, 2) . '/assets/js/icy-table-sort.js';
    $sortJsVersion = file_exists($sortJsPath) ? filemtime($sortJsPath) : time();
    ?>
    <script src="/assets/js/icy-table-sort.js?v=<?php echo (int)$sortJsVersion; ?>"></script>
    <script src="/assets/js/customer-location.js?v=p4_5_1"></script>
    <script id="quotesPayload" type="application/json"><?php echo json_encode($quotesPayload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); ?></script>
    <script id="quotesOpportunitiesPayload" type="application/json"><?php echo json_encode($opportunitiesPayload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); ?></script>
    <script>
        window.ICY_USER_NAME = <?php echo json_encode($currentUserName, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); ?>;
        window.ICY_QUOTES_ALLOWED_STATUSES = <?php echo json_encode($allowedStatuses, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); ?>;
        window.ICY_QUOTES_SYSTEM_STATUSES = <?php echo json_encode($systemManagedStatuses, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); ?>;
        window.ICY_QUOTES_CAN_BILL = <?php echo (function_exists('user_can') && user_can('module.billing')) ? 'true' : 'false'; ?>;
        window.ICY_QUOTES_CAN_GUIDE = <?php echo ((function_exists('user_can') && (user_can('billing.issue.dispatch_guide') || user_can('module.billing'))) || current_user_role() === 'admin') ? 'true' : 'false'; ?>;
        window.ICY_BILLING_PAYMENT_METHODS = <?php echo json_encode($billingPaymentMethodsPayload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); ?>;
        window.ICY_BILLING_CASH_ACCOUNTS = <?php echo json_encode($billingCashAccountsPayload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); ?>;
        window.ICY_BILLING_COUNTRIES = <?php echo json_encode($billingCountriesPayload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); ?>;
        window.ICY_BILLING_CURRENCIES = <?php echo json_encode($billingCurrenciesPayload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); ?>;
        window.ICY_BILLING_ENVIRONMENT = <?php echo json_encode($billingConfiguredEnvironment, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); ?>;
        window.ICY_BILLING_CONFIRMATION_TEXT = <?php echo json_encode($billingConfiguredConfirmationText, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); ?>;
    </script>

<style>
    .quotes-page-shell {
        padding-top: var(--space-6);
    }
    
    #quotesMain .page-header.header-responsive {
        display: flex;
        align-items: flex-start;
        justify-content: space-between;
        gap: var(--space-4);
        margin-bottom: var(--space-4);
    }
    
    #quotesMain .top-actions-cluster {
        display: flex;
        align-items: center;
        justify-content: flex-end;
        gap: var(--space-2);
        flex: 0 0 auto;
    }
    
    #quotesMain .quote-header-crm,
    #quotesMain .quote-header-embudo,
    #quotesMain .quote-header-create {
        white-space: nowrap;
        flex: 0 0 auto;
    }
    
    #quotesMain .quote-header-create {
        white-space: nowrap;
        flex: 0 0 auto;
    }
    
    .quotes-filter-bar {
        display: grid;
        grid-template-columns: minmax(280px, 1fr) 180px minmax(360px, 430px);
        gap: var(--space-3);
        align-items: center;
        width: 100%;
        max-width: 100%;
        padding: var(--space-4);
        background: var(--surface-1);
        border: 1px solid var(--border-default);
        border-radius: var(--radius-2xl);
        margin-bottom: var(--space-4);
        overflow: hidden;
    }
    
    .quotes-filter-bar > * {
        min-width: 0;
    }
    
    .quotes-kpis-row {
        margin-bottom: var(--space-4);
    }
    
    .quote-filter-date-group {
        display: grid;
        grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
        gap: var(--space-2);
        min-width: 0;
        width: 100%;
    }
    
    .quote-filter-date-group input {
        width: 100%;
        min-width: 0;
    }

    #quotesMain .quote-date-filter-field {
        position: relative;
        display: block;
        min-width: 0;
        width: 100%;
    }

    #quotesMain .quote-date-filter-field > span,
    #quotesMain .quote-date-filter-field > em {
        display: none;
    }
@media (min-width: 981px) and (max-width: 1280px) {
    #quotesMain .quotes-filter-bar {
        grid-template-columns: minmax(220px, 1fr) 160px minmax(300px, 360px);
        gap: var(--space-2);
    }

    #quotesMain .quote-filter-date-group {
        grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    }
}
.quote-status-quick { width: 100%; height: 36px; border-radius: var(--radius-lg); border: 1px solid var(--border-default); background: var(--surface-2); color: var(--text-primary); font-weight: 800; font-size: 0.82rem; padding: 0 24px 0 10px; cursor: pointer; }
.quote-status-quick.badge-success { border-color: rgba(0, 200, 80, 0.35); color: var(--color-primary); background: rgba(0, 255, 48, 0.08); }
.quote-status-quick.badge-warning { border-color: rgba(255, 190, 80, 0.45); color: var(--text-primary); background: rgba(255, 190, 80, 0.08); }
.quote-status-quick.badge-error { border-color: rgba(255, 77, 79, 0.45); color: var(--color-error); background: rgba(255, 77, 79, 0.08); }
.quote-row-deleted { opacity: 0.74; }
.quote-row-deleted .quote-link { text-decoration: line-through; }

/* Filtros: asegurar que las filas ocultas por JS desaparezcan también en móvil,
   donde las filas se fuerzan como grid con display: grid !important. */
#quotesMain #quotesTableBody tr.quote-row.is-filter-hidden,
#quotesMain #quotesTableBody tr.quote-row[hidden] {
    display: none !important;
}

.quote-restore-inline-form { display: inline-flex; }
.quotes-table-card {
    overflow: hidden !important;
    display: flex;
    flex-direction: column;
    min-height: clamp(280px, calc(100dvh - 420px), 520px);
}

.quotes-table-wrap {
    width: 100% !important;
    max-width: 100% !important;
    flex: 1 1 auto;
    min-height: 0;
    overflow-x: hidden !important;
    overflow-y: auto;
}

.quotes-table {
    width: 100% !important;
    min-width: 0 !important;
    max-width: 100% !important;
    table-layout: fixed !important;
    border-collapse: collapse;
}

.quotes-table th,
.quotes-table td {
    vertical-align: middle;
    min-width: 0 !important;
    box-sizing: border-box;
}

.col-folio { width: 12%; }
.col-client { width: 20%; }
.col-origin { width: auto; }
.col-status { width: 135px; }
.col-total { width: 128px; }
.col-date { width: 120px; }
.col-action { width: 96px; }

.quote-link {
    border: 0;
    background: transparent;
    padding: 0;
    color: var(--color-primary);
    font: inherit;
    font-weight: 800;
    cursor: pointer;
    text-align: left;
    max-width: 100%;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.quote-client-cell,
.quote-origin-cell {
    display: grid;
    min-width: 0;
    max-width: 100%;
}

.quote-client-cell strong,
.quote-origin-cell span {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.quote-client-cell span {
    color: var(--text-muted);
    font-size: 0.85rem;
}

.actions-cell-align {
    text-align: center !important;
    width: 76px;
    min-width: 76px;
    white-space: nowrap;
}

.quotes-table th:last-child {
    text-align: center !important;
}

.actions-btn-group {
    display: flex !important;
    justify-content: center !important;
    align-items: center !important;
    gap: 6px !important;
    flex-wrap: nowrap !important;
}

.action-icon-btn {
    padding: 0 !important;
    width: 36px !important;
    height: 36px !important;
    min-width: 36px !important;
    max-width: 36px !important;
    display: inline-flex !important;
    align-items: center !important;
    justify-content: center !important;
    border: 1px solid var(--border-default) !important;
    border-radius: var(--radius-md) !important;
    background: var(--surface-2) !important;
    color: var(--text-primary) !important;
    cursor: pointer;
}

.action-icon-btn:hover {
    background: var(--surface-3) !important;
}

.action-square-btn {
    padding: 0 !important;
    width: 42px !important;
    height: 42px !important;
    min-width: 42px !important;
    max-width: 42px !important;
    display: inline-flex !important;
    align-items: center !important;
    justify-content: center !important;
    border: 1px solid var(--border-default) !important;
    border-radius: var(--radius-lg) !important;
    background: var(--surface-1) !important;
    color: var(--text-primary) !important;
    cursor: pointer;
}

.action-square-btn:hover {
    background: var(--surface-2) !important;
}
.product-modal-backdrop { position: fixed; inset: 0; background: rgba(0,0,0,0.6); backdrop-filter: blur(4px); z-index: 1050; display: none; align-items: center; justify-content: center; opacity: 0; transition: opacity 0.2s ease; }
.product-modal-backdrop.show { opacity: 1; }
.quotes-modal-box { width: 1120px; max-width: 96%; max-height: 94dvh; overflow: hidden; position: relative; background: var(--surface-1); border: 1px solid var(--border-default); border-radius: var(--radius-xl); box-shadow: var(--shadow-lg); display: flex; flex-direction: column; transform: translateY(20px); transition: transform 0.2s ease; }
.product-modal-backdrop.show .quotes-modal-box { transform: translateY(0); }
.quote-view-layer { display: none; flex-direction: column; flex: 1; min-height: 0; width: 100%; }
.quote-view-layer.active { display: flex; }
.quote-modal-header { flex-shrink: 0; display: flex; align-items: flex-start; gap: 12px; justify-content: space-between; padding: var(--space-4) var(--space-5); border-bottom: 1px solid var(--border-subtle); }
.quote-modal-title { flex: 1; min-width: 0; }
.quote-modal-title h2 { margin-bottom: 2px; }
.quote-save-notice { padding: 10px var(--space-5); border-bottom: 1px solid rgba(0,255,48,0.18); background: var(--color-primary-soft); color: var(--text-primary); font-size: 0.9rem; font-weight: 700; }
.quote-document-body { padding: 0 !important; overflow-y: auto; flex: 1; background: #fff; }
.quote-doc { color: #00122f; background: #fff; padding: 42px 52px 28px; font-family: var(--font-sans); }
.quote-doc-head { display: flex; justify-content: space-between; gap: 24px; margin-bottom: 28px; align-items: flex-start; }
.quote-doc h2 { color: #00122f; font-size: 1.55rem; letter-spacing: -0.03em; }
.quote-doc-grid { display: grid; grid-template-columns: 1fr 1fr; border: 1px solid #dbe3ef; border-radius: 8px; overflow: hidden; margin-bottom: 34px; }
.quote-doc-cell { padding: 14px 18px; border-bottom: 1px solid #dbe3ef; min-height: 74px; }
.quote-doc-cell:nth-child(odd) { border-right: 1px solid #dbe3ef; }
.quote-doc-cell:nth-last-child(-n+2) { border-bottom: 0; }
.quote-doc-label { display: block; color: #536b8f; font-size: 0.76rem; font-weight: 800; text-transform: uppercase; margin-bottom: 8px; }
.quote-doc-value { color: #00122f; font-size: 1rem; }
.quote-doc-table { width: 100%; border-collapse: collapse; margin-bottom: 28px; }
.quote-doc-table th { color: #536b8f; font-size: 0.76rem; text-transform: uppercase; text-align: left; padding: 10px 14px; border-bottom: 2px solid #dbe3ef; }
.quote-doc-table td { color: #00122f; padding: 14px; border-bottom: 1px solid #dbe3ef; }
.quote-doc-table .text-right { text-align: right; }
.quote-doc-bottom { display: grid; grid-template-columns: minmax(0, 1fr) 380px; gap: 30px; align-items: start; }
.quote-doc-conditions { border: 1px solid #dbe3ef; border-radius: 8px; min-height: 118px; padding: 18px; background: #f8fafc; }
.quote-doc-totalbox { border: 1px solid #dbe3ef; border-radius: 8px; padding: 18px 22px; display: grid; gap: 12px; }
.quote-doc-totalbox div { display: flex; justify-content: space-between; gap: 16px; }
.quote-doc-totalbox hr { border: 0; border-top: 1px solid #dbe3ef; width: 100%; }
.quote-doc-totalbox .grand { font-size: 1.25rem; font-weight: 800; }
.product-modal-footer.quote-modal-footer { flex-shrink: 0; display: flex !important; flex-direction: row !important; justify-content: space-between !important; align-items: center !important; gap: 8px !important; padding: var(--space-3) var(--space-4) !important; background: var(--surface-0); border-top: 1px solid var(--border-subtle); border-radius: 0 0 var(--radius-xl) var(--radius-xl); }
.modal-footer-actions-right { display: flex !important; gap: 8px !important; justify-content: flex-end !important; flex: 1; flex-wrap: nowrap; }
.quote-wa-btn { color: #25D366 !important; border-color: #25D366 !important; }
.quote-form-grid-main { display: grid; grid-template-columns: minmax(0, 1.3fr) minmax(0, 1.2fr) 150px 130px; gap: var(--space-3); align-items: start; }
.quote-inline-search { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: var(--space-2); }
.quote-search-field { position: relative; }
.search-results-box { position: absolute; width: 100%; z-index: 1060; box-shadow: var(--shadow-lg); border: 1px solid var(--border-default); background: var(--surface-1); max-height: 240px; overflow-y: auto; border-radius: var(--radius-lg); top: calc(100% + 6px); left: 0; display: none; }
.search-results-box.show { display: block; }
.search-result-item { width: 100%; background: transparent; border: none; padding: 10px 12px; text-align: left; cursor: pointer; color: var(--text-secondary); border-bottom: 1px solid var(--border-subtle); display: flex; flex-direction: column; }
.search-result-item:hover { background: var(--surface-2); }
.search-result-item strong { color: var(--text-primary); font-size: 0.9rem; }
.search-result-item span { color: var(--text-muted); font-size: 0.75rem; margin-top: 2px; }
.search-result-item.is-muted { cursor: default; }
.selected-chip { margin-top: 8px; padding: 8px 10px; background: var(--color-primary-soft); border: 1px solid rgba(0,255,48,0.2); border-radius: var(--radius-md); font-size: 0.85rem; color: var(--text-primary); }
.selected-chip.is-locked { background: var(--surface-2); border-color: var(--border-default); }
.quote-builder-head { display: grid; grid-template-columns: minmax(220px, 1fr) 105px 82px 110px 130px 120px 42px; gap: var(--space-2); align-items: center; margin-top: var(--space-5); padding: 0 10px 8px; border-bottom: 1px solid var(--border-subtle); color: var(--text-muted); font-size: 0.75rem; font-weight: 800; text-transform: uppercase; }
.quote-items-box { display: grid; gap: var(--space-2); }
.eq-row { display: grid; grid-template-columns: minmax(220px, 1fr) 105px 82px 110px 130px 120px 42px; gap: var(--space-2); align-items: end; padding: 10px; background: var(--surface-2); border: 1px solid var(--border-default); border-radius: var(--radius-lg); }
.mobile-label { display: none; }
.it-total-text { height: 42px; display: flex; align-items: center; font-weight: 800; }
.quote-form-bottom { display: grid; grid-template-columns: minmax(0, 1fr) 360px; gap: var(--space-4); align-items: start; }
.quote-summary-card { border: 1px solid var(--border-default); border-radius: var(--radius-xl); padding: var(--space-4); display: grid; gap: 12px; background: var(--surface-2); }
.quote-summary-card div { display: flex; justify-content: space-between; gap: 14px; }
.quote-summary-card hr { width: 100%; border: 0; border-top: 1px solid var(--border-default); }
.quote-summary-card .total { font-size: 1.15rem; font-weight: 800; }
.quote-customer-grid { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-3); }
.hidden { display: none !important; }

#quotesMain .quote-folio-mobile {
    display: none;
}

#quotesMain .quote-folio-full {
    display: inline;
}
/* QUOTES RESPONSIVE CLEANUP FINAL */

/* Tablet / escritorio angosto */
@media (max-width: 980px) {
    #quotesMain .quote-form-grid-main,
    #quotesMain .quote-form-bottom,
    #quotesMain .quote-doc-bottom {
        grid-template-columns: 1fr !important;
    }

    #quotesMain .quote-doc {
        padding: 30px 26px;
    }

    #quotesMain .quote-builder-head {
        display: none !important;
    }
}

/* Mobile real / touch */
@media (max-width: 980px), (hover: none) and (pointer: coarse) {
    #quotesMain {
        overflow: hidden !important;
        max-width: 100vw !important;
    }

    #quotesMain * {
        box-sizing: border-box !important;
    }

    #quotesMain .quotes-page-shell {
        display: flex !important;
        flex-direction: column !important;
        height: calc(100dvh - var(--topbar-height, 72px)) !important;
        max-height: calc(100dvh - var(--topbar-height, 72px)) !important;
        min-height: 0 !important;
        overflow: hidden !important;
        padding: var(--space-3) !important;
        max-width: 100% !important;
    }

    /* Header móvil homologado a embudo */
    #quotesMain .page-header.header-responsive {
        display: grid !important;
        grid-template-columns: minmax(0, 1fr) auto !important;
        align-items: start !important;
        gap: 12px !important;
        width: 100% !important;
        margin-bottom: var(--space-4) !important;
    }

    #quotesMain .page-header-content {
        display: block !important;
        grid-column: 1 / 2 !important;
        min-width: 0 !important;
        max-width: 100% !important;
    }

    #quotesMain .page-header-content .label {
        display: block !important;
        margin: 0 0 6px 0 !important;
        font-size: 0.8rem !important;
        line-height: 1.1 !important;
    }

    #quotesMain .page-header-content h1 {
        display: block !important;
        margin: 0 0 8px 0 !important;
        font-size: 1.55rem !important;
        line-height: 1.15 !important;
    }

    #quotesMain .page-header-content p {
        display: block !important;
        margin: 0 !important;
        font-size: 0.95rem !important;
        line-height: 1.35 !important;
        color: var(--text-secondary) !important;
    }

    #quotesMain .top-actions-cluster {
        display: flex !important;
        grid-column: 2 / 3 !important;
        align-items: flex-start !important;
        justify-content: flex-end !important;
        gap: 8px !important;
        width: auto !important;
        min-width: 92px !important;
        max-width: max-content !important;
        padding-top: 34px !important;
        margin: 0 !important;
        flex: 0 0 auto !important;
    }

    #quotesMain .top-actions-cluster .quote-header-crm {
        display: none !important;
    }

    #quotesMain .top-actions-cluster .quote-header-embudo,
    #quotesMain .top-actions-cluster .quote-header-create {
        width: 42px !important;
        height: 42px !important;
        min-width: 42px !important;
        max-width: 42px !important;
        padding: 0 !important;
        margin: 0 !important;
        display: inline-flex !important;
        align-items: center !important;
        justify-content: center !important;
        border-radius: var(--radius-md) !important;
        font-size: 0 !important;
        flex: 0 0 42px !important;
    }

    #quotesMain .top-actions-cluster .quote-header-embudo span,
    #quotesMain .top-actions-cluster .quote-header-create span {
        display: none !important;
    }

    #quotesMain .top-actions-cluster .quote-header-embudo svg,
    #quotesMain .top-actions-cluster .quote-header-create svg {
        margin: 0 !important;
    }

    /* Filtros móviles */
    #quotesMain .quotes-filter-bar {
        display: grid !important;
        grid-template-columns: minmax(0, 1fr) !important;
        gap: 8px !important;
        flex-shrink: 0 !important;
        width: 100% !important;
        max-width: 100% !important;
        margin: 0 0 var(--space-3) 0 !important;
        padding: 0 !important;
        background: transparent !important;
        border: 0 !important;
        box-shadow: none !important;
        border-radius: 0 !important;
        overflow: visible !important;
    }

    #quotesMain #quoteSearch,
    #quotesMain #quoteStatusFilter {
        grid-column: 1 / -1 !important;
        min-width: 0 !important;
        width: 100% !important;
        max-width: 100% !important;
        height: 44px !important;
    }

    #quotesMain #quoteStatusFilter {
        padding-right: 38px !important;
    }

    #quotesMain .quote-filter-date-group {
        grid-column: 1 / -1 !important;
        display: grid !important;
        grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) !important;
        gap: 8px !important;
        width: 100% !important;
        max-width: 100% !important;
    }

    #quotesMain .quote-filter-date-group input {
        height: 42px !important;
        min-width: 0 !important;
        width: 100% !important;
        max-width: 100% !important;
    }

    #quotesMain .quote-date-filter-field {
        position: relative !important;
        min-width: 0 !important;
        width: 100% !important;
    }

    #quotesMain .quote-date-filter-field > span {
        display: block !important;
        margin-bottom: 4px !important;
        font-size: 0.68rem !important;
        line-height: 1 !important;
        font-weight: 800 !important;
        letter-spacing: 0.04em !important;
        text-transform: uppercase !important;
        color: var(--text-muted) !important;
    }

    #quotesMain .quote-date-filter-field > input {
        height: 44px !important;
        min-height: 44px !important;
        padding-left: 12px !important;
        padding-right: 12px !important;
        color: var(--text-primary) !important;
        background: var(--surface-1) !important;
    }

    #quotesMain .quote-date-filter-field > em {
        position: absolute !important;
        left: 12px !important;
        bottom: 15px !important;
        display: block !important;
        font-style: normal !important;
        font-size: 0.88rem !important;
        line-height: 1 !important;
        color: var(--text-muted) !important;
        pointer-events: none !important;
        opacity: 0.68 !important;
    }

    #quotesMain .quote-date-filter-field.has-value > em {
        display: none !important;
    }

    #quotesMain .mobile-hidden {
        display: none !important;
    }

    #quotesMain .quotes-kpis-row,
    #quotesMain .responsive-kpis.quotes-kpis-row,
    #quotesMain .grid.grid-4.responsive-kpis.quotes-kpis-row {
        display: none !important;
        visibility: hidden !important;
        height: 0 !important;
        min-height: 0 !important;
        max-height: 0 !important;
        margin: 0 !important;
        padding: 0 !important;
        overflow: hidden !important;
    }

    /* Tarjeta tabla */
    #quotesMain .quotes-table-card {
        flex: 1 1 auto !important;
        min-height: 0 !important;
        width: 100% !important;
        max-width: 100% !important;
        display: flex !important;
        flex-direction: column !important;
        overflow: hidden !important;
        border-radius: var(--radius-xl) !important;
    }

    #quotesMain .quotes-table-wrap,
    #quotesMain .table-wrap {
        flex: 1 1 auto !important;
        min-height: 0 !important;
        width: 100% !important;
        max-width: 100% !important;
        overflow-x: hidden !important;
        overflow-y: auto !important;
        -webkit-overflow-scrolling: touch !important;
    }

    #quotesMain .quotes-table-wrap::after,
    #quotesMain .table-wrap::after {
        content: none !important;
        display: none !important;
    }

    #quotesMain .quotes-table,
    #quotesMain .quotes-table thead,
    #quotesMain .quotes-table tbody {
        display: block !important;
        width: 100% !important;
        max-width: 100% !important;
        min-width: 0 !important;
        border-collapse: collapse !important;
        table-layout: fixed !important;
    }

    #quotesMain .quotes-table colgroup,
    #quotesMain .quotes-table th:nth-child(3),
    #quotesMain .quotes-table td:nth-child(3),
    #quotesMain .quotes-table th:nth-child(5),
    #quotesMain .quotes-table td:nth-child(5),
    #quotesMain .quotes-table th:nth-child(6),
    #quotesMain .quotes-table td:nth-child(6) {
        display: none !important;
        width: 0 !important;
        max-width: 0 !important;
        padding: 0 !important;
        border: 0 !important;
        overflow: hidden !important;
    }

    /* Columnas móviles: Folio | Cliente | Estado | Acción */
    #quotesMain .quotes-table thead tr,
    #quotesMain .quotes-table tbody tr.quote-row {
        display: grid !important;
        grid-template-columns: 82px minmax(0, 1fr) 104px 92px !important;
        width: 100% !important;
        max-width: 100% !important;
        min-width: 0 !important;
        align-items: stretch !important;
        gap: 0 !important;
        box-sizing: border-box !important;
    }

    #quotesMain .quotes-table thead tr {
        min-height: 42px !important;
        border-bottom: 1px solid var(--border-subtle) !important;
    }

    #quotesMain .quotes-table tbody tr.quote-row {
        min-height: 72px !important;
        border-bottom: 1px solid var(--border-subtle) !important;
    }

    #quotesMain .quotes-table th,
    #quotesMain .quotes-table td {
        display: flex !important;
        align-items: center !important;
        align-self: stretch !important;
        width: auto !important;
        min-width: 0 !important;
        max-width: 100% !important;
        margin: 0 !important;
        padding: 10px 8px !important;
        border: 0 !important;
        overflow: hidden !important;
        white-space: nowrap !important;
    }

    #quotesMain .quotes-table thead th {
        min-height: 42px !important;
        font-size: 0.69rem !important;
        line-height: 1 !important;
        letter-spacing: 0.04em !important;
        color: var(--text-muted) !important;
    }

    #quotesMain .quotes-table tbody td {
        min-height: 72px !important;
    }

    #quotesMain .quotes-table th:nth-child(4),
    #quotesMain .quotes-table td:nth-child(4),
    #quotesMain .quotes-table th:nth-child(7),
    #quotesMain .quotes-table td:nth-child(7) {
        justify-content: center !important;
        text-align: center !important;
        padding-left: 4px !important;
        padding-right: 4px !important;
    }

    #quotesMain .quote-folio-full {
        display: none !important;
    }

    #quotesMain .quote-folio-mobile {
        display: inline !important;
    }

    #quotesMain .quote-link {
        display: block !important;
        width: 100% !important;
        max-width: 100% !important;
        min-width: 0 !important;
        color: var(--color-primary) !important;
        font-size: 0.82rem !important;
        font-weight: 800 !important;
        line-height: 1.1 !important;
        white-space: nowrap !important;
        overflow: hidden !important;
        text-overflow: ellipsis !important;
        text-align: left !important;
        border: 0 !important;
        background: transparent !important;
        box-shadow: none !important;
        appearance: none !important;
        -webkit-appearance: none !important;
    }

    #quotesMain .quote-client-cell {
        display: flex !important;
        flex-direction: column !important;
        justify-content: center !important;
        gap: 2px !important;
        width: 100% !important;
        min-width: 0 !important;
        max-width: 100% !important;
        overflow: hidden !important;
    }

    #quotesMain .quote-client-cell strong,
    #quotesMain .quote-client-cell span {
        display: block !important;
        width: 100% !important;
        min-width: 0 !important;
        max-width: 100% !important;
        white-space: nowrap !important;
        overflow: hidden !important;
        text-overflow: ellipsis !important;
    }

    #quotesMain .quote-client-cell strong {
        font-size: 0.82rem !important;
        line-height: 1.12 !important;
        font-weight: 800 !important;
    }

    #quotesMain .quote-client-cell span {
        font-size: 0.75rem !important;
        line-height: 1.1 !important;
        color: var(--text-muted) !important;
    }

    #quotesMain .quote-status-quick {
        width: 104px !important;
        max-width: 104px !important;
        min-width: 104px !important;
        height: 38px !important;
        padding: 0 34px 0 10px !important;
        font-size: 0.74rem !important;
        text-overflow: ellipsis !important;
        overflow: hidden !important;
        white-space: nowrap !important;
    }

    #quotesMain .actions-cell-align,
    #quotesMain .actions-btn-group {
        display: flex !important;
        align-items: center !important;
        justify-content: center !important;
        width: 100% !important;
        min-width: 0 !important;
        max-width: 100% !important;
        padding-left: 0 !important;
        padding-right: 0 !important;
        overflow: visible !important;
    }

    #quotesMain .action-icon-btn {
        width: 40px !important;
        height: 40px !important;
        min-width: 40px !important;
        max-width: 40px !important;
        flex: 0 0 40px !important;
        padding: 0 !important;
        border-radius: var(--radius-lg) !important;
    }

    #quotesMain .quotes-pager {
        flex-shrink: 0 !important;
        min-height: 48px !important;
    }

    /* Modal móvil */
    #quotesMain .product-modal-backdrop {
        align-items: stretch !important;
        padding: 8px !important;
        overflow: hidden !important;
    }

    #quotesMain .quotes-modal-box {
        width: 100% !important;
        max-width: 100% !important;
        height: calc(100dvh - 16px) !important;
        max-height: calc(100dvh - 16px) !important;
        display: flex !important;
        flex-direction: column !important;
        overflow: hidden !important;
    }

    #quotesMain .quote-view-layer {
        flex: 1 1 auto !important;
        min-height: 0 !important;
        height: 100% !important;
        overflow: hidden !important;
    }

    #quotesMain .quote-view-layer.active {
        display: flex !important;
        flex-direction: column !important;
        min-height: 0 !important;
    }

    #quotesMain .quote-modal-header {
        flex: 0 0 auto !important;
        padding: var(--space-3) !important;
    }

    #quotesMain #quoteForm,
    #quotesMain #customerQuickForm {
        display: flex !important;
        flex-direction: column !important;
        flex: 1 1 auto !important;
        min-height: 0 !important;
        overflow: hidden !important;
    }

    #quotesMain .quote-document-body,
    #quotesMain #quoteForm > .product-modal-body,
    #quotesMain #customerQuickForm > .product-modal-body,
    #quotesMain .quote-view-layer .product-modal-body {
        flex: 1 1 auto !important;
        min-height: 0 !important;
        overflow-y: auto !important;
        overflow-x: hidden !important;
        -webkit-overflow-scrolling: touch !important;
    }

    #quotesMain .product-modal-footer.quote-modal-footer {
        flex: 0 0 68px !important;
        height: 68px !important;
        min-height: 68px !important;
        max-height: 68px !important;
        padding: 12px !important;
        gap: 8px !important;
        flex-wrap: nowrap !important;
        overflow: hidden !important;
    }

    #quotesMain .modal-footer-actions-left {
        display: flex !important;
        align-items: center !important;
        justify-content: flex-start !important;
        gap: 8px !important;
        flex: 0 0 auto !important;
        max-width: 104px !important;
        overflow: hidden !important;
    }

    #quotesMain .modal-footer-actions-left:empty {
        display: none !important;
    }

    #quotesMain .modal-footer-actions-right {
        display: flex !important;
        gap: 8px !important;
        flex: 1 1 auto !important;
        min-width: 0 !important;
        max-width: 100% !important;
        overflow: hidden !important;
        justify-content: flex-end !important;
    }

    #quotesMain .quote-modal-footer .btn {
        width: 44px !important;
        min-width: 44px !important;
        max-width: 44px !important;
        height: 44px !important;
        min-height: 44px !important;
        padding: 0 !important;
        flex: 0 0 44px !important;
        border-radius: var(--radius-lg) !important;
    }

    #quotesMain .quote-modal-footer .btn span {
        display: none !important;
    }

    #quotesMain .quote-modal-footer .btn svg {
        margin: 0 !important;
        flex-shrink: 0 !important;
    }

    #quotesMain #quoteSaveBtn {
        width: 48px !important;
        min-width: 48px !important;
        max-width: 48px !important;
        flex-basis: 48px !important;
    }

    #quotesMain #quoteFormDeleteBtn,
    #quotesMain .quote-delete-btn,
    #quotesMain .quote-danger-btn {
        color: var(--color-error) !important;
        border-color: rgba(255, 77, 79, 0.55) !important;
        background: rgba(255, 77, 79, 0.05) !important;
    }

    #quotesMain #quoteFormDeleteBtn svg,
    #quotesMain .quote-delete-btn svg,
    #quotesMain .quote-danger-btn svg {
        color: var(--color-error) !important;
        stroke: var(--color-error) !important;
    }

    #quotesMain .quote-doc {
        padding: 22px 16px !important;
    }

    #quotesMain .quote-doc-head,
    #quotesMain .quote-doc-grid {
        display: block !important;
    }

    #quotesMain .quote-doc-cell {
        border-right: 0 !important;
        border-bottom: 1px solid #dbe3ef !important;
    }

    #quotesMain .quote-doc-table th:nth-child(1),
    #quotesMain .quote-doc-table td:nth-child(1) {
        display: none !important;
    }

    #quotesMain .quote-customer-grid,
    #quotesMain .quote-form-grid-main {
        grid-template-columns: 1fr !important;
    }

    #quotesMain .eq-row {
        display: grid !important;
        grid-template-columns: 1fr 1fr !important;
        align-items: stretch !important;
        gap: 12px !important;
        padding: 16px !important;
        overflow: visible !important;
    }

    #quotesMain .eq-col-prod,
    #quotesMain .eq-col-del {
        grid-column: 1 / -1 !important;
    }

    #quotesMain .mobile-label {
        display: block !important;
        font-size: 11px !important;
        font-weight: 700 !important;
        text-transform: uppercase !important;
        color: var(--text-muted) !important;
        margin-bottom: 6px !important;
    }

    #quotesMain .eq-col-del .remove-row-btn {
        width: 100% !important;
        max-width: 100% !important;
        color: var(--color-error) !important;
        border-color: rgba(255, 77, 79, 0.55) !important;
    }

    #quotesMain .eq-col-del .remove-row-btn svg {
        color: var(--color-error) !important;
        stroke: var(--color-error) !important;
    }
}



/* P0.1 MODAL QUOTES - geometría estable y footer siempre visible */
#quotesMain .product-modal-backdrop {
    align-items: center !important;
    justify-content: center !important;
    padding: 24px !important;
    overflow: hidden !important;
}

#quotesMain .quotes-modal-box {
    width: min(1120px, calc(100vw - 48px)) !important;
    max-width: calc(100vw - 48px) !important;
    height: min(860px, calc(100vh - 48px)) !important;
    height: min(860px, calc(100dvh - 48px)) !important;
    max-height: calc(100vh - 48px) !important;
    max-height: calc(100dvh - 48px) !important;
    min-height: min(620px, calc(100vh - 48px)) !important;
    min-height: min(620px, calc(100dvh - 48px)) !important;
    display: flex !important;
    flex-direction: column !important;
    overflow: hidden !important;
}

#quotesMain .quote-view-layer,
#quotesMain .quote-view-layer.active {
    flex: 1 1 auto !important;
    min-height: 0 !important;
    overflow: hidden !important;
}

#quotesMain .quote-view-layer.active {
    display: flex !important;
    flex-direction: column !important;
}

#quotesMain #quoteForm,
#quotesMain #customerQuickForm {
    display: flex !important;
    flex: 1 1 auto !important;
    flex-direction: column !important;
    min-height: 0 !important;
    overflow: hidden !important;
}

#quotesMain .quote-document-body,
#quotesMain #quoteForm > .product-modal-body,
#quotesMain #customerQuickForm > .product-modal-body {
    flex: 1 1 auto !important;
    min-height: 0 !important;
    overflow-y: auto !important;
    overflow-x: hidden !important;
    -webkit-overflow-scrolling: touch !important;
}

#quotesMain .product-modal-footer.quote-modal-footer {
    flex: 0 0 auto !important;
    min-height: 72px !important;
    height: auto !important;
    max-height: none !important;
    overflow: visible !important;
}

#quotesMain #customerSelectorBlock.is-locked .quote-open-customer-create {
    display: none !important;
}

#quotesMain #customerSelectorBlock.is-locked .quote-inline-search {
    grid-template-columns: minmax(0, 1fr) !important;
}

#quotesMain #customerSelectorBlock.is-locked #customerSearch {
    width: 100% !important;
}

#quotesMain #customerSelectorBlock.is-locked .selected-chip {
    width: 100% !important;
    max-width: 100% !important;
}

@media (max-width: 980px), (hover: none) and (pointer: coarse) {
    #quotesMain .product-modal-backdrop {
        align-items: stretch !important;
        padding: 8px !important;
    }

    #quotesMain .quotes-modal-box {
        width: 100% !important;
        max-width: 100% !important;
        height: calc(100vh - 16px) !important;
        height: calc(100dvh - 100px) !important;
        max-height: calc(100vh - 100px) !important;
        max-height: calc(100dvh - 100px) !important;
        min-height: 0 !important;
    }

    #quotesMain .product-modal-footer.quote-modal-footer {
        min-height: 68px !important;
        height: 68px !important;
        max-height: 68px !important;
        overflow: hidden !important;
    }
}

/* Campos comerciales nuevos */
#quotesMain .quote-invoice-field.hidden,
#quotesMain .quote-payment-field.hidden {
    display: none !important;
}


/* P0.1C QUOTES DASHBOARD - sin scroll vertical de página en escritorio
   Regla: el único scroll vertical permitido en el dashboard de cotizaciones
   es el scroll interno de .quotes-table-wrap. */
@media (min-width: 981px) {
    #quotesMain {
        height: 100vh !important;
        max-height: 100vh !important;
        overflow: hidden !important;
    }

    #quotesMain .topbar {
        flex: 0 0 auto !important;
    }

    #quotesMain .quotes-page-shell {
        flex: 1 1 auto !important;
        min-height: 0 !important;
        height: auto !important;
        max-height: none !important;
        display: flex !important;
        flex-direction: column !important;
        overflow: hidden !important;
        padding-top: var(--space-5) !important;
        padding-bottom: var(--space-5) !important;
    }

    #quotesMain .page-header.header-responsive,
    #quotesMain .quotes-filter-bar,
    #quotesMain .quotes-kpis-row {
        flex: 0 0 auto !important;
    }

    #quotesMain .quotes-table-card {
        flex: 1 1 0 !important;
        min-height: 0 !important;
        height: auto !important;
        max-height: none !important;
        display: flex !important;
        flex-direction: column !important;
        overflow: hidden !important;
    }

    #quotesMain .quotes-table-wrap {
        flex: 1 1 auto !important;
        min-height: 0 !important;
        height: auto !important;
        max-height: none !important;
        overflow-y: auto !important;
        overflow-x: hidden !important;
        -webkit-overflow-scrolling: touch !important;
    }

    #quotesMain .quotes-table {
        margin-bottom: 0 !important;
    }
}


/* P14 - cotizaciones: footer homologado con Crear Cliente y modal contenido */
#quotesMain .product-modal-backdrop {
    align-items: center !important;
    justify-content: center !important;
    padding: 24px !important;
    overflow: hidden !important;
}

#quotesMain .quotes-modal-box {
    width: min(1120px, calc(100vw - 48px)) !important;
    max-width: calc(100vw - 48px) !important;
    height: min(700px, calc(100dvh - 100px)) !important;
    min-height: min(560px, calc(100dvh - 100px)) !important;
    max-height: calc(100dvh - 100px) !important;
    display: flex !important;
    flex-direction: column !important;
    overflow: hidden !important;
}

#quotesMain .quote-view-layer.active,
#quotesMain #quoteForm,
#quotesMain #customerQuickForm {
    display: flex !important;
    flex-direction: column !important;
    flex: 1 1 auto !important;
    min-height: 0 !important;
    overflow: hidden !important;
}

#quotesMain .quote-document-body,
#quotesMain #quoteForm > .product-modal-body,
#quotesMain #customerQuickForm > .product-modal-body {
    flex: 1 1 auto !important;
    min-height: 0 !important;
    overflow-y: auto !important;
    overflow-x: hidden !important;
    -webkit-overflow-scrolling: touch !important;
    padding-bottom: var(--space-5) !important;
}

#quotesMain .product-modal-footer.quote-modal-footer {
    flex: 0 0 auto !important;
    min-height: 72px !important;
    height: auto !important;
    max-height: none !important;
    display: flex !important;
    align-items: center !important;
    justify-content: flex-end !important;
    gap: 12px !important;
    flex-wrap: nowrap !important;
    padding: var(--space-4) var(--space-5) !important;
    background: var(--surface-0) !important;
    border-top: 1px solid var(--border-subtle) !important;
    border-radius: 0 0 var(--radius-xl) var(--radius-xl) !important;
    overflow: visible !important;
}

#quotesMain .product-modal-footer.quote-modal-footer:has(.modal-footer-actions-left:not(:empty)) {
    justify-content: space-between !important;
}

#quotesMain .modal-footer-actions-left {
    display: flex !important;
    align-items: center !important;
    justify-content: flex-start !important;
    gap: 10px !important;
    margin-right: auto !important;
    min-width: 0 !important;
    max-width: none !important;
    overflow: visible !important;
    flex: 0 0 auto !important;
}

#quotesMain .modal-footer-actions-left:empty {
    display: none !important;
}

#quotesMain .modal-footer-actions-right {
    width: auto !important;
    min-width: 0 !important;
    margin-left: auto !important;
    display: flex !important;
    align-items: center !important;
    justify-content: flex-end !important;
    gap: 12px !important;
    flex: 0 1 auto !important;
    overflow: visible !important;
}

#quotesMain .quote-modal-footer .btn,
#quotesMain .quote-modal-footer #quoteSaveBtn {
    width: auto !important;
    max-width: none !important;
    min-width: 132px !important;
    height: 48px !important;
    min-height: 48px !important;
    padding: 0 18px !important;
    display: inline-flex !important;
    align-items: center !important;
    justify-content: center !important;
    gap: 8px !important;
    flex: 0 0 auto !important;
    white-space: nowrap !important;
    border-radius: var(--radius-lg) !important;
}

#quotesMain #quoteFormDeleteBtn,
#quotesMain .quote-delete-btn,
#quotesMain .quote-danger-btn {
    width: 48px !important;
    min-width: 48px !important;
    max-width: 48px !important;
    padding: 0 !important;
    flex: 0 0 48px !important;
    color: var(--color-error) !important;
    border-color: rgba(255, 77, 79, 0.58) !important;
    background: rgba(255, 77, 79, 0.06) !important;
}

#quotesMain #quoteFormDeleteBtn svg,
#quotesMain .quote-delete-btn svg,
#quotesMain .quote-danger-btn svg {
    color: var(--color-error) !important;
    stroke: var(--color-error) !important;
}

#quotesMain .quote-modal-footer .btn span {
    display: inline-block !important;
    max-width: 18ch !important;
    overflow: hidden !important;
    text-overflow: ellipsis !important;
    white-space: nowrap !important;
}

#quotesMain #quoteFormDeleteBtn span,
#quotesMain .quote-delete-btn span,
#quotesMain .quote-danger-btn span {
    display: none !important;
}

@media (max-width: 760px) {
    #quotesMain .product-modal-backdrop {
        align-items: center !important;
        padding: 8px !important;
    }

    #quotesMain .quotes-modal-box {
        width: calc(100vw - 16px) !important;
        max-width: calc(100vw - 16px) !important;
        height: calc(100dvh - 200px) !important;
        min-height: 0 !important;
        max-height: calc(100dvh - 200px) !important;
    }

    #quotesMain .product-modal-footer.quote-modal-footer {
        min-height: 68px !important;
        height: auto !important;
        max-height: none !important;
        padding: 10px 18px calc(10px + env(safe-area-inset-bottom)) !important;
        gap: 10px !important;
        overflow: visible !important;
    }

    #quotesMain .modal-footer-actions-left,
    #quotesMain .modal-footer-actions-right {
        gap: 10px !important;
    }

    #quotesMain .modal-footer-actions-right {
        flex: 1 1 auto !important;
    }

    #quotesMain .quote-modal-footer .btn,
    #quotesMain .quote-modal-footer #quoteSaveBtn {
        width: auto !important;
        min-width: 104px !important;
        max-width: calc((100vw - 122px) / 2) !important;
        height: 48px !important;
        min-height: 48px !important;
        padding: 0 14px !important;
        flex: 0 1 auto !important;
    }

    #quotesMain #quoteFormDeleteBtn,
    #quotesMain .quote-delete-btn,
    #quotesMain .quote-danger-btn {
        width: 48px !important;
        min-width: 48px !important;
        max-width: 48px !important;
        padding: 0 !important;
        flex: 0 0 48px !important;
    }

    #quotesMain .quote-modal-footer .btn span {
        display: inline-block !important;
        max-width: 12ch !important;
        overflow: hidden !important;
        text-overflow: ellipsis !important;
        white-space: nowrap !important;
    }

    #quotesMain #quoteFormDeleteBtn span,
    #quotesMain .quote-delete-btn span,
    #quotesMain .quote-danger-btn span {
        display: none !important;
    }
}



/* P15 - Cotizaciones: vista previa compacta móvil sin romper footer */
@media (max-width: 760px) {
    #quotesMain #quoteLayerPreview .product-modal-footer.quote-modal-footer {
        justify-content: flex-end !important;
    }

    #quotesMain #quoteLayerPreview .modal-footer-actions-right {
        width: auto !important;
        max-width: 100% !important;
        flex: 0 1 auto !important;
        gap: 8px !important;
        overflow: visible !important;
    }

    #quotesMain #quoteLayerPreview .quote-modal-footer .btn {
        width: 48px !important;
        min-width: 48px !important;
        max-width: 48px !important;
        height: 48px !important;
        min-height: 48px !important;
        padding: 0 !important;
        flex: 0 0 48px !important;
    }

    #quotesMain #quoteLayerPreview .quote-modal-footer .btn span {
        display: none !important;
    }
}


/* =========================================================
   P22 - Quotes: modales homologados a Crear/Editar oportunidad
   Header/footer fijos; body con scroll interno; footer sin
   desplazamientos, con eliminar a la izquierda y acciones a la derecha.
   ========================================================= */
#quotesMain #quotesModalBackdrop.product-modal-backdrop {
    position: fixed !important;
    inset: 0 !important;
    display: none;
    align-items: center !important;
    justify-content: center !important;
    padding: 24px !important;
    overflow: hidden !important;
    background: rgba(0, 0, 0, 0.68) !important;
    backdrop-filter: blur(5px) !important;
    z-index: 1050 !important;
}

#quotesMain #quotesModalBackdrop.product-modal-backdrop.show {
    display: flex !important;
    opacity: 1 !important;
}

#quotesMain #quotesModalBackdrop > .quotes-modal-box {
    width: min(1120px, calc(100vw - 48px)) !important;
    max-width: calc(100vw - 48px) !important;
    height: min(700px, calc(100dvh - 100px)) !important;
    min-height: min(700px, calc(100dvh - 100px)) !important;
    max-height: calc(100dvh - 100px) !important;
    display: flex !important;
    flex-direction: column !important;
    overflow: hidden !important;
    border-radius: var(--radius-xl) !important;
    background: var(--surface-1) !important;
    border: 1px solid var(--border-default) !important;
    box-shadow: var(--shadow-lg) !important;
}

#quotesMain #quotesModalBackdrop .quote-view-layer {
    display: none !important;
    flex: 1 1 auto !important;
    width: 100% !important;
    height: 100% !important;
    min-height: 0 !important;
    overflow: hidden !important;
}

#quotesMain #quotesModalBackdrop .quote-view-layer.active {
    display: flex !important;
    flex-direction: column !important;
}

#quotesMain #quotesModalBackdrop #quoteForm,
#quotesMain #quotesModalBackdrop #customerQuickForm {
    display: flex !important;
    flex: 1 1 auto !important;
    flex-direction: column !important;
    width: 100% !important;
    min-height: 0 !important;
    overflow: hidden !important;
}

#quotesMain #quotesModalBackdrop .quote-modal-header {
    flex: 0 0 auto !important;
    min-height: 78px !important;
    padding: 20px 24px !important;
    display: flex !important;
    flex-direction: row !important;
    align-items: flex-start !important;
    justify-content: space-between !important;
    gap: 14px !important;
    border-bottom: 1px solid var(--border-subtle) !important;
    background: var(--surface-1) !important;
}

#quotesMain #quotesModalBackdrop .quote-modal-title {
    flex: 1 1 auto !important;
    min-width: 0 !important;
}

#quotesMain #quotesModalBackdrop .quote-modal-title .h3,
#quotesMain #quotesModalBackdrop .quote-modal-title h2 {
    line-height: 1.14 !important;
    margin-bottom: 3px !important;
}

#quotesMain #quotesModalBackdrop .quote-document-body,
#quotesMain #quotesModalBackdrop #quoteForm > .product-modal-body,
#quotesMain #quotesModalBackdrop #customerQuickForm > .product-modal-body,
#quotesMain #quotesModalBackdrop .quote-view-layer > .product-modal-body {
    flex: 1 1 auto !important;
    min-height: 0 !important;
    max-height: none !important;
    padding: 20px 24px !important;
    overflow-y: auto !important;
    overflow-x: hidden !important;
    -webkit-overflow-scrolling: touch !important;
    overscroll-behavior: contain !important;
}

#quotesMain #quotesModalBackdrop .product-modal-footer.quote-modal-footer {
    flex: 0 0 auto !important;
    min-height: 76px !important;
    height: auto !important;
    max-height: none !important;
    padding: 14px 20px !important;
    display: flex !important;
    flex-direction: row !important;
    align-items: center !important;
    justify-content: flex-end !important;
    gap: 12px !important;
    flex-wrap: nowrap !important;
    background: var(--surface-0) !important;
    border-top: 1px solid var(--border-subtle) !important;
    border-radius: 0 0 var(--radius-xl) var(--radius-xl) !important;
    overflow: visible !important;
}

#quotesMain #quoteLayerForm .product-modal-footer.quote-modal-footer:has(.modal-footer-actions-left:not(:empty)) {
    justify-content: space-between !important;
}

#quotesMain #quotesModalBackdrop .modal-footer-actions-left {
    flex: 0 0 auto !important;
    min-width: 48px !important;
    margin-right: auto !important;
    display: flex !important;
    align-items: center !important;
    justify-content: flex-start !important;
    gap: 8px !important;
    overflow: visible !important;
}

#quotesMain #quotesModalBackdrop .modal-footer-actions-left:empty {
    display: none !important;
    min-width: 0 !important;
    margin-right: 0 !important;
}

#quotesMain #quoteLayerForm .modal-footer-actions-left:has(#quoteFormDeleteBtn.hidden) {
    display: none !important;
    min-width: 0 !important;
    margin-right: 0 !important;
}

#quotesMain #quoteLayerForm .product-modal-footer.quote-modal-footer:has(#quoteFormDeleteBtn.hidden) {
    justify-content: flex-end !important;
}

#quotesMain #quotesModalBackdrop .modal-footer-actions-right {
    flex: 0 1 auto !important;
    width: auto !important;
    min-width: 0 !important;
    margin-left: auto !important;
    display: flex !important;
    align-items: center !important;
    justify-content: flex-end !important;
    gap: 12px !important;
    overflow: visible !important;
    flex-wrap: nowrap !important;
}

#quotesMain #quotesModalBackdrop .quote-modal-footer .btn,
#quotesMain #quotesModalBackdrop #quoteSaveBtn {
    width: auto !important;
    min-width: 118px !important;
    max-width: none !important;
    height: 48px !important;
    min-height: 48px !important;
    padding: 0 18px !important;
    display: inline-flex !important;
    align-items: center !important;
    justify-content: center !important;
    gap: 8px !important;
    flex: 0 0 auto !important;
    white-space: nowrap !important;
    text-align: center !important;
    overflow: visible !important;
    text-overflow: clip !important;
    border-radius: var(--radius-lg) !important;
}

#quotesMain #quotesModalBackdrop .modal-footer-actions-right .btn-primary,
#quotesMain #quotesModalBackdrop #quoteSaveBtn {
    min-width: 178px !important;
}

#quotesMain #quotesModalBackdrop .quote-modal-footer .btn span,
#quotesMain #quotesModalBackdrop #quoteSaveBtn span {
    display: inline-flex !important;
    align-items: center !important;
    justify-content: center !important;
    min-width: 0 !important;
    max-width: none !important;
    overflow: visible !important;
    text-overflow: clip !important;
    white-space: nowrap !important;
    text-align: center !important;
}

#quotesMain #quotesModalBackdrop #quoteFormDeleteBtn,
#quotesMain #quotesModalBackdrop .quote-delete-btn,
#quotesMain #quotesModalBackdrop .quote-danger-btn {
    width: 48px !important;
    min-width: 48px !important;
    max-width: 48px !important;
    height: 48px !important;
    min-height: 48px !important;
    padding: 0 !important;
    flex: 0 0 48px !important;
    color: var(--color-error) !important;
    border-color: rgba(255, 77, 79, 0.58) !important;
    background: rgba(255, 77, 79, 0.06) !important;
}

#quotesMain #quotesModalBackdrop #quoteFormDeleteBtn span,
#quotesMain #quotesModalBackdrop .quote-delete-btn span,
#quotesMain #quotesModalBackdrop .quote-danger-btn span {
    display: none !important;
}

@media (max-width: 760px) {
    #quotesMain #quotesModalBackdrop.product-modal-backdrop {
        align-items: center !important;
        justify-content: center !important;
        padding: 12px !important;
    }

    #quotesMain #quotesModalBackdrop > .quotes-modal-box {
        width: calc(100vw - 24px) !important;
        max-width: calc(100vw - 24px) !important;
        height: calc(100dvh - 200px) !important;
        min-height: 0 !important;
        max-height: calc(100dvh - 200px) !important;
        border-radius: 18px !important;
    }

    #quotesMain #quotesModalBackdrop .quote-modal-header {
        min-height: 64px !important;
        padding: 14px 14px !important;
        align-items: flex-start !important;
        gap: 10px !important;
    }

    #quotesMain #quotesModalBackdrop .quote-modal-title .h3,
    #quotesMain #quotesModalBackdrop .quote-modal-title h2 {
        font-size: 1.05rem !important;
        line-height: 1.14 !important;
        margin-bottom: 2px !important;
    }

    #quotesMain #quotesModalBackdrop .quote-modal-title .text-sm {
        font-size: 0.78rem !important;
        line-height: 1.15 !important;
    }

    #quotesMain #quotesModalBackdrop .quote-document-body,
    #quotesMain #quotesModalBackdrop #quoteForm > .product-modal-body,
    #quotesMain #quotesModalBackdrop #customerQuickForm > .product-modal-body,
    #quotesMain #quotesModalBackdrop .quote-view-layer > .product-modal-body {
        padding: 14px !important;
    }

    #quotesMain #quotesModalBackdrop .product-modal-footer.quote-modal-footer {
        min-height: 68px !important;
        padding: 10px 12px calc(10px + env(safe-area-inset-bottom)) !important;
        gap: 10px !important;
        overflow: visible !important;
    }

    #quotesMain #quotesModalBackdrop .modal-footer-actions-left {
        min-width: 44px !important;
        gap: 8px !important;
    }

    #quotesMain #quotesModalBackdrop .modal-footer-actions-right {
        gap: 10px !important;
        flex: 0 1 auto !important;
        min-width: 0 !important;
    }

    #quotesMain #quotesModalBackdrop .quote-modal-footer .btn,
    #quotesMain #quotesModalBackdrop #quoteSaveBtn {
        height: 44px !important;
        min-height: 44px !important;
        min-width: 106px !important;
        max-width: none !important;
        padding: 0 14px !important;
        font-size: 0.94rem !important;
        flex: 0 0 auto !important;
    }

    #quotesMain #quotesModalBackdrop .modal-footer-actions-right .btn-primary,
    #quotesMain #quotesModalBackdrop #quoteSaveBtn {
        min-width: 124px !important;
    }

    #quotesMain #quotesModalBackdrop .quote-modal-footer .btn span,
    #quotesMain #quotesModalBackdrop #quoteSaveBtn span {
        display: inline-flex !important;
        overflow: visible !important;
        text-overflow: clip !important;
        max-width: none !important;
    }

    #quotesMain #quotesModalBackdrop #quoteFormDeleteBtn,
    #quotesMain #quotesModalBackdrop .quote-delete-btn,
    #quotesMain #quotesModalBackdrop .quote-danger-btn {
        width: 44px !important;
        min-width: 44px !important;
        max-width: 44px !important;
        height: 44px !important;
        min-height: 44px !important;
        padding: 0 !important;
        flex: 0 0 44px !important;
    }

    #quotesMain #quoteLayerPreview .modal-footer-actions-right {
        gap: 8px !important;
    }

    #quotesMain #quoteLayerPreview .quote-modal-footer .btn {
        width: 44px !important;
        min-width: 44px !important;
        max-width: 44px !important;
        height: 44px !important;
        min-height: 44px !important;
        padding: 0 !important;
        flex: 0 0 44px !important;
    }

    #quotesMain #quoteLayerPreview .quote-modal-footer .btn span {
        display: none !important;
    }

    #quotesMain #quoteLayerPreview .quote-modal-footer .btn svg,
    #quotesMain #quotesModalBackdrop .quote-danger-btn svg {
        margin: 0 !important;
        flex-shrink: 0 !important;
    }
}

@media (max-width: 980px), (hover: none) and (pointer: coarse) {
    #quotesMain.quotes-deleted-view .quotes-table thead tr,
    #quotesMain.quotes-deleted-view .quotes-table tbody tr.quote-row {
        grid-template-columns: 82px minmax(0, 1fr) 104px 92px !important;
    }

    #quotesMain.quotes-deleted-view .actions-btn-group {
        gap: 6px !important;
        justify-content: center !important;
    }

    #quotesMain.quotes-deleted-view .action-icon-btn {
        width: 38px !important;
        height: 38px !important;
        min-width: 38px !important;
        max-width: 38px !important;
        flex: 0 0 38px !important;
    }

    #quotesMain.quotes-deleted-view .quote-status-quick {
        width: 98px !important;
        min-width: 98px !important;
        max-width: 98px !important;
    }
}

/* CRM Cotizaciones v7.2.8: misma capa visual que Crear Oportunidad. */
#quotesMain #quoteLayerForm.quote-opportunity-skin,
#quotesMain #quoteLayerForm.quote-opportunity-skin #quoteForm,
#quotesMain #quoteLayerForm.quote-opportunity-skin #quoteForm > .product-modal-body {
    background: var(--surface-1) !important;
}

#quotesMain #quoteLayerForm.quote-opportunity-skin #quoteForm > .product-modal-body {
    padding: var(--space-5) !important;
}

#quotesMain #quoteLayerForm.quote-opportunity-skin .quote-form-grid-main {
    column-gap: var(--space-4) !important;
    row-gap: var(--space-4) !important;
}

#quotesMain #quoteLayerForm.quote-opportunity-skin .form-field:not(.hidden),
#quotesMain #quoteLayerForm.quote-opportunity-skin .quote-search-field {
    display: grid !important;
    min-width: 0 !important;
    gap: 8px !important;
    align-content: start !important;
}

#quotesMain #quoteLayerForm.quote-opportunity-skin .form-field.hidden,
#quotesMain #quoteLayerForm.quote-opportunity-skin .quote-invoice-field.hidden,
#quotesMain #quoteLayerForm.quote-opportunity-skin .quote-payment-field.hidden {
    display: none !important;
}

#quotesMain #quoteLayerForm.quote-opportunity-skin .label {
    display: block !important;
    min-height: 18px !important;
    margin: 0 !important;
    color: var(--text-muted) !important;
    font-size: .78rem !important;
    font-weight: 700 !important;
    line-height: 1.2 !important;
    letter-spacing: .04em !important;
    text-transform: uppercase !important;
}

#quotesMain #quoteLayerForm.quote-opportunity-skin .input,
#quotesMain #quoteLayerForm.quote-opportunity-skin .select,
#quotesMain #quoteLayerForm.quote-opportunity-skin .textarea,
#quotesMain #quoteLayerForm.quote-opportunity-skin .form-field input:not([type="hidden"]),
#quotesMain #quoteLayerForm.quote-opportunity-skin .form-field select,
#quotesMain #quoteLayerForm.quote-opportunity-skin .form-field textarea {
    width: 100% !important;
    min-height: 46px !important;
    border: 1px solid var(--border-default) !important;
    border-radius: 14px !important;
    background-color: var(--surface-1) !important;
    color: var(--text-primary) !important;
    font-family: inherit !important;
    font-size: .94rem !important;
    line-height: 1.3 !important;
    box-shadow: none !important;
}

#quotesMain #quoteLayerForm.quote-opportunity-skin .input,
#quotesMain #quoteLayerForm.quote-opportunity-skin .select,
#quotesMain #quoteLayerForm.quote-opportunity-skin .form-field input:not([type="hidden"]),
#quotesMain #quoteLayerForm.quote-opportunity-skin .form-field select {
    height: 46px !important;
    padding: 0 14px !important;
}

#quotesMain #quoteLayerForm.quote-opportunity-skin .select,
#quotesMain #quoteLayerForm.quote-opportunity-skin .form-field select {
    padding-right: 44px !important;
}

#quotesMain #quoteLayerForm.quote-opportunity-skin .textarea,
#quotesMain #quoteLayerForm.quote-opportunity-skin .form-field textarea {
    min-height: 96px !important;
    padding: 14px !important;
    resize: vertical !important;
}

#quotesMain #quoteLayerForm.quote-opportunity-skin .input:hover,
#quotesMain #quoteLayerForm.quote-opportunity-skin .select:hover,
#quotesMain #quoteLayerForm.quote-opportunity-skin .textarea:hover {
    border-color: var(--color-primary) !important;
}

#quotesMain #quoteLayerForm.quote-opportunity-skin .input:focus,
#quotesMain #quoteLayerForm.quote-opportunity-skin .select:focus,
#quotesMain #quoteLayerForm.quote-opportunity-skin .textarea:focus {
    outline: none !important;
    border-color: var(--color-primary) !important;
    box-shadow: 0 0 0 4px rgba(0, 255, 48, .08) !important;
}

#quotesMain #quoteLayerForm.quote-opportunity-skin .quote-inline-search {
    gap: 8px !important;
    align-items: stretch !important;
}

#quotesMain #quoteLayerForm.quote-opportunity-skin .quote-inline-search > .btn {
    min-height: 46px !important;
    height: 46px !important;
    border-radius: 14px !important;
}

#quotesMain #quoteLayerForm.quote-opportunity-skin .quote-builder-head {
    margin-top: var(--space-4) !important;
}

#quotesMain #quoteLayerForm.quote-opportunity-skin .eq-row {
    background: var(--surface-1) !important;
    border-radius: 14px !important;
}

#quotesMain #quoteLayerForm.quote-opportunity-skin .quote-form-bottom {
    gap: var(--space-4) !important;
}

#quotesMain #quoteLayerForm.quote-opportunity-skin .quote-summary-card {
    background: var(--surface-1) !important;
    border-radius: 14px !important;
    box-shadow: none !important;
}

@media (max-width: 760px) {
    #quotesMain #quoteLayerForm.quote-opportunity-skin #quoteForm > .product-modal-body {
        padding: 16px !important;
    }

    #quotesMain #quoteLayerForm.quote-opportunity-skin .quote-form-grid-main,
    #quotesMain #quoteLayerForm.quote-opportunity-skin .quote-form-bottom {
        row-gap: 16px !important;
    }
}


/* Paso 5.2 - Wizard facturación tributaria dentro del modal de cotización */
#quotesMain .quote-billing-body { padding: 0 !important; background: var(--surface-0); }
#quotesMain .quote-billing-content { display: flex; flex-direction: column; gap: 18px; padding: 22px 24px 28px; }
#quotesMain .quote-billing-stepbar { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px; }
#quotesMain .quote-billing-step { border: 1px solid var(--border-subtle); border-radius: 999px; padding: 8px 10px; font-size: .78rem; font-weight: 700; text-align: center; color: var(--text-secondary); background: var(--surface-1); }
#quotesMain .quote-billing-step.is-active { color: #fff; background: var(--primary-600, #0ea5e9); border-color: var(--primary-600, #0ea5e9); }
#quotesMain .quote-billing-card { border: 1px solid var(--border-subtle); border-radius: 18px; padding: 18px; background: var(--surface-1); }
#quotesMain .quote-billing-card h3 { margin: 0 0 12px; font-size: 1.05rem; }
#quotesMain .quote-billing-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; }
#quotesMain .quote-billing-grid.three { grid-template-columns: repeat(3, minmax(0, 1fr)); }
#quotesMain .quote-billing-grid .span-2 { grid-column: span 2; }
#quotesMain .quote-billing-readonly { display: block; border: 1px solid var(--border-subtle); border-radius: 12px; padding: 10px 12px; min-height: 44px; background: #fff; }
#quotesMain .quote-billing-readonly .label { margin: 0 0 3px; font-size: .72rem; text-transform: uppercase; letter-spacing: .04em; color: var(--text-secondary); }
#quotesMain .quote-billing-readonly strong, #quotesMain .quote-billing-readonly span.value { display: block; min-width: 0; overflow-wrap: anywhere; }
#quotesMain .quote-billing-table-wrap { overflow: auto; border: 1px solid var(--border-subtle); border-radius: 14px; background: #fff; }
#quotesMain .quote-billing-detail-doc { background:#fff; }
#quotesMain .quote-billing-detail-doc .quote-doc-table { margin-top: 4px; }
#quotesMain .quote-billing-doc-bottom { margin-top: 18px; }
#quotesMain .quote-billing-table { width: 100%; border-collapse: collapse; min-width: 760px; }
#quotesMain .quote-billing-table th, #quotesMain .quote-billing-table td { padding: 10px 12px; border-bottom: 1px solid var(--border-subtle); text-align: left; vertical-align: top; }
#quotesMain .quote-billing-table th { font-size: .74rem; text-transform: uppercase; letter-spacing: .04em; color: var(--text-secondary); background: var(--surface-2); }
#quotesMain .quote-billing-summary { margin-left: auto; width: min(420px, 100%); display: grid; gap: 6px; }
#quotesMain .quote-billing-summary div { display: flex; justify-content: space-between; gap: 12px; }
#quotesMain .quote-billing-alert { border-radius: 16px; padding: 16px 18px; border: 1px solid var(--border-subtle); background: var(--surface-1); }
#quotesMain .quote-billing-alert.success { border-color: rgba(34,197,94,.35); background: rgba(34,197,94,.08); }
#quotesMain .quote-billing-alert.error { border-color: rgba(239,68,68,.35); background: rgba(239,68,68,.08); }
#quotesMain .quote-billing-alert.warning { border-color: rgba(245,158,11,.35); background: rgba(245,158,11,.08); }
#quotesMain .quote-billing-alert.draft { border-color: rgba(14,165,233,.32); background: rgba(14,165,233,.08); }
#quotesMain .quote-billing-result-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; }
#quotesMain .quote-billing-pdf-frame { width: 100%; height: min(70vh, 680px); min-height: 460px; border: 1px solid var(--border-subtle); border-radius: 14px; background: #fff; }
#quotesMain .quote-billing-confirm-box { display: grid; gap: 12px; }
#quotesMain .quote-billing-confirm-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; }
#quotesMain .quote-billing-email-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
#quotesMain .quote-billing-email-grid .span-2 { grid-column: span 2; }
#quotesMain .quote-billing-email-options { display: flex; flex-wrap: wrap; gap: 10px; }
#quotesMain .quote-billing-email-options label { display: inline-flex; align-items: center; gap: 8px; border: 1px solid var(--border-subtle); background: #fff; border-radius: 12px; padding: 10px 12px; font-weight: 700; }
#quotesMain #quoteBillingBtn[disabled] { cursor: not-allowed; opacity: .6; }
@media (max-width: 820px) {
    #quotesMain .quote-billing-stepbar { grid-template-columns: repeat(2, minmax(0, 1fr)); }
    #quotesMain .quote-billing-grid, #quotesMain .quote-billing-grid.three, #quotesMain .quote-billing-result-grid, #quotesMain .quote-billing-confirm-grid, #quotesMain .quote-billing-email-grid { grid-template-columns: 1fr; }
    #quotesMain .quote-billing-grid .span-2 { grid-column: auto; }
    #quotesMain .quote-billing-content { padding: 16px; }
    #quotesMain .quote-billing-pdf-frame { min-height: 420px; height: 62vh; }
}


/* Fase 8.2 v2: guía de despacho integrada al modal de cotización. */
#quotesMain .quote-guide-body { padding: 0 !important; background: var(--surface-0); }
#quotesMain .quote-guide-content { display: flex; flex-direction: column; gap: 18px; padding: 22px 24px 28px; }
#quotesMain .quote-guide-stepbar { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px; }
#quotesMain .quote-guide-step { border: 1px solid var(--border-subtle); border-radius: 999px; padding: 8px 10px; font-size: .78rem; font-weight: 700; text-align: center; color: var(--text-secondary); background: var(--surface-1); }
#quotesMain .quote-guide-step.is-active { color: #fff; background: var(--primary-600, #0ea5e9); border-color: var(--primary-600, #0ea5e9); }
#quotesMain .quote-guide-table-wrap { width: 100%; overflow: auto; border: 1px solid var(--border-subtle); border-radius: 14px; background: #fff; }
#quotesMain .quote-guide-table { width: 100%; min-width: 960px; border-collapse: collapse; }
#quotesMain .quote-guide-table th, #quotesMain .quote-guide-table td { padding: 10px 12px; border-bottom: 1px solid var(--border-subtle); text-align: left; vertical-align: middle; }
#quotesMain .quote-guide-table th { background: var(--surface-2); color: var(--text-secondary); font-size: .73rem; text-transform: uppercase; letter-spacing: .04em; }
#quotesMain .quote-guide-table input[type="number"] { width: 112px; }
#quotesMain .quote-guide-preview-frame { width: 100%; min-height: 460px; height: min(70vh, 680px); border: 1px solid var(--border-subtle); border-radius: 14px; background: #fff; }
#quotesMain .quote-guide-muted { color: var(--text-secondary); font-size: .86rem; }
#quotesMain .quote-guide-file-actions { display: flex; flex-wrap: wrap; gap: 10px; }
#quotesMain .quote-guide-destination { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; }
#quotesMain .quote-guide-destination label, #quotesMain .quote-guide-field { display: grid; gap: 6px; }
#quotesMain .quote-guide-destination .label, #quotesMain .quote-guide-field .label { font-size: .76rem; font-weight: 800; text-transform: uppercase; letter-spacing: .04em; color: var(--text-secondary); }
#quotesMain .quote-guide-confirm { display: grid; gap: 12px; }
@media (max-width: 760px) {
    #quotesMain .quote-guide-stepbar { grid-template-columns: repeat(2, minmax(0, 1fr)); }
    #quotesMain .quote-guide-content { padding: 16px; }
    #quotesMain .quote-guide-destination { grid-template-columns: 1fr; }
    #quotesMain .quote-guide-preview-frame { min-height: 420px; height: 62vh; }
}

</style>

<script>
(function () {
    function root() { return document.getElementById('quotesMain'); }
    function jsonFrom(id, fallback) { const el = document.getElementById(id); if (!el) return fallback; try { return JSON.parse(el.textContent || JSON.stringify(fallback)); } catch (e) { return fallback; } }
    function initState() { window.ICY_QUOTES_DATA = jsonFrom('quotesPayload', []); window.ICY_QUOTES_OPPORTUNITIES = jsonFrom('quotesOpportunitiesPayload', []); window.ICY_QUOTES_STATE = { currentQuoteId: 0, formMode: 'create', itemIndex: 0, noticeTimer: null, customerTimer: null, productTimer: null }; }
    function money(n) { return new Intl.NumberFormat('es-CL', { style: 'currency', currency: 'CLP', maximumFractionDigits: 0 }).format(Number(n || 0)); }
    function esc(v) { return String(v ?? '').replace(/[&<>"']/g, function (m) { return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#039;'}[m]; }); }
    function compactQuoteNumber(value) { return String(value || '').replace(/^([A-Z]+)-\d{4}-(\d+)$/, '$1-$2'); }
    function badgeClass(status) { if (['Aceptada', 'Pagada'].includes(status)) return 'badge-success'; if (['Enviada', 'Facturada', 'Esperando pago'].includes(status)) return 'badge-warning'; if (['Rechazada', 'Vencida', 'Anulada'].includes(status)) return 'badge-error'; return ''; }
    function isSystemManagedStatus(status) { return (window.ICY_QUOTES_SYSTEM_STATUSES || ['Facturada', 'Esperando pago', 'Pagada']).includes(status); }
    function statusOptionsHtml(current) { return (window.ICY_QUOTES_ALLOWED_STATUSES || ['Borrador', 'Enviada', 'Aceptada', 'Facturada', 'Esperando pago', 'Pagada', 'Rechazada', 'Vencida', 'Anulada']).map(function (status) { const managed = isSystemManagedStatus(status); return `<option value="${esc(status)}"${status === current ? ' selected' : ''}${managed && status !== current ? ' disabled' : ''}>${esc(managed ? status + ' · automático' : status)}</option>`; }).join(''); }
    const QUOTE_UNITS = ['unidad', 'mL', 'L', 'g', 'kg', 'pack', 'caja', 'servicio'];
    function cleanUnit(value) { return QUOTE_UNITS.includes(String(value || '').trim()) ? String(value).trim() : 'unidad'; }
    function unitSelectHtml(name, value) { const current = cleanUnit(value); return `<select class="input it-unit" name="${esc(name)}">${QUOTE_UNITS.map(function(unit) { return `<option value="${esc(unit)}"${unit === current ? ' selected' : ''}>${esc(unit)}</option>`; }).join('')}</select>`; }
    function statusSelectHtml(q) { const current = q.status || 'Borrador'; return `<select class="quote-status-quick ${badgeClass(current)}" data-quote-id="${esc(q.id)}" data-current="${esc(current)}" aria-label="Actualizar estado de cotización"${isSystemManagedStatus(current) ? ' disabled' : ''}>${statusOptionsHtml(current)}</select>`; }
    function quoteById(id) { return (window.ICY_QUOTES_DATA || []).find(function (q) { return Number(q.id) === Number(id); }); }
    function opportunityById(id) { return (window.ICY_QUOTES_OPPORTUNITIES || []).find(function (o) { return Number(o.id) === Number(id); }); }
    function modal() { return document.getElementById('quotesModalBackdrop'); }
    function setLayer(id) { document.querySelectorAll('#quotesModalBackdrop .quote-view-layer').forEach(function (layer) { layer.classList.remove('active'); }); const layer = document.getElementById(id); if (layer) { layer.classList.add('active'); const body = layer.querySelector('.product-modal-body'); if (body) body.scrollTop = 0; const modalBox = layer.closest('.quotes-modal-box'); if (modalBox) modalBox.dataset.activeLayer = id; } if (window.lucide) lucide.createIcons(); }
    function openModal() { const m = modal(); if (!m) return; m.style.display = 'flex'; requestAnimationFrame(function () { m.classList.add('show'); }); m.setAttribute('aria-hidden', 'false'); document.body.style.overflow = 'hidden'; }
    window.closeQuotesModal = function () { const m = modal(); if (!m) { document.body.style.overflow = ''; return; } m.classList.remove('show'); m.setAttribute('aria-hidden', 'true'); document.body.style.overflow = ''; setTimeout(function () { if (!m.classList.contains('show')) { m.style.display = 'none'; } if (window.ICY_QUOTES_REFRESH_AFTER_CLOSE) { window.ICY_QUOTES_REFRESH_AFTER_CLOSE = false; if (window.ICY_SPA_GO) window.ICY_SPA_GO(window.location.pathname + window.location.search, { replace: true, force: true }); else window.location.reload(); } }, 180); };
    function showNotice(message) { const notice = document.getElementById('quoteSaveNotice'); if (!notice) return; notice.textContent = message; notice.classList.remove('hidden'); if (window.ICY_QUOTES_STATE.noticeTimer) clearTimeout(window.ICY_QUOTES_STATE.noticeTimer); window.ICY_QUOTES_STATE.noticeTimer = setTimeout(function () { notice.classList.add('hidden'); }, 4200); }
    function hideNotice() { const notice = document.getElementById('quoteSaveNotice'); if (notice) notice.classList.add('hidden'); }
    function netAmount(q) { return Number(q.subtotal || 0) - Number(q.discount_amount || 0); }
    function renderPreviewDoc(q) {
        const rows = (q.items || []).map(function (it) { const sku = it.sku || ''; const displayName = it.display_name || String(it.name || '').replace(/\s*\(SKU:\s*[^)]+\)\s*$/i, ''); return `<tr><td><span class="mono" style="color:#536b8f;">${esc(sku || '—')}</span></td><td><strong>${esc(displayName)}</strong></td><td class="text-right">${esc(it.quantity || 1)}</td><td>${esc(cleanUnit(it.unit))}</td><td class="text-right">${money(it.price_unit)}</td><td class="text-right"><strong>${money(it.total_item)}</strong></td></tr>`; }).join('');
        return `<article class="quote-doc"><div class="quote-doc-head"><h2>Cotización ${esc(q.quote_number)}</h2><span class="badge ${badgeClass(q.status)}">${esc(String(q.status || 'Borrador').toUpperCase())}</span></div><div class="quote-doc-grid"><div class="quote-doc-cell"><span class="quote-doc-label">Razón social</span><strong class="quote-doc-value">${esc(q.customer_name || '')}</strong></div><div class="quote-doc-cell"><span class="quote-doc-label">Fecha</span><span class="quote-doc-value">${esc(q.created_at || '-')}</span></div><div class="quote-doc-cell"><span class="quote-doc-label">R.U.T.</span><span class="quote-doc-value">${esc(q.customer_tax_id || '—')}</span></div><div class="quote-doc-cell"><span class="quote-doc-label">Email cliente</span><span class="quote-doc-value">${esc(q.email || 'Sin email')}</span></div><div class="quote-doc-cell"><span class="quote-doc-label">Teléfono</span><span class="quote-doc-value">${esc(q.phone || 'Sin teléfono')}</span></div><div class="quote-doc-cell"><span class="quote-doc-label">Vendedor</span><span class="quote-doc-value">${esc(window.ICY_USER_NAME || 'Sistema')}</span></div></div><table class="quote-doc-table"><thead><tr><th>SKU</th><th>Glosa / Descripción</th><th class="text-right">Cant.</th><th>Unidad</th><th class="text-right">Prc. Unit</th><th class="text-right">Monto</th></tr></thead><tbody>${rows || '<tr><td colspan="6">Sin productos asociados.</td></tr>'}</tbody></table><div class="quote-doc-bottom"><div><span class="quote-doc-label">Condiciones comerciales</span><div class="quote-doc-conditions">${esc(q.notes || 'Sin condiciones comerciales adicionales especificadas.')}</div></div><div class="quote-doc-totalbox"><div><span>Subtotal Bruto:</span><strong>${money(q.subtotal)}</strong></div><div><span>Monto Neto:</span><strong>${money(netAmount(q))}</strong></div><div><span>Monto Exento:</span><strong>${money(0)}</strong></div><div><span>IVA (19%):</span><strong>${money(q.iva)}</strong></div><hr><div class="grand"><span>Total General:</span><strong>${money(q.total)}</strong></div></div></div></article>`;
    }
    window.openQuotePreview = function (id, noticeMessage) {
        const q = quoteById(id);
        if (!q) return;

        window.ICY_QUOTES_STATE.currentQuoteId = Number(q.id);
        document.getElementById('quotePreviewDocument').innerHTML = renderPreviewDoc(q);

        const publicUrl = `${window.location.origin}/modules/crm/quote_view.php?id=${q.id}`;
        const message = `Hola, te comparto la cotización ${q.quote_number} de ICYLAB por ${money(q.total)}: ${publicUrl}`;
        const pdfBtn = document.getElementById('quotePdfBtn');
        const waBtn = document.getElementById('quoteWhatsAppBtn');
        const mailBtn = document.getElementById('quoteMailBtn');
        const billingBtn = document.getElementById('quoteBillingBtn');
        const guideBtn = document.getElementById('quoteDispatchGuideBtn');
        const formDeleteBtn = document.getElementById('quoteFormDeleteBtn');

        if (formDeleteBtn) {
            formDeleteBtn.dataset.quoteId = q.id;
            formDeleteBtn.classList.toggle('hidden', !!q.is_deleted);
        }

        const editBtn = document.querySelector('#quoteLayerPreview .quote-open-edit');
        if (editBtn) editBtn.classList.toggle('hidden', !!q.is_deleted);

        if (pdfBtn) {
            pdfBtn.href = `/modules/crm/quote_pdf.php?id=${encodeURIComponent(q.id)}`;
            pdfBtn.setAttribute('download', `${String(q.quote_number || 'cotizacion').replace(/[^A-Za-z0-9_.-]/g, '_')}.pdf`);
        }

        if (waBtn) {
            if (q.whatsapp_phone) {
                waBtn.href = `https://wa.me/${q.whatsapp_phone}?text=${encodeURIComponent(message)}`;
                waBtn.classList.remove('hidden');
            } else {
                waBtn.href = '#';
                waBtn.classList.add('hidden');
            }
        }

        if (mailBtn) {
            if (q.email) {
                mailBtn.href = `mailto:${encodeURIComponent(q.email)}?subject=${encodeURIComponent('Cotización ICYLAB ' + q.quote_number)}&body=${encodeURIComponent(message)}`;
                mailBtn.classList.remove('hidden');
            } else {
                mailBtn.href = '#';
                mailBtn.classList.add('hidden');
            }
        }

        if (guideBtn) {
            const guideStatus = String(q.status || '').trim().toLowerCase();
            const canCreateGuide = !!window.ICY_QUOTES_CAN_GUIDE && ['aceptada', 'pagada', 'facturada'].includes(guideStatus) && !q.is_deleted;
            guideBtn.dataset.quoteId = q.id;
            guideBtn.classList.toggle('hidden', !canCreateGuide);
            guideBtn.disabled = !canCreateGuide;
            guideBtn.title = canCreateGuide ? 'Crear guía de despacho desde esta cotización' : 'Disponible para cotizaciones Aceptadas, Pagadas o Facturadas';
        }

        if (billingBtn) {
            const hasLinkedTaxDocument = String(q.facto_document_id || '').trim() !== '' || String(q.invoice_number || '').trim() !== '';
            const hasRecoverableIssue = String(q.billing_recovery_document_id || '').trim() !== '';
            const status = String(q.status || '').trim();
            const isAllowedBillingStatus = ['Aceptada', 'Pagada'].includes(status);
            const hasDraftTaxDocument = Number(q.billing_draft_log_id || 0) > 0;
            const canReconcileBilling = !!window.ICY_QUOTES_CAN_BILL && hasRecoverableIssue && !hasLinkedTaxDocument && !q.is_deleted;
            const canStartBilling = !!window.ICY_QUOTES_CAN_BILL && isAllowedBillingStatus && !hasLinkedTaxDocument && !hasRecoverableIssue && !q.is_deleted;
            const canReviewBilling = !!window.ICY_QUOTES_CAN_BILL && hasLinkedTaxDocument;
            const shouldShowBlocked = !!window.ICY_QUOTES_CAN_BILL && !hasLinkedTaxDocument && !hasRecoverableIssue && !q.is_deleted;

            billingBtn.dataset.quoteId = q.id;
            billingBtn.disabled = shouldShowBlocked && !canStartBilling && !canReconcileBilling;
            billingBtn.classList.toggle('hidden', !(canStartBilling || canReconcileBilling || canReviewBilling || shouldShowBlocked));
            billingBtn.classList.toggle('btn-primary', canStartBilling || canReconcileBilling);
            billingBtn.classList.toggle('btn-outline', canReviewBilling || (shouldShowBlocked && !canStartBilling && !canReconcileBilling));
            billingBtn.classList.toggle('quote-billing-blocked', shouldShowBlocked && !canStartBilling);
            billingBtn.title = canReconcileBilling
                ? 'Conciliar el documento que FACTO ya emitió sin crear otro folio'
                : canStartBilling
                ? (hasDraftTaxDocument ? 'Continuar borrador tributario' : 'Facturar cotización')
                : (canReviewBilling ? 'Ver documento tributario vinculado' : 'Disponible sólo para cotizaciones Aceptadas o Pagadas');

            const icon = billingBtn.querySelector('i');
            const label = billingBtn.querySelector('span');
            if (icon) icon.setAttribute('data-lucide', canReconcileBilling ? 'refresh-cw' : (canReviewBilling ? 'receipt-text' : (hasDraftTaxDocument ? 'file-clock' : 'file-text')));
            if (label) label.textContent = canReconcileBilling ? 'Conciliar emisión' : (canReviewBilling ? 'Ver facturación' : (hasDraftTaxDocument ? 'Continuar borrador' : 'Facturar'));
        }

        setLayer('quoteLayerPreview');
        if (noticeMessage) showNotice(noticeMessage); else hideNotice();
        openModal();
    };
    function setCustomerChip(customer, locked, reason) { const chip = document.getElementById('selectedCustomerChip'); const input = document.getElementById('formCustomerId'); if (!chip || !input || !customer) return; input.value = customer.id || 0; chip.innerHTML = `Cliente seleccionado: <strong>${esc(customer.display_name || customer.legal_name || customer.customer_name || '')}</strong>${customer.customer_tax_id ? ' · ' + esc(customer.customer_tax_id) : ''}${locked ? '<span class="text-muted"> · bloqueado</span>' : ''}${reason ? '<div class="text-muted" style="margin-top:2px;">' + esc(reason) + '</div>' : ''}`; chip.classList.remove('hidden'); chip.classList.toggle('is-locked', !!locked); }
    function applyCustomerLock(locked) { const search = document.getElementById('customerSearch'); const btn = document.querySelector('.quote-open-customer-create'); const block = document.getElementById('customerSelectorBlock'); if (block) block.classList.toggle('is-locked', !!locked); if (search) { search.disabled = !!locked; search.placeholder = locked ? 'Cliente bloqueado para esta cotización' : 'Buscar cliente por nombre o RUT'; if (locked) search.value = ''; } if (btn) { btn.disabled = !!locked; btn.setAttribute('aria-hidden', locked ? 'true' : 'false'); btn.style.opacity = locked ? '0.45' : ''; btn.style.pointerEvents = locked ? 'none' : ''; } const results = document.getElementById('customerResults'); if (results) { results.innerHTML = ''; results.classList.remove('show'); } }
    function clearForm() { const form = document.getElementById('quoteForm'); if (form) form.reset(); document.getElementById('formQuoteId').value = 0; document.getElementById('formCustomerId').value = 0; document.getElementById('formOpportunityId').value = 0; document.getElementById('quoteItems').innerHTML = ''; document.getElementById('opportunitySelect').disabled = false; document.getElementById('opportunitySelect').value = 0; document.getElementById('selectedCustomerChip').classList.add('hidden'); document.getElementById('selectedCustomerChip').classList.remove('is-locked'); document.getElementById('customerSearch').value = ''; document.getElementById('productSearch').value = ''; document.getElementById('customerResults').classList.remove('show'); document.getElementById('productResults').classList.remove('show'); window.ICY_QUOTES_STATE.itemIndex = 0; applyCustomerLock(false); calculateQuote(); }
    const BILLING_DOCUMENT_TYPES = [
        { key: 'invoice_electronic', label: 'Factura electrónica afecta emitida' },
        { key: 'invoice_exempt_electronic', label: 'Factura exenta electrónica emitida' },
        { key: 'invoice_export_electronic', label: 'Factura de exportación electrónica emitida' }
    ];
    const BILLING_TERMS = [
        { key: 'contado', label: 'Contado' },
        { key: 'credito_7', label: 'Crédito 7 días' },
        { key: 'credito_15', label: 'Crédito 15 días' },
        { key: 'credito_30', label: 'Crédito 30 días' },
        { key: 'credito_45', label: 'Crédito 45 días' },
        { key: 'otro', label: 'Otra condición' }
    ];

    const BILLING_PAYMENT_METHODS = (window.ICY_BILLING_PAYMENT_METHODS || []).map(function (method) {
        return { id: method.id || 0, key: method.method_key || method.key || '', label: method.method_name || method.label || 'Método de pago', type: method.method_type || '', is_default: Number(method.is_default || 0) === 1 };
    });
    const BILLING_CASH_ACCOUNTS = (window.ICY_BILLING_CASH_ACCOUNTS || []).map(function (account) {
        return { id: account.id || 0, key: account.account_key || account.key || '', label: account.account_name || account.label || 'Caja/cuenta', type: account.account_type || '', currency: account.currency_code || 'CLP', is_default: Number(account.is_default || 0) === 1 };
    });
    const BILLING_COUNTRIES = (window.ICY_BILLING_COUNTRIES || []).map(function (country) { return { id: Number(country.facto_country_id || 0), label: country.country_name || '' }; });
    const BILLING_CURRENCIES = (window.ICY_BILLING_CURRENCIES || []).map(function (currency) { return { code: currency.currency_code || '', label: currency.currency_name || '', id: Number(currency.facto_currency_id || 0) }; });
    const QUOTE_BILLING_STATE = { config: null, validation: null, preview: null, readiness: null, readinessKey: '' };
    function configuredBillingEnvironment() {
        const runtime = QUOTE_BILLING_STATE.readiness && QUOTE_BILLING_STATE.readiness.environment;
        const value = String(runtime || window.ICY_BILLING_ENVIRONMENT || 'sandbox').toLowerCase();
        return value === 'production' ? 'production' : 'sandbox';
    }
    function billingEnvironmentLabel() {
        return configuredBillingEnvironment() === 'production' ? 'Producción · documento real' : 'Sandbox · prueba';
    }
    function billingConfirmationText() {
        return String((QUOTE_BILLING_STATE.readiness && QUOTE_BILLING_STATE.readiness.confirmation_text) || window.ICY_BILLING_CONFIRMATION_TEXT || (configuredBillingEnvironment() === 'production' ? 'EMITIR PRODUCCION' : 'EMITIR')).trim().toUpperCase();
    }
    function isExportDocumentKey(key) { return String(key || '') === 'invoice_export_electronic'; }
    function billingCountryOptions(selected) { let html='<option value="">Seleccionar país</option>'; BILLING_COUNTRIES.forEach(function(c){ if(c.id===253)return; html+='<option value="'+esc(c.id)+'" '+(String(c.id)===String(selected||'')?'selected':'')+'>'+esc(c.label)+'</option>';}); return html; }
    function billingCurrencyOptions(selected) { let html=''; BILLING_CURRENCIES.forEach(function(c){ html+='<option value="'+esc(c.code)+'" '+(String(c.code)===String(selected||'USD')?'selected':'')+'>'+esc(c.code+' · '+c.label)+'</option>';}); return html; }
    function billingPaymentMethodOptionsHtml() {
        let html = '<option value="">Seleccionar método</option>';
        BILLING_PAYMENT_METHODS.forEach(function (method) { const value = method.id ? String(method.id) : method.key; html += '<option value="' + esc(value) + '" data-key="' + esc(method.key) + '" data-label="' + esc(method.label) + '"' + (method.is_default ? ' data-default="1"' : '') + '>' + esc(method.label) + '</option>'; });
        return html;
    }
    function billingCashAccountOptionsHtml() {
        let html = '<option value="">Seleccionar caja/cuenta</option>';
        BILLING_CASH_ACCOUNTS.forEach(function (account) { const value = account.id ? String(account.id) : account.key; const suffix = account.currency ? ' · ' + account.currency : ''; html += '<option value="' + esc(value) + '" data-key="' + esc(account.key) + '" data-label="' + esc(account.label) + '"' + (account.is_default ? ' data-default="1"' : '') + '>' + esc(account.label + suffix) + '</option>'; });
        return html;
    }
    function todayISO() { return new Date().toISOString().slice(0, 10); }
    function billingContent() { return document.getElementById('quoteBillingContent'); }
    function billingActions() { return document.getElementById('quoteBillingActions'); }
    function billingTitle(title, subtitle) { const t = document.getElementById('quoteBillingTitle'); const st = document.getElementById('quoteBillingSubtitle'); if (t) t.textContent = title || 'Facturación tributaria'; if (st) st.textContent = subtitle || 'Emisión vinculada a cotización'; }
    function renderBillingSteps(active) { const steps = [['prepare','1. Documento'],['detail','2. Receptor y detalle'],['preview','3. Revisión'],['result','4. Resultado']]; return '<div class="quote-billing-stepbar">' + steps.map(function (step) { return '<div class="quote-billing-step ' + (step[0] === active ? 'is-active' : '') + '">' + esc(step[1]) + '</div>'; }).join('') + '</div>'; }
    function documentTypeLabel(key) { const found = BILLING_DOCUMENT_TYPES.find(function (item) { return item.key === key; }); return found ? found.label : 'Documento tributario'; }
    function billingMoney(value, calculation) {
        const code = String((calculation && calculation.document_currency_code) || (billingConfig().documentKey === 'invoice_export_electronic' ? billingConfig().currencyCode : 'CLP') || 'CLP').toUpperCase();
        const amount = Number(value || 0);
        if (code === 'CLP') return money(amount);
        try { return new Intl.NumberFormat('es-CL', { style: 'currency', currency: code, minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(amount); }
        catch (e) { return code + ' ' + amount.toFixed(2); }
    }
    function initialBillingConfig(q) {
        return {
            documentKey: draftDocumentKey(q), environment: configuredBillingEnvironment(), issueDate: draftIssueDate(q), paymentTerms: draftPaymentTerms(q),
            receiverLegalName: q.customer_legal_name || q.customer_name || '', receiverTaxIdCode: q.export_tax_id_code || '', receiverTaxIdType: q.export_tax_id_type || '',
            receiverCountryId: q.export_country_id || '', receiverAddress: q.address || '', receiverDistrict: q.district || '', receiverCity: q.city || '', receiverPhone: q.phone || '',
            receiverActivity: q.business_activity || '', receiverEmail: q.email || '', currencyCode: 'USD', exchangeRateValue: '', saleModality: '1', saleClause: '6', exportPricingMode: 'convert_from_clp'
        };
    }
    function syncBillingConfigFromForm() {
        const current = QUOTE_BILLING_STATE.config || {};
        const map = {
            documentKey:'quoteBillingDocumentType',issueDate:'quoteBillingIssueDate',paymentTerms:'quoteBillingPaymentTerms',
            receiverLegalName:'quoteBillingReceiverLegalName',receiverTaxIdCode:'quoteBillingReceiverTaxId',receiverTaxIdType:'quoteBillingReceiverTaxType',receiverCountryId:'quoteBillingReceiverCountry',
            receiverAddress:'quoteBillingReceiverAddress',receiverDistrict:'quoteBillingReceiverDistrict',receiverCity:'quoteBillingReceiverCity',receiverPhone:'quoteBillingReceiverPhone',receiverActivity:'quoteBillingReceiverActivity',receiverEmail:'quoteBillingReceiverEmail',
            currencyCode:'quoteBillingCurrency',exchangeRateValue:'quoteBillingExchangeRate',saleModality:'quoteBillingSaleModality',saleClause:'quoteBillingSaleClause',exportPricingMode:'quoteBillingExportPricingMode'
        };
        Object.keys(map).forEach(function(key){ const el=document.getElementById(map[key]); if(el) current[key]=el.value; });
        current.environment=configuredBillingEnvironment();
        QUOTE_BILLING_STATE.config=current; return current;
    }
    function billingConfig() { return syncBillingConfigFromForm(); }
    function billingRequestPayload(q) {
        const cfg=billingConfig();
        return {
            quote_id:q.id,
            document_key:cfg.documentKey,
            document_type_key:cfg.documentKey,
            environment:cfg.environment,
            issue_date:cfg.issueDate,
            payment_terms:cfg.paymentTerms,
            payment_terms_key:cfg.paymentTerms,
            currency_code:isExportDocumentKey(cfg.documentKey)?(cfg.currencyCode||'USD'):'CLP', exchange_rate_value:cfg.exchangeRateValue||'', receiver_country_id:cfg.receiverCountryId||'', receiver_tax_id_code:cfg.receiverTaxIdCode||'',
            receiver_tax_id_type:cfg.receiverTaxIdType||'', receiver_legal_name:cfg.receiverLegalName||'', receiver_address:cfg.receiverAddress||'', receiver_district:cfg.receiverDistrict||'',
            receiver_city:cfg.receiverCity||'', receiver_phone:cfg.receiverPhone||'', receiver_activity:cfg.receiverActivity||'', receiver_email:cfg.receiverEmail||'', export_pricing_mode:cfg.exportPricingMode||'convert_from_clp',
            sale_modality:cfg.saleModality||'', sale_clause:cfg.saleClause||''
        };
    }
    function renderQuoteExportFields(cfg) {
        const display=isExportDocumentKey(cfg.documentKey)?'block':'none';
        return '<div class="quote-billing-card quote-export-config" id="quoteBillingExportFields" style="display:'+display+'"><h3>Datos de exportación</h3><p class="text-secondary">La cotización está expresada en CLP. El documento se convertirá a la moneda seleccionada usando el tipo de cambio informado.</p><div class="quote-billing-grid three">'+
            '<label><span class="label">Razón social extranjera</span><input class="input" id="quoteBillingReceiverLegalName" value="'+esc(cfg.receiverLegalName||'')+'"></label>'+ 
            '<label><span class="label">ID tributario extranjero</span><input class="input" id="quoteBillingReceiverTaxId" value="'+esc(cfg.receiverTaxIdCode||'')+'"></label>'+ 
            '<label><span class="label">Tipo ID tributario</span><input class="input" id="quoteBillingReceiverTaxType" placeholder="MX-RFC, US-EIN..." value="'+esc(cfg.receiverTaxIdType||'')+'"></label>'+ 
            '<label><span class="label">País receptor</span><select class="input" id="quoteBillingReceiverCountry">'+billingCountryOptions(cfg.receiverCountryId)+'</select></label>'+ 
            '<label><span class="label">Dirección</span><input class="input" id="quoteBillingReceiverAddress" value="'+esc(cfg.receiverAddress||'')+'"></label>'+ 
            '<label><span class="label">Distrito / comuna</span><input class="input" id="quoteBillingReceiverDistrict" value="'+esc(cfg.receiverDistrict||'')+'"></label>'+ 
            '<label><span class="label">Ciudad</span><input class="input" id="quoteBillingReceiverCity" value="'+esc(cfg.receiverCity||'')+'"></label>'+ 
            '<label><span class="label">Actividad económica</span><input class="input" id="quoteBillingReceiverActivity" value="'+esc(cfg.receiverActivity||'')+'"></label>'+ 
            '<label><span class="label">Email</span><input class="input" id="quoteBillingReceiverEmail" value="'+esc(cfg.receiverEmail||'')+'"></label>'+ 
            '<label><span class="label">Teléfono</span><input class="input" id="quoteBillingReceiverPhone" value="'+esc(cfg.receiverPhone||'')+'"></label>'+ 
            '<label><span class="label">Moneda</span><select class="input" id="quoteBillingCurrency">'+billingCurrencyOptions(cfg.currencyCode||'USD')+'</select></label>'+ 
            '<label><span class="label">Tipo de cambio a CLP</span><input class="input" type="number" min="0.000001" step="0.000001" id="quoteBillingExchangeRate" value="'+esc(cfg.exchangeRateValue||'')+'" placeholder="Consultando Findic…"><small id="quoteBillingExchangeRateMeta" class="text-secondary">Banco Central de Chile vía Findic</small></label>'+ 
            '<label><span class="label">Modalidad de venta</span><select class="input" id="quoteBillingSaleModality"><option value="1" '+(String(cfg.saleModality)==='1'?'selected':'')+'>A firme</option><option value="2" '+(String(cfg.saleModality)==='2'?'selected':'')+'>Bajo condición</option><option value="3" '+(String(cfg.saleModality)==='3'?'selected':'')+'>Consignación libre</option><option value="4" '+(String(cfg.saleModality)==='4'?'selected':'')+'>Consignación con mínimo</option><option value="9" '+(String(cfg.saleModality)==='9'?'selected':'')+'>Sin pago</option></select></label>'+ 
            '<label><span class="label">Cláusula de venta</span><select class="input" id="quoteBillingSaleClause"><option value="1" '+(String(cfg.saleClause)==='1'?'selected':'')+'>CIF</option><option value="2" '+(String(cfg.saleClause)==='2'?'selected':'')+'>CFR</option><option value="3" '+(String(cfg.saleClause)==='3'?'selected':'')+'>EXW</option><option value="5" '+(String(cfg.saleClause)==='5'?'selected':'')+'>FOB</option><option value="6" '+(String(cfg.saleClause)==='6'?'selected':'')+'>Sin cláusula</option><option value="9" '+(String(cfg.saleClause)==='9'?'selected':'')+'>DDP</option><option value="10" '+(String(cfg.saleClause)==='10'?'selected':'')+'>FCA</option><option value="11" '+(String(cfg.saleClause)==='11'?'selected':'')+'>CPT</option><option value="12" '+(String(cfg.saleClause)==='12'?'selected':'')+'>CIP</option><option value="17" '+(String(cfg.saleClause)==='17'?'selected':'')+'>DAT</option><option value="18" '+(String(cfg.saleClause)==='18'?'selected':'')+'>DAP</option></select></label>'+ 
            '<input type="hidden" id="quoteBillingExportPricingMode" value="convert_from_clp"></div></div>';
    }
    function toggleQuoteExportFields(){ const cfg=syncBillingConfigFromForm(); const panel=document.getElementById('quoteBillingExportFields'); if(panel) panel.style.display=isExportDocumentKey(cfg.documentKey)?'block':'none'; }
    async function refreshQuoteExchangeRate() {
        const cfg=syncBillingConfigFromForm();
        if (!isExportDocumentKey(cfg.documentKey)) return;
        const currencyEl=document.getElementById('quoteBillingCurrency');
        const dateEl=document.getElementById('quoteBillingIssueDate');
        const rateEl=document.getElementById('quoteBillingExchangeRate');
        const metaEl=document.getElementById('quoteBillingExchangeRateMeta');
        if(!currencyEl||!dateEl||!rateEl||!dateEl.value) return;
        const currency=String(currencyEl.value||'USD').toUpperCase();
        if(currency==='CLP'){
            rateEl.value='1'; rateEl.readOnly=true;
            QUOTE_BILLING_STATE.config.exchangeRateValue='1';
            if(metaEl) metaEl.textContent='CLP · sin conversión';
            return;
        }
        rateEl.readOnly=false; rateEl.setAttribute('aria-busy','true');
        if(metaEl) metaEl.textContent='Consultando Banco Central de Chile vía Findic…';
        try{
            const response=await postBilling('/modules/billing/exchange_rate_ajax.php',{currency_code:currency,date:dateEl.value});
            const result=response.result||response.data||response;
            rateEl.value=Number(result.rate_to_clp||0).toFixed(6).replace(/0+$/,'').replace(/\.$/,'');
            QUOTE_BILLING_STATE.config.exchangeRateValue=rateEl.value;
            if(metaEl) metaEl.textContent='Banco Central vía Findic · '+String(result.rate_date||dateEl.value)+(result.is_fallback_date?' · último día hábil disponible':'');
        }catch(error){
            if(metaEl) metaEl.textContent='Findic no disponible. Puedes informar una tasa manual autorizada.';
            if(!rateEl.value) rateEl.placeholder='Ingresa tasa manual';
        }finally{ rateEl.removeAttribute('aria-busy'); }
    }
    function setBillingActions(html) { const actions = billingActions(); if (actions) actions.innerHTML = html || ''; if (window.lucide) lucide.createIcons(); }
    function renderReadonly(label, value) { return '<div class="quote-billing-readonly"><div class="label">' + esc(label) + '</div><strong>' + esc(value || '—') + '</strong></div>'; }
    function hasNumberValue(value) { return value !== undefined && value !== null && value !== '' && !Number.isNaN(Number(value)); }
    function billingCalcValue(calculation, key, fallback) { return hasNumberValue(calculation && calculation[key]) ? Number(calculation[key]) : Number(fallback || 0); }
    function renderBillingQuoteDetail(q) {
        const rows = (q.items || []).map(function (it) {
            const sku = it.sku || '';
            const displayName = it.display_name || String(it.name || '').replace(/\s*\(SKU:\s*[^)]+\)\s*$/i, '');
            return '<tr>' +
                '<td><span class="mono" style="color:#536b8f;">' + esc(sku || '—') + '</span></td>' +
                '<td><strong>' + esc(displayName || 'Producto') + '</strong></td>' +
                '<td class="text-right">' + esc(it.quantity || 1) + '</td>' +
                '<td>' + esc(cleanUnit(it.unit)) + '</td>' +
                '<td class="text-right">' + money(it.price_unit) + '</td>' +
                '<td class="text-right"><strong>' + money(it.total_item) + '</strong></td>' +
                '</tr>';
        }).join('');
        return '<div class="quote-billing-card quote-billing-detail-doc"><h3>Detalle cotizado</h3>' +
            '<table class="quote-doc-table quote-billing-doc-table"><thead><tr><th>SKU</th><th>Glosa / Descripción</th><th class="text-right">Cant.</th><th>Unidad</th><th class="text-right">Prc. Unit</th><th class="text-right">Monto</th></tr></thead><tbody>' +
            (rows || '<tr><td colspan="6">Sin productos asociados.</td></tr>') +
            '</tbody></table>' +
            '<div class="quote-doc-bottom quote-billing-doc-bottom"><div><span class="quote-doc-label">Condiciones comerciales</span><div class="quote-doc-conditions">' + esc(q.notes || 'Sin condiciones comerciales adicionales especificadas.') + '</div></div>' +
            '<div class="quote-doc-totalbox"><div><span>Subtotal Bruto:</span><strong>' + money(q.subtotal) + '</strong></div><div><span>Monto Neto:</span><strong>' + money(netAmount(q)) + '</strong></div><div><span>Monto Exento:</span><strong>' + money(0) + '</strong></div><div><span>IVA (19%):</span><strong>' + money(q.iva) + '</strong></div><hr><div class="grand"><span>Total General:</span><strong>' + money(q.total) + '</strong></div></div></div>' +
            '</div>';
    }
    function normalizeValidationPayload(data) {
        if (!data) return null;
        if (data.result) return data.result;
        if (data.validation) return data.validation;
        return data;
    }

    function hasBillingDraft(q) {
        return q && Number(q.billing_draft_log_id || 0) > 0 && !(String(q.facto_document_id || '').trim() || String(q.invoice_number || '').trim());
    }
    function draftDocumentKey(q) {
        return (q && q.billing_draft_document_key) ? q.billing_draft_document_key : 'invoice_electronic';
    }
    function draftEnvironment(q) {
        return (q && q.billing_draft_environment) ? q.billing_draft_environment : 'sandbox';
    }
    function draftIssueDate(q) {
        return (q && q.billing_draft_issue_date) ? String(q.billing_draft_issue_date).slice(0, 10) : todayISO();
    }
    function draftPaymentTerms(q) {
        return (q && q.billing_draft_payment_terms) ? q.billing_draft_payment_terms : 'contado';
    }
    function billingDraftAlert(q) {
        if (!hasBillingDraft(q)) return '';
        let body = '<strong>Borrador tributario vinculado.</strong><br>Esta cotización tiene una preparación tributaria guardada';
        if (q.billing_draft_updated_at) body += ' · actualizado ' + esc(q.billing_draft_updated_at);
        body += '.';
        if (q.billing_draft_document_number) body += '<br>Folio preview/borrador: ' + esc(q.billing_draft_document_number);
        if (q.billing_draft_error_message) body += '<br><span class="text-secondary">Pendiente: ' + esc(q.billing_draft_error_message) + '</span>';
        return '<div class="quote-billing-alert draft">' + body + '</div>';
    }
    function billingReadinessHtml(q, cfg) {
        const readiness = QUOTE_BILLING_STATE.readiness;
        const readinessKey = Number(q.id) + ':' + String(cfg.documentKey || '');
        if (!readiness || QUOTE_BILLING_STATE.readinessKey !== readinessKey) {
            return '<div class="quote-billing-alert warning"><strong>Verificando datos…</strong><br>La aplicación está comprobando permisos y antecedentes tributarios.</div>';
        }
        const checks = Array.isArray(readiness.checks) ? readiness.checks : [];
        const failedChecks = checks.filter(function (check) { return check.required !== false && !check.ok; });
        const list = failedChecks.map(function (check) {
            return '<li><strong>' + (check.ok ? '✓ ' : '✕ ') + esc(check.label || 'Control') + ':</strong> ' + esc(check.detail || '') + '</li>';
        }).join('');
        const title = readiness.ready ? 'Listo para facturar' : 'Faltan datos para facturar';
        const css = readiness.ready ? 'success' : 'error';
        let html = '<div class="quote-billing-alert ' + css + '"><strong>' + esc(title) + '</strong><br>' + (readiness.ready ? 'Permisos, cliente y monto validados.' : 'Corrige los puntos indicados antes de continuar.') + '</div>';
        if (failedChecks.length) html += '<div class="quote-billing-card"><h3>Datos pendientes</h3><ul>' + list + '</ul></div>';
        return html;
    }
    async function refreshQuoteBillingReadiness(q) {
        if (!q) return;
        const cfg = billingConfig();
        const key = Number(q.id) + ':' + String(cfg.documentKey || '');
        QUOTE_BILLING_STATE.readiness = null;
        QUOTE_BILLING_STATE.readinessKey = '';
        renderBillingPrepare(q);
        try {
            const data = await postBilling('/modules/billing/quote_readiness_ajax.php', billingRequestPayload(q));
            QUOTE_BILLING_STATE.readiness = data && data.result ? data.result : null;
            QUOTE_BILLING_STATE.readinessKey = key;
            if (QUOTE_BILLING_STATE.config) QUOTE_BILLING_STATE.config.environment = configuredBillingEnvironment();
            renderBillingPrepare(q);
        } catch (error) {
            QUOTE_BILLING_STATE.readiness = {
                ready: false,
                environment: configuredBillingEnvironment(),
                environment_label: billingEnvironmentLabel(),
                confirmation_text: billingConfirmationText(),
                checks: [{ ok: false, label: 'Diagnóstico', detail: error.message || 'No fue posible verificar la habilitación.' }],
                context: { user_id: '—', customer_id: q.customer_id || '—', total_amount: q.total }
            };
            QUOTE_BILLING_STATE.readinessKey = key;
            renderBillingPrepare(q);
        }
    }
    function renderBillingPrepare(q, message) {
        if (!QUOTE_BILLING_STATE.config || Number(QUOTE_BILLING_STATE.quoteId || 0) !== Number(q.id)) { QUOTE_BILLING_STATE.config = initialBillingConfig(q); QUOTE_BILLING_STATE.quoteId = Number(q.id); }
        const cfg = QUOTE_BILLING_STATE.config;
        billingTitle('Facturar cotización', (q.quote_number || '') + ' · flujo vinculado');
        const content = billingContent();
        if (!content) return;
        const allowed = ['Aceptada', 'Pagada'].includes(String(q.status || '').trim());
        const linked = String(q.facto_document_id || '').trim() !== '' || String(q.invoice_number || '').trim() !== '';
        const recoveryDocumentId = String(q.billing_recovery_document_id || '').trim();
        let html = renderBillingSteps('prepare');
        if (message) html += '<div class="quote-billing-alert success">' + esc(message) + '</div>';
        if (!linked && recoveryDocumentId) {
            billingTitle('Conciliar emisión existente', (q.quote_number || '') + ' · no se emitirá otro documento');
            html += '<div class="quote-billing-alert error"><strong>FACTO ya creó este documento.</strong><br>No vuelvas a facturar la cotización. La conciliación consultará el documento existente y completará solamente la actualización local de ICYLAB OS.</div>';
            html += '<div class="quote-billing-card"><h3>Documento pendiente de vincular</h3><div class="quote-billing-grid">' +
                renderReadonly('FACTO document_id', recoveryDocumentId) +
                renderReadonly('Cotización', q.quote_number || '') +
                renderReadonly('Total', money(q.total)) +
                renderReadonly('Resultado financiero', 'Cuenta por cobrar') +
                '</div><p class="text-secondary mt-2">Esta acción usa GET sobre el documento existente; no ejecuta POST /documents, no consume un folio nuevo y no registra pago automático.</p></div>';
            if (q.billing_recovery_message) {
                html += '<div class="quote-billing-alert warning"><strong>Observación original</strong><br>' + esc(q.billing_recovery_message) + '</div>';
            }
            content.innerHTML = html;
            setBillingActions('<button type="button" class="btn btn-outline quote-modal-close"><i data-lucide="x"></i><span>Cerrar</span></button><button type="button" class="btn btn-primary quote-billing-reconcile"><i data-lucide="refresh-cw"></i><span>Conciliar documento existente</span></button>');
            return;
        }
        if (linked) {
            html += '<div class="quote-billing-alert success"><strong>Documento tributario vinculado.</strong><br>Esta cotización ya cuenta con documento asociado.</div><div class="quote-billing-card"><h3>Datos disponibles</h3><div class="quote-billing-grid">' + renderReadonly('Factura / folio', q.invoice_number || 'Sin folio local') + renderReadonly('FACTO document_id', q.facto_document_id || 'No informado') + renderReadonly('Estado cotización', q.status || 'Sin estado') + renderReadonly('Total', money(q.total)) + '</div></div>';
            if (q.billing_pdf_url) {
                html += '<div class="quote-billing-card"><h3>PDF factura asociada</h3><iframe class="quote-billing-pdf-frame" src="' + esc(q.billing_pdf_url) + '"></iframe></div>';
            } else {
                html += '<div class="quote-billing-alert warning"><strong>PDF no disponible.</strong><br>La cotización tiene documento tributario vinculado, pero no se encontró PDF asociado en billing_issue_logs.</div>';
            }
            content.innerHTML = html;
            setBillingActions('<button type="button" class="btn btn-outline quote-modal-close"><i data-lucide="x"></i><span>Cerrar</span></button><button type="button" class="btn btn-primary quote-billing-email-layer"><i data-lucide="mail"></i><span>Enviar correo</span></button>');
            return;
        }
        if (!allowed || q.is_deleted) {
            html += '<div class="quote-billing-alert error"><strong>No se puede iniciar facturación.</strong><br>';
            if (!allowed) html += 'La cotización debe estar Aceptada o Pagada. Estado actual: ' + esc(q.status || 'Sin estado') + '.<br>';
            if (q.is_deleted) html += 'La cotización está eliminada.<br>';
            html += '</div>';
            content.innerHTML = html;
            setBillingActions('<button type="button" class="btn btn-outline quote-modal-close"><i data-lucide="x"></i><span>Cerrar</span></button>');
            return;
        }
        html += billingDraftAlert(q);
        html += billingReadinessHtml(q, cfg);
        html += '<div class="quote-billing-card"><h3>Datos del documento</h3><div class="quote-billing-grid three">';
        html += '<label><span class="label">Tipo documento</span><select class="input" id="quoteBillingDocumentType">' + BILLING_DOCUMENT_TYPES.map(function (dt) { return '<option value="' + esc(dt.key) + '" ' + (dt.key === cfg.documentKey ? 'selected' : '') + '>' + esc(dt.label) + '</option>'; }).join('') + '</select></label>';
        html += renderReadonly('Ambiente', billingEnvironmentLabel());
        html += '<label><span class="label">Fecha emisión</span><input class="input" type="date" id="quoteBillingIssueDate" value="' + esc(cfg.issueDate) + '"></label>';
        html += '<label><span class="label">Condición de pago</span><select class="input" id="quoteBillingPaymentTerms">' + BILLING_TERMS.map(function (term) { return '<option value="' + esc(term.key) + '" ' + (term.key === cfg.paymentTerms ? 'selected' : '') + '>' + esc(term.label) + '</option>'; }).join('') + '</select></label>';
        html += renderReadonly('Folio cotización', q.quote_number || '');
        html += renderReadonly('Total cotizado', money(q.total));
        html += '</div></div>' + renderQuoteExportFields(cfg) + '<div class="quote-billing-card"><h3>Reglas del flujo vinculado</h3><p class="text-secondary">Los productos y cantidades quedan bloqueados a la cotización seleccionada. En exportación puedes completar los datos extranjeros del receptor y convertir los montos desde CLP.</p></div>';
        content.innerHTML = html;
        const readinessKey = Number(q.id) + ':' + String(cfg.documentKey || '');
        const readinessCurrent = QUOTE_BILLING_STATE.readiness && QUOTE_BILLING_STATE.readinessKey === readinessKey;
        const readinessReady = readinessCurrent && QUOTE_BILLING_STATE.readiness.ready;
        const retry = '<button type="button" class="btn btn-outline quote-billing-readiness-retry"><i data-lucide="refresh-cw"></i><span>Revisar habilitación</span></button>';
        const draft = '<button type="button" class="btn btn-outline quote-billing-save-draft"><i data-lucide="save"></i><span>Guardar borrador</span></button>';
        const next = '<button type="button" class="btn btn-primary quote-billing-continue"' + (readinessReady ? '' : ' disabled aria-disabled="true"') + '><i data-lucide="arrow-right"></i><span>Continuar</span></button>';
        setBillingActions((readinessCurrent && !readinessReady ? retry : '') + draft + next);
        if (isExportDocumentKey(cfg.documentKey) && !cfg.exchangeRateValue) window.setTimeout(refreshQuoteExchangeRate, 0);
    }
    function renderBillingDetail(q) {
        const cfg = syncBillingConfigFromForm();
        billingTitle('Receptor y detalle', (q.quote_number || '') + ' · revisión previa');
        const content = billingContent();
        if (!content) return;
        let html = renderBillingSteps('detail');
        const exportMode = isExportDocumentKey(cfg.documentKey);
        html += '<div class="quote-billing-card"><h3>Receptor</h3><div class="quote-billing-grid">' +
            renderReadonly(exportMode ? 'ID tributario extranjero' : 'RUT', exportMode ? (cfg.receiverTaxIdCode || 'Sin ID') : (q.customer_tax_id || 'Sin RUT')) +
            renderReadonly('Razón social', exportMode ? (cfg.receiverLegalName || 'Sin razón social') : (q.customer_name || 'Sin razón social')) +
            renderReadonly('Email', exportMode ? (cfg.receiverEmail || 'Sin email') : (q.email || 'Sin email')) +
            renderReadonly(exportMode ? 'País / ciudad' : 'Teléfono', exportMode ? ((cfg.receiverCountryId || '—') + ' · ' + (cfg.receiverCity || '—')) : (q.phone || 'Sin teléfono')) +
            '</div><p class="text-secondary mt-2">' + (exportMode ? 'Documento de exportación en ' + esc(cfg.currencyCode || 'USD') + ' · tipo de cambio ' + esc(cfg.exchangeRateValue || 'sin informar') + '.' : 'El receptor no se puede cambiar en este flujo. Si falta información tributaria, edita el cliente y vuelve a validar.') + '</p></div>';
        html += renderBillingQuoteDetail(q);
        content.innerHTML = html;
        setBillingActions('<button type="button" class="btn btn-outline quote-billing-save-draft"><i data-lucide="save"></i><span>Guardar borrador</span></button><button type="button" class="btn btn-primary quote-billing-validate"><i data-lucide="shield-check"></i><span>Validar</span></button>');
    }
    function renderValidationResult(q, data) {
        const validation = normalizeValidationPayload(data);
        if (!validation) {
            renderBillingError('La respuesta no contiene validación tributaria.');
            return;
        }
        const ok = !!validation.ok;
        const calculation = validation.calculation || {};
        const errors = Array.isArray(validation.errors) ? validation.errors.slice() : [];
        const warnings = Array.isArray(validation.warnings) ? validation.warnings.slice() : [];
        if (!ok && !errors.length) {
            if (validation.message) errors.push(validation.message);
            else if (validation.error) errors.push(validation.error);
            else errors.push('La validación fue bloqueada, pero el servicio no devolvió el detalle del bloqueo. Revisa el log de billing_issue_logs o vuelve a validar.');
        }
        const subtotalValue = billingCalcValue(calculation, 'subtotal_before_global_discount', q.subtotal);
        const globalDiscountValue = billingCalcValue(calculation, 'global_discount_amount', q.discount_amount || 0);
        const taxableValue = billingCalcValue(calculation, 'taxable_net_amount', netAmount(q));
        const exemptValue = billingCalcValue(calculation, 'exempt_net_amount', 0);
        const ivaValue = billingCalcValue(calculation, 'taxes_amount', q.iva);
        const totalValue = billingCalcValue(calculation, 'total_amount', q.total);
        billingTitle(ok ? 'Datos validados' : 'Faltan datos', (q.quote_number || '') + ' · ' + documentTypeLabel(billingConfig().documentKey));
        let html = renderBillingSteps('detail');
        html += '<div class="quote-billing-alert ' + (ok ? 'success' : 'error') + '"><strong>' + (ok ? 'La información está completa' : 'No se puede continuar aún') + '</strong><br>' + (ok ? 'Revisa los montos y continúa a la vista previa.' : 'Corrige los datos indicados y vuelve a validar.') + '</div>';
        if (errors.length) html += '<div class="quote-billing-card"><h3>Bloqueos</h3><ul>' + errors.map(function (e) { return '<li>' + esc(e) + '</li>'; }).join('') + '</ul></div>';
        if (warnings.length) html += '<div class="quote-billing-card"><h3>Advertencias</h3><ul>' + warnings.map(function (e) { return '<li>' + esc(e) + '</li>'; }).join('') + '</ul></div>';
        html += '<div class="quote-billing-result-grid"><div class="quote-billing-card"><h3>Montos del documento</h3><div class="quote-billing-summary"><div><span>Subtotal</span><strong>' + billingMoney(subtotalValue, calculation) + '</strong></div><div><span>Descuento</span><strong>' + billingMoney(globalDiscountValue, calculation) + '</strong></div><div><span>Neto afecto</span><strong>' + billingMoney(taxableValue, calculation) + '</strong></div><div><span>Neto exento</span><strong>' + billingMoney(exemptValue, calculation) + '</strong></div><div><span>IVA</span><strong>' + billingMoney(ivaValue, calculation) + '</strong></div><hr><div><span>Total</span><strong>' + billingMoney(totalValue, calculation) + '</strong></div></div></div><div class="quote-billing-card"><h3>Pago</h3><div class="quote-billing-grid">' + renderReadonly('Condición', (validation.payment && validation.payment.payment_conditions) || billingConfig().paymentTerms || 'Contado') + renderReadonly('Vencimiento', (validation.payment && validation.payment.due_date) || 'No definido') + '</div><p class="text-secondary mt-2">La factura quedará por cobrar, salvo que marques expresamente “Registrar como pagada” en la confirmación final.</p></div></div>';
        billingContent().innerHTML = html;
        setBillingActions(ok ? '<button type="button" class="btn btn-outline quote-billing-save-draft"><i data-lucide="save"></i><span>Guardar borrador</span></button><button type="button" class="btn btn-primary quote-billing-preview"><i data-lucide="file-search"></i><span>Previsualizar</span></button>' : '<button type="button" class="btn btn-outline quote-billing-save-draft"><i data-lucide="save"></i><span>Guardar borrador</span></button><button type="button" class="btn btn-outline quote-modal-close"><i data-lucide="x"></i><span>Cerrar</span></button>');
    }
    function renderBillingPreview(q, data) {
        const result = data && data.result ? data.result : null;
        if (!result) {
            renderBillingError('La respuesta no contiene resultado de preview.');
            return;
        }
        const preview = result.preview || {};
        const validation = result.validation || {};
        const ok = !!result.ok;
        const isLocal = !!preview.local_only || preview.mode === 'local_validation';
        const customer = validation.customer || {};
        const payment = validation.payment || {};
        billingTitle(ok ? 'Revisar factura' : 'No fue posible preparar la revisión', (q.quote_number || '') + ' · revisión final');
        let html = renderBillingSteps('preview');
        html += '<div class="quote-billing-alert ' + (ok ? 'success' : 'error') + '"><strong>' + (ok ? 'BORRADOR VISUAL — TODAVÍA NO EMITIDO' : 'No se pudo preparar el borrador') + '</strong><br>' + (ok ? 'Revisa receptor, productos y montos. La siguiente pantalla solicitará la confirmación final.' : esc(result.message || preview.message || '')) + '</div>';
        html += '<div class="quote-billing-card"><h3>Receptor</h3><div class="quote-billing-grid three">' +
            renderReadonly('Razón social', customer.name || q.customer_name || '') +
            renderReadonly('RUT', customer.tax_id || q.customer_tax_id || '') +
            renderReadonly('Dirección', [q.address, q.district, q.city].filter(Boolean).join(' · ')) +
            renderReadonly('Fecha de emisión', billingConfig().issueDate || todayISO()) +
            renderReadonly('Condición de pago', payment.payment_conditions || billingConfig().paymentTerms || 'Contado') +
            renderReadonly('Vencimiento', payment.due_date || 'No definido') +
            '</div></div>';
        html += renderBillingQuoteDetail(q);
        if (preview.has_pdf && preview.pdf_url) {
            html += '<div class="quote-billing-card"><h3>Documento preliminar</h3><iframe class="quote-billing-pdf-frame" src="' + esc(preview.pdf_url) + '"></iframe></div>';
        } else if (!isLocal) {
            html += '<div class="quote-billing-alert warning"><strong>Documento preliminar no disponible.</strong><br>Revisa la información mostrada antes de continuar.</div>';
        }
        billingContent().innerHTML = html;
        setBillingActions(ok ? '<button type="button" class="btn btn-primary quote-billing-confirm-layer"><i data-lucide="arrow-right"></i><span>Continuar a confirmación</span></button>' : '<button type="button" class="btn btn-outline quote-modal-close"><i data-lucide="x"></i><span>Cerrar</span></button>');
    }
    function issueConfirmationIsReady() {
        const confirmationInput = document.getElementById('quoteBillingIssueConfirmation');
        const confirmation = confirmationInput ? String(confirmationInput.value || '').trim().toUpperCase() : '';
        if (confirmation !== billingConfirmationText()) return false;
        const markPaid = document.getElementById('quoteBillingMarkAsPaid');
        if (markPaid && markPaid.checked) {
            const paymentMethod = document.getElementById('quoteBillingPaymentMethod');
            const cashAccount = document.getElementById('quoteBillingCashAccount');
            const paymentDate = document.getElementById('quoteBillingPaymentDate');
            if (!paymentMethod || !paymentMethod.value) return false;
            if (!cashAccount || !cashAccount.value) return false;
            if (!paymentDate || !paymentDate.value) return false;
        }
        return true;
    }
    function updateIssueConfirmButton() {
        const btn = document.querySelector('.quote-billing-confirm-issue');
        if (!btn) return;
        const ready = issueConfirmationIsReady();
        btn.disabled = !ready;
        btn.setAttribute('aria-disabled', ready ? 'false' : 'true');
        btn.title = ready ? 'Confirmar emisión en ' + billingEnvironmentLabel() : 'Completa la confirmación requerida antes de emitir';
    }
    function renderBillingConfirm(q) {
        const confirmationText = billingConfirmationText();
        const isProduction = configuredBillingEnvironment() === 'production';
        billingTitle(isProduction ? 'Confirmar emisión real' : 'Confirmar emisión sandbox', (q.quote_number || '') + ' · requiere ' + confirmationText);
        let html = renderBillingSteps('preview');
        const exportMode = isExportDocumentKey(billingConfig().documentKey);
        const paymentBlock = exportMode ? '<div class="quote-billing-alert warning"><strong>Estado inicial: por cobrar.</strong><br>El pago de la factura de exportación se registra posteriormente en Cobranza.</div>' : '<div class="quote-billing-alert warning"><strong>Estado inicial: por cobrar.</strong><br>Si el cliente ya pagó, marca expresamente la opción siguiente y completa los datos del cobro.</div><label class="quote-billing-readonly"><input type="checkbox" id="quoteBillingMarkAsPaid" value="1"> El cliente ya pagó: registrar el cobro junto con la factura</label><div class="quote-billing-confirm-grid" id="quoteBillingPaidFields" style="display:none;"><label><span class="label">Fecha de pago</span><input class="input" type="date" id="quoteBillingPaymentDate" value="' + esc(todayISO()) + '"></label><label><span class="label">Método de pago</span><select class="input" id="quoteBillingPaymentMethod">' + billingPaymentMethodOptionsHtml() + '</select></label><label><span class="label">Caja / cuenta destino</span><select class="input" id="quoteBillingCashAccount">' + billingCashAccountOptionsHtml() + '</select></label><label><span class="label">Referencia pago</span><input class="input" type="text" id="quoteBillingPaymentReference" placeholder="Nº transferencia, voucher, folio..."></label><label><span class="label">Nota pago</span><input class="input" type="text" id="quoteBillingPaymentNotes" placeholder="Comentario interno para análisis"></label></div>';
        html += '<div class="quote-billing-card quote-billing-confirm-box"><h3>Confirmación final</h3><div class="quote-billing-alert ' + (isProduction ? 'error' : 'warning') + '"><strong>' + (isProduction ? 'EMISIÓN REAL E IRREVERSIBLE' : 'Emisión de prueba') + '</strong><br>' + (isProduction ? 'La siguiente acción enviará la factura a FACTO producción, consumirá un folio tributario y creará la cuenta por cobrar.' : 'El documento se emitirá únicamente en sandbox.') + '</div>' + renderReadonly('Cliente', q.customer_name || '') + renderReadonly('RUT receptor', q.customer_tax_id || '') + renderReadonly('Total documento', money(q.total)) + paymentBlock + '<label><span class="label">Confirmación</span><input class="input" type="text" id="quoteBillingIssueConfirmation" placeholder="Escribe ' + esc(confirmationText) + '" autocomplete="off"></label><p class="text-secondary mt-2">El botón se activa únicamente al escribir <strong>' + esc(confirmationText) + '</strong>.</p></div>';
        billingContent().innerHTML = html;
        setBillingActions('<button type="button" class="btn btn-danger quote-billing-confirm-issue" disabled aria-disabled="true"><i data-lucide="send"></i><span>' + (isProduction ? 'Emitir factura real' : 'Confirmar emisión sandbox') + '</span></button>');
        updateIssueConfirmButton();
    }
    function renderIssueResult(q, data) {
        const result = data && data.result ? data.result : null;
        if (!result) {
            renderBillingError('La respuesta no contiene resultado de emisión.');
            return;
        }
        const issue = result.issue || {};
        const ok = !!result.ok;
        billingTitle(ok ? 'Factura emitida' : 'Emisión pendiente de conciliación', (q.quote_number || '') + ' · resultado');
        let html = renderBillingSteps('result');
        html += '<div class="quote-billing-alert ' + (ok ? 'success' : 'error') + '"><strong>' + (ok ? 'Documento tributario emitido y vinculado' : 'No vuelvas a emitir esta cotización') + '</strong><br>' + esc(result.message || '') + '</div>';
        html += '<div class="quote-billing-result-grid"><div class="quote-billing-card"><h3>Factura</h3><div class="quote-billing-grid">' + renderReadonly('Folio', issue.document_number || 'Pendiente') + renderReadonly('Total', money(q.total)) + renderReadonly('PDF', issue.has_pdf ? 'Disponible' : 'Pendiente de sincronización') + renderReadonly('XML', issue.has_xml ? 'Disponible' : 'Pendiente de sincronización') + '</div></div><div class="quote-billing-card"><h3>Actualización automática</h3><div class="quote-billing-grid">' + renderReadonly('Cotización', issue.quote_status || (ok ? 'Esperando pago' : 'Pendiente')) + renderReadonly('Cobranza', issue.payment_status === 'pagado' ? 'Pagada' : (issue.payment_status === 'por_cobrar' ? 'Por cobrar' : 'Pendiente')) + renderReadonly('Cuenta por cobrar', issue.collection_created ? 'Creada' : 'Pendiente') + renderReadonly('Pago', issue.payment_registered ? 'Registrado' : 'No registrado') + '</div></div></div>';
        if (issue.has_pdf && issue.pdf_url) {
            html += '<div><h3>PDF emitido</h3><iframe class="quote-billing-pdf-frame" src="' + esc(issue.pdf_url) + '"></iframe></div>';
        }
        billingContent().innerHTML = html;
        if (ok) {
            q.status = issue.quote_status || 'Facturada';
            q.facto_document_id = issue.facto_document_id || q.facto_document_id || '';
            q.invoice_number = issue.document_number || q.invoice_number || '';
            q.billing_issue_log_id = issue.log_id || q.billing_issue_log_id || 0;
            q.billing_pdf_url = issue.pdf_url || q.billing_pdf_url || '';
            q.billing_xml_available = !!issue.has_xml;
            q.billing_issue_environment = issue.environment || q.billing_issue_environment || configuredBillingEnvironment();
            renderTable();
            window.ICY_QUOTES_REFRESH_AFTER_CLOSE = true;
        }
        setBillingActions('<button type="button" class="btn btn-outline quote-modal-close"><i data-lucide="x"></i><span>Cerrar</span></button>' + (ok ? '<button type="button" class="btn btn-primary quote-billing-email-layer"><i data-lucide="mail"></i><span>Enviar correo</span></button>' : ''));
    }
    function defaultBillingEmailSubject(q) {
        const folio = q.invoice_number || q.quote_number || '';
        return 'Documento tributario ICYLAB ' + folio;
    }
    function defaultBillingEmailBody(q) {
        const folio = q.invoice_number || 'emitido';
        return 'Hola,\n\nAdjuntamos el documento tributario asociado a la cotización ' + (q.quote_number || '') + '.\n\nFolio: ' + folio + '\n\nSaludos,\nICYLAB';
    }
    function renderBillingEmail(q, message) {
        billingTitle('Enviar documento', (q.quote_number || '') + ' · PDF/XML');
        const content = billingContent();
        if (!content) return;
        let html = renderBillingSteps('result');
        if (message) html += '<div class="quote-billing-alert success">' + esc(message) + '</div>';
        const hasDocument = String(q.facto_document_id || '').trim() !== '' || String(q.invoice_number || '').trim() !== '' || Number(q.billing_issue_log_id || 0) > 0;
        if (!hasDocument) {
            html += '<div class="quote-billing-alert error"><strong>No hay documento emitido.</strong><br>Primero debes emitir o vincular un documento tributario.</div>';
            content.innerHTML = html;
            setBillingActions('<button type="button" class="btn btn-outline quote-billing-prepare"><i data-lucide="arrow-left"></i><span>Volver</span></button>');
            return;
        }
        const pdfChecked = q.billing_pdf_url ? 'checked' : '';
        const xmlChecked = q.billing_xml_available ? 'checked' : '';
        const xmlDisabled = q.billing_xml_available ? '' : 'disabled';
        html += '<div class="quote-billing-card"><h3>Documento a enviar</h3><div class="quote-billing-grid">' +
            renderReadonly('Factura / folio', q.invoice_number || 'Sin folio local') +
            renderReadonly('FACTO document_id', q.facto_document_id || 'No informado') +
            renderReadonly('Cliente', q.customer_name || '') +
            renderReadonly('Total', money(q.total)) +
            '</div></div>';
        html += '<div class="quote-billing-card"><h3>Correo</h3><div class="quote-billing-email-grid">' +
            '<label><span class="label">Email cliente</span><input class="input" type="email" id="quoteBillingEmailTo" value="' + esc(q.email || '') + '" placeholder="cliente@dominio.cl"></label>' +
            '<label><span class="label">CC / email adicional</span><input class="input" type="text" id="quoteBillingEmailCc" placeholder="correo1@dominio.cl, correo2@dominio.cl"></label>' +
            '<label class="span-2"><span class="label">Asunto</span><input class="input" type="text" id="quoteBillingEmailSubject" value="' + esc(defaultBillingEmailSubject(q)) + '"></label>' +
            '<label class="span-2"><span class="label">Mensaje</span><textarea class="input" id="quoteBillingEmailBody" rows="7">' + esc(defaultBillingEmailBody(q)) + '</textarea></label>' +
            '</div><div class="quote-billing-email-options mt-3">' +
            '<label><input type="checkbox" id="quoteBillingAttachPdf" value="1" ' + pdfChecked + '> Adjuntar PDF</label>' +
            '<label><input type="checkbox" id="quoteBillingAttachXml" value="1" ' + xmlChecked + ' ' + xmlDisabled + '> Adjuntar XML</label>' +
            '</div><p class="text-secondary mt-2">El envío queda registrado en billing_document_emails para trazabilidad.</p></div>';
        content.innerHTML = html;
        setBillingActions('<button type="button" class="btn btn-outline quote-billing-prepare"><i data-lucide="arrow-left"></i><span>Volver</span></button><button type="button" class="btn btn-primary quote-billing-send-email"><i data-lucide="mail"></i><span>Enviar documento</span></button>');
    }
    function renderBillingEmailResult(q, data) {
        const result = data && data.result ? data.result : {};
        billingTitle('Documento enviado', (q.quote_number || '') + ' · correo');
        let html = renderBillingSteps('result');
        html += '<div class="quote-billing-alert success"><strong>Correo enviado correctamente.</strong><br>' + esc(result.message || 'Documento enviado.') + '</div>';
        html += '<div class="quote-billing-card"><h3>Detalle del envío</h3><div class="quote-billing-grid">' +
            renderReadonly('Destinatario', result.recipient_email || '') +
            renderReadonly('CC', result.cc_email || 'Sin copia') +
            renderReadonly('Asunto', result.subject || '') +
            renderReadonly('Registro email', result.email_id ? ('#' + result.email_id) : 'No informado') +
            '</div></div>';
        billingContent().innerHTML = html;
        setBillingActions('<button type="button" class="btn btn-outline quote-billing-prepare"><i data-lucide="arrow-left"></i><span>Volver a facturación</span></button><button type="button" class="btn btn-primary quote-modal-close"><i data-lucide="x"></i><span>Cerrar</span></button>');
    }
    function renderBillingError(message) { billingTitle('Error facturación', 'Flujo vinculado'); billingContent().innerHTML = renderBillingSteps('detail') + '<div class="quote-billing-alert error">' + esc(message || 'Error no especificado') + '</div>'; setBillingActions('<button type="button" class="btn btn-outline quote-modal-close"><i data-lucide="x"></i><span>Cerrar</span></button>'); }
    async function postBilling(url, fields) { const fd = new FormData(); Object.keys(fields || {}).forEach(function (key) { fd.append(key, fields[key]); }); const response = await fetch(url, { method: 'POST', body: fd, credentials: 'same-origin', headers: { 'Accept': 'application/json' } }); const raw = await response.text(); let data = null; try { data = JSON.parse(raw); } catch (e) { throw new Error('Respuesta no JSON. HTTP ' + response.status + '. ' + raw.slice(0, 240)); } if (!response.ok || !data.success) throw new Error((data && data.message) ? data.message : ('Error HTTP ' + response.status)); return data; }

    const QUOTE_GUIDE_STATE = {
        step: 'prepare',
        environment: 'sandbox',
        source: null,
        guideId: 0,
        preview: null,
        draft: null
    };
    function quoteGuideContent() { return document.getElementById('quoteGuideContent'); }
    function quoteGuideActions() { return document.getElementById('quoteGuideActions'); }
    function quoteGuideTitle(title, subtitle) {
        const t = document.getElementById('quoteGuideTitle');
        const st = document.getElementById('quoteGuideSubtitle');
        if (t) t.textContent = title || 'Guía de despacho';
        if (st) st.textContent = subtitle || 'Emisión vinculada a cotización';
    }
    function setQuoteGuideActions(html) {
        const actions = quoteGuideActions();
        if (actions) actions.innerHTML = html || '';
        if (window.lucide) lucide.createIcons();
    }
    function renderQuoteGuideSteps(active) {
        const steps = [['prepare','1. Preparar'],['preview','2. Preview'],['confirm','3. Confirmar'],['result','4. Resultado']];
        return '<div class="quote-guide-stepbar">' + steps.map(function (step) {
            return '<div class="quote-guide-step ' + (step[0] === active ? 'is-active' : '') + '">' + esc(step[1]) + '</div>';
        }).join('') + '</div>';
    }
    function quoteGuideTypeOptions(selected) {
        const items = [[1,'Operación constituye venta'],[2,'Ventas por efectuar'],[3,'Consignación'],[4,'Entrega gratuita'],[5,'Traslado interno'],[6,'Otros traslados sin venta'],[7,'Devolución'],[8,'Traslado para exportación'],[9,'Venta para exportación']];
        return items.map(function (item) { return '<option value="' + item[0] + '"' + (Number(selected || 1) === item[0] ? ' selected' : '') + '>' + esc(item[1]) + '</option>'; }).join('');
    }
    function quoteGuideTransferOptions(selected) {
        const items = [[1,'Despacho por cuenta del receptor'],[2,'Despacho por cuenta del emisor a instalaciones del receptor'],[3,'Despacho por cuenta del emisor a otras instalaciones']];
        return items.map(function (item) { return '<option value="' + item[0] + '"' + (Number(selected || 2) === item[0] ? ' selected' : '') + '>' + esc(item[1]) + '</option>'; }).join('');
    }
    function quoteGuideQty(value) {
        const number = Number(value || 0);
        return Number.isInteger(number) ? String(number) : String(Math.round(number * 10000) / 10000);
    }
    function quoteGuideDraftFromForm() {
        const lines = [];
        document.querySelectorAll('#quoteGuideContent .quote-guide-line-enabled').forEach(function (checkbox) {
            if (!checkbox.checked) return;
            const index = Number(checkbox.dataset.index || -1);
            const quantityInput = document.querySelector('#quoteGuideContent .quote-guide-line-qty[data-index="' + index + '"]');
            const sourceLine = QUOTE_GUIDE_STATE.source && QUOTE_GUIDE_STATE.source.lines ? QUOTE_GUIDE_STATE.source.lines[index] : null;
            if (!sourceLine) return;
            const quantity = Number(quantityInput ? quantityInput.value : 0);
            if (quantity > 0) lines.push({ source_line_id: sourceLine.source_line_id, quantity: quantity });
        });
        return {
            issueDate: (document.getElementById('quoteGuideIssueDate') || {}).value || todayISO(),
            guideType: Number((document.getElementById('quoteGuideType') || {}).value || 1),
            transferType: Number((document.getElementById('quoteGuideTransferType') || {}).value || 2),
            address: ((document.getElementById('quoteGuideAddress') || {}).value || '').trim(),
            district: ((document.getElementById('quoteGuideDistrict') || {}).value || '').trim(),
            city: ((document.getElementById('quoteGuideCity') || {}).value || '').trim(),
            observations: ((document.getElementById('quoteGuideObservations') || {}).value || '').trim(),
            lines: lines
        };
    }
    function quoteGuideLineMap(draft) {
        const map = {};
        (draft && draft.lines ? draft.lines : []).forEach(function (line) { map[String(line.source_line_id)] = Number(line.quantity || 0); });
        return map;
    }
    function syncQuoteGuideTransferField() {
        const type = Number((document.getElementById('quoteGuideType') || {}).value || 1);
        const field = document.getElementById('quoteGuideTransferField');
        if (field) field.style.display = type === 5 ? 'none' : 'grid';
    }
    function renderQuoteGuideError(message, step) {
        QUOTE_GUIDE_STATE.step = step || QUOTE_GUIDE_STATE.step || 'prepare';
        quoteGuideTitle('Error guía de despacho', 'Flujo vinculado a cotización');
        const content = quoteGuideContent();
        if (content) content.innerHTML = renderQuoteGuideSteps(QUOTE_GUIDE_STATE.step) + '<div class="quote-billing-alert error"><strong>No fue posible continuar.</strong><br>' + esc(message || 'Error no especificado') + '</div>';
        setQuoteGuideActions('<button type="button" class="btn btn-outline quote-guide-return-quote"><i data-lucide="arrow-left"></i><span>Volver a cotización</span></button>');
    }
    function renderQuoteGuidePrepare(message) {
        const source = QUOTE_GUIDE_STATE.source;
        const q = quoteById(window.ICY_QUOTES_STATE.currentQuoteId);
        if (!source || !q) { renderQuoteGuideError('No se encontró la cotización de origen.', 'prepare'); return; }
        QUOTE_GUIDE_STATE.step = 'prepare';
        quoteGuideTitle('Guía de despacho', (q.quote_number || '') + ' · flujo vinculado');
        const draft = QUOTE_GUIDE_STATE.draft || {};
        const customer = source.customer || {};
        const lineMap = quoteGuideLineMap(draft);
        const lines = (source.lines || []).map(function (line, index) {
            const available = Number(line.available_quantity ?? line.source_quantity ?? 0);
            const disabled = available <= 0;
            const hasSaved = Object.prototype.hasOwnProperty.call(lineMap, String(line.source_line_id));
            const selectedQty = hasSaved ? lineMap[String(line.source_line_id)] : available;
            const checked = !disabled && (!QUOTE_GUIDE_STATE.draft || hasSaved);
            return '<tr>' +
                '<td><input type="checkbox" class="quote-guide-line-enabled" data-index="' + index + '" ' + (disabled ? 'disabled' : (checked ? 'checked' : '')) + '></td>' +
                '<td>' + esc(line.sku || '—') + '</td>' +
                '<td><strong>' + esc(line.description || 'Producto') + '</strong><div class="quote-guide-muted">' + esc(line.long_description || '') + '</div></td>' +
                '<td>' + esc(quoteGuideQty(line.original_quantity ?? line.source_quantity)) + '</td>' +
                '<td>' + esc(quoteGuideQty(line.dispatched_quantity || 0)) + '</td>' +
                '<td><strong>' + esc(quoteGuideQty(available)) + '</strong></td>' +
                '<td><input class="input quote-guide-line-qty" type="number" min="0" max="' + esc(available) + '" step="0.0001" value="' + esc(quoteGuideQty(selectedQty)) + '" data-index="' + index + '" ' + (disabled || !checked ? 'disabled' : '') + '></td>' +
                '<td>' + esc(line.unit_measure || 'UN') + '</td>' +
                '<td>' + money(line.unit_price) + '</td>' +
                '</tr>';
        }).join('');
        let html = renderQuoteGuideSteps('prepare');
        if (message) html += '<div class="quote-billing-alert success">' + esc(message) + '</div>';
        html += '<div id="quoteGuidePrepareError" class="quote-billing-alert error hidden"></div>';
        html += '<div class="quote-billing-alert warning"><strong>Guía sandbox vinculada.</strong><br>La emisión no modifica stock ni cobranza en esta fase.</div>';
        html += '<div class="quote-billing-grid">' +
            '<div class="quote-billing-card"><h3>Origen</h3><div class="quote-billing-grid">' +
                renderReadonly('Cotización', source.number || q.quote_number || '') +
                renderReadonly('Cliente', customer.legal_name || q.customer_name || '') +
                renderReadonly('RUT', customer.tax_id || q.customer_tax_id || '') +
                renderReadonly('Total cotizado', money(source.total_amount || q.total || 0)) +
            '</div></div>' +
            '<div class="quote-billing-card"><h3>Regla operativa</h3><p class="text-secondary">Puedes despachar total o parcialmente. La cantidad no puede superar el saldo disponible del origen.</p><p class="text-secondary">Para corregir cliente o productos, vuelve a la cotización antes de emitir.</p></div>' +
        '</div>';
        html += '<div class="quote-billing-card"><h3>Datos de la guía</h3><div class="quote-guide-destination">' +
            '<label><span class="label">Fecha emisión</span><input class="input" id="quoteGuideIssueDate" type="date" value="' + esc(draft.issueDate || todayISO()) + '"></label>' +
            '<label><span class="label">Tipo de guía</span><select class="input" id="quoteGuideType">' + quoteGuideTypeOptions(draft.guideType || 1) + '</select></label>' +
            '<label id="quoteGuideTransferField"><span class="label">Tipo de traslado</span><select class="input" id="quoteGuideTransferType">' + quoteGuideTransferOptions(draft.transferType || 2) + '</select></label>' +
            '<label><span class="label">Dirección destino</span><input class="input" id="quoteGuideAddress" value="' + esc(draft.address ?? customer.address ?? '') + '"></label>' +
            '<label><span class="label">Comuna destino</span><input class="input" id="quoteGuideDistrict" value="' + esc(draft.district ?? customer.district ?? '') + '"></label>' +
            '<label><span class="label">Ciudad destino</span><input class="input" id="quoteGuideCity" value="' + esc(draft.city ?? customer.city ?? '') + '"></label>' +
        '</div><label class="quote-guide-field mt-3"><span class="label">Observaciones</span><textarea class="input" id="quoteGuideObservations" maxlength="220" rows="3" placeholder="Datos de despacho o indicaciones internas">' + esc(draft.observations || '') + '</textarea></label></div>';
        html += '<div class="quote-billing-card"><h3>Detalle a despachar</h3><div class="quote-guide-table-wrap"><table class="quote-guide-table"><thead><tr><th></th><th>SKU</th><th>Producto</th><th class="crm-mobile-hidden">Origen</th><th>Ya despachado</th><th>Disponible</th><th>Despachar</th><th>Unidad</th><th>Prc. unit.</th></tr></thead><tbody>' + (lines || '<tr><td colspan="9">Sin líneas disponibles.</td></tr>') + '</tbody></table></div></div>';
        quoteGuideContent().innerHTML = html;
        setQuoteGuideActions('<button type="button" class="btn btn-primary quote-guide-preview"><i data-lucide="file-search"></i><span>Previsualizar</span></button>');
        syncQuoteGuideTransferField();
    }
    function renderQuoteGuidePreview(data) {
        const q = quoteById(window.ICY_QUOTES_STATE.currentQuoteId);
        QUOTE_GUIDE_STATE.step = 'preview';
        quoteGuideTitle('Preview FACTO sandbox', (q ? q.quote_number : '') + ' · revisión PDF');
        const preview = data.preview || {};
        const source = data.source || {};
        let html = renderQuoteGuideSteps('preview');
        html += '<div class="quote-billing-alert warning"><strong>VISTA PREVIA — NO VÁLIDA TRIBUTARIAMENTE.</strong><br>No se envía al SII, no modifica stock y no genera cuentas por cobrar.</div>';
        html += '<div class="quote-billing-alert success"><strong>Preview verificable.</strong><br>FACTO devolvió document_id temporal y PDF.</div>';
        html += '<div class="quote-billing-grid three">' +
            renderReadonly('Líneas', data.line_count || 0) +
            renderReadonly('Total referencial', money(data.amounts ? data.amounts.total_amount : 0)) +
            renderReadonly('Tipo de guía', data.guide_type_label || '') +
        '</div>';
        html += '<div class="quote-billing-grid">' +
            '<div class="quote-billing-card"><h3>Referencia</h3><p><strong>Cotización:</strong> ' + esc(source.number || (q ? q.quote_number : '')) + '</p><p><strong>Destino:</strong> ' + esc((data.destination || {}).address || '') + '</p><p>' + esc((data.destination || {}).district || '') + ' · ' + esc((data.destination || {}).city || '') + '</p></div>' +
            '<div class="quote-billing-card"><h3>Preview FACTO</h3><p><strong>document_id temporal:</strong> ' + esc(preview.document_id || '—') + '</p><p><strong>Referencia temporal:</strong> ' + esc(preview.document_number || '—') + '</p><p><strong>PDF:</strong> ' + (preview.pdf_available ? 'Disponible' : 'No disponible') + '</p></div>' +
        '</div>';
        if (preview.pdf_available) html += '<div class="quote-billing-card"><h3>PDF preliminar</h3><iframe class="quote-guide-preview-frame" src="/modules/billing/dispatch_guide_file.php?guide_id=' + Number(data.guide_id) + '&kind=pdf&stage=preview"></iframe></div>';
        quoteGuideContent().innerHTML = html;
        setQuoteGuideActions('<button type="button" class="btn btn-primary quote-guide-confirm-layer"><i data-lucide="arrow-right"></i><span>Continuar</span></button>');
    }
    function renderQuoteGuideConfirm() {
        const data = QUOTE_GUIDE_STATE.preview || {};
        const q = quoteById(window.ICY_QUOTES_STATE.currentQuoteId);
        QUOTE_GUIDE_STATE.step = 'confirm';
        quoteGuideTitle('Confirmar emisión sandbox', (q ? q.quote_number : '') + ' · requiere EMITIR');
        let html = renderQuoteGuideSteps('confirm');
        html += '<div id="quoteGuideConfirmError" class="quote-billing-alert error hidden"></div>';
        html += '<div class="quote-billing-alert warning"><strong>Operación tributaria irreversible.</strong><br>Se emitirá una guía definitiva en FACTO sandbox. No se aplicará efecto de stock en esta fase.</div>';
        html += '<div class="quote-billing-grid three">' +
            renderReadonly('Origen', (data.source || {}).number || (q ? q.quote_number : '')) +
            renderReadonly('Líneas', data.line_count || 0) +
            renderReadonly('Total', money(data.amounts ? data.amounts.total_amount : 0)) +
        '</div>';
        html += '<div class="quote-billing-card quote-guide-confirm"><label class="quote-guide-field"><span class="label">Confirmación</span><input class="input" id="quoteGuideIssueConfirmation" autocomplete="off" placeholder="Escribe EMITIR"></label></div>';
        quoteGuideContent().innerHTML = html;
        setQuoteGuideActions('<button type="button" class="btn btn-danger quote-guide-confirm-issue" disabled><i data-lucide="send"></i><span>Confirmar guía sandbox</span></button>');
    }
    function updateQuoteGuideConfirmButton() {
        const input = document.getElementById('quoteGuideIssueConfirmation');
        const button = document.querySelector('#quoteGuideActions .quote-guide-confirm-issue');
        if (button) button.disabled = !input || input.value.trim().toUpperCase() !== 'EMITIR';
    }
    function renderQuoteGuideResult(data) {
        const q = quoteById(window.ICY_QUOTES_STATE.currentQuoteId);
        QUOTE_GUIDE_STATE.step = 'result';
        quoteGuideTitle(data.status === 'issued' ? 'Guía emitida' : 'Resultado guía', data.document_number ? ('Folio ' + data.document_number) : ((q ? q.quote_number : '') + ' · resultado'));
        let html = renderQuoteGuideSteps('result');
        html += '<div class="quote-billing-alert ' + (data.status === 'issued' ? 'success' : 'warning') + '"><strong>' + (data.status === 'issued' ? 'Guía emitida y registrada correctamente.' : 'La guía requiere revisión.') + '</strong></div>';
        html += '<div class="quote-billing-grid three">' +
            renderReadonly('Estado', data.status || '') +
            renderReadonly('FACTO document_id', data.facto_document_id || '—') +
            renderReadonly('Folio definitivo', data.document_number || '—') +
        '</div>';
        html += '<div class="quote-billing-card"><h3>Archivos</h3><div class="quote-guide-file-actions">' +
            (data.pdf_available ? '<a class="btn btn-outline" target="_blank" rel="noopener" href="/modules/billing/dispatch_guide_file.php?guide_id=' + Number(data.guide_id) + '&kind=pdf&stage=issued"><i data-lucide="file-text"></i><span>Ver PDF</span></a>' : '') +
            (data.xml_available ? '<a class="btn btn-outline" data-no-spa="true" href="/modules/billing/dispatch_guide_file.php?guide_id=' + Number(data.guide_id) + '&kind=xml&stage=issued"><i data-lucide="download"></i><span>Descargar XML</span></a>' : '') +
            (data.transfer_copy_pdf_available ? '<a class="btn btn-outline" target="_blank" rel="noopener" href="/modules/billing/dispatch_guide_file.php?guide_id=' + Number(data.guide_id) + '&kind=transfer_pdf&stage=issued"><i data-lucide="copy"></i><span>Ver copia cedible</span></a>' : '') +
        '</div></div>';
        if (data.error_message) html += '<div class="quote-billing-alert error">' + esc(data.error_message) + '</div>';
        quoteGuideContent().innerHTML = html;
        setQuoteGuideActions('<button type="button" class="btn btn-primary quote-guide-return-quote"><i data-lucide="arrow-left"></i><span>Volver a cotización</span></button>');
    }
    async function loadQuoteGuideSource() {
        const q = quoteById(window.ICY_QUOTES_STATE.currentQuoteId);
        if (!q) return;
        quoteGuideTitle('Guía de despacho', (q.quote_number || '') + ' · cargando origen');
        quoteGuideContent().innerHTML = renderQuoteGuideSteps('prepare') + '<div class="quote-billing-card"><p class="text-secondary">Cargando cotización y cantidades disponibles...</p></div>';
        setQuoteGuideActions('');
        try {
            const response = await fetch('/modules/billing/dispatch_guide_source_ajax.php?source_type=quote&source_id=' + encodeURIComponent(q.id) + '&environment=sandbox', { credentials: 'same-origin', headers: { 'Accept': 'application/json' } });
            const raw = await response.text();
            let payload = null;
            try { payload = JSON.parse(raw); } catch (e) { throw new Error('Respuesta no JSON. HTTP ' + response.status + '. ' + raw.slice(0, 220)); }
            if (!response.ok || !payload || payload.success === false) throw new Error(payload && payload.message ? payload.message : ('Error HTTP ' + response.status));
            QUOTE_GUIDE_STATE.source = payload.source;
            renderQuoteGuidePrepare();
        } catch (error) {
            renderQuoteGuideError(error.message || 'No fue posible cargar la cotización.', 'prepare');
        }
    }
    window.openQuoteDispatchGuide = function (id) {
        const q = quoteById(id || window.ICY_QUOTES_STATE.currentQuoteId);
        if (!q) return;
        window.ICY_QUOTES_STATE.currentQuoteId = Number(q.id);
        QUOTE_GUIDE_STATE.step = 'prepare';
        QUOTE_GUIDE_STATE.environment = 'sandbox';
        QUOTE_GUIDE_STATE.source = null;
        QUOTE_GUIDE_STATE.guideId = 0;
        QUOTE_GUIDE_STATE.preview = null;
        QUOTE_GUIDE_STATE.draft = null;
        setLayer('quoteLayerDispatchGuide');
        openModal();
        loadQuoteGuideSource();
    };
    async function previewQuoteGuide(trigger) {
        const q = quoteById(window.ICY_QUOTES_STATE.currentQuoteId);
        if (!q || !QUOTE_GUIDE_STATE.source) return;
        const draft = quoteGuideDraftFromForm();
        QUOTE_GUIDE_STATE.draft = draft;
        const errorBox = document.getElementById('quoteGuidePrepareError');
        if (!draft.lines.length) {
            if (errorBox) { errorBox.textContent = 'Debes seleccionar al menos una línea con cantidad mayor a cero.'; errorBox.classList.remove('hidden'); }
            return;
        }
        if (trigger) { trigger.disabled = true; trigger.innerHTML = '<i data-lucide="loader" class="spin-icon"></i><span>Generando...</span>'; if (window.lucide) lucide.createIcons(); }
        try {
            const payload = await postBilling('/modules/billing/dispatch_guide_preview_ajax.php', {
                environment: 'sandbox', source_type: 'quote', source_id: q.id,
                issue_date: draft.issueDate, guide_type: draft.guideType, transfer_type: draft.transferType,
                dispatch_address: draft.address, dispatch_district: draft.district, dispatch_city: draft.city,
                observations: draft.observations, lines_json: JSON.stringify(draft.lines)
            });
            const data = payload.data || payload.result || payload;
            QUOTE_GUIDE_STATE.preview = data;
            QUOTE_GUIDE_STATE.guideId = Number(data.guide_id || 0);
            renderQuoteGuidePreview(data);
        } catch (error) {
            if (errorBox) { errorBox.textContent = error.message || 'No fue posible generar el preview.'; errorBox.classList.remove('hidden'); }
            else renderQuoteGuideError(error.message || 'No fue posible generar el preview.', 'prepare');
        } finally {
            if (trigger && document.body.contains(trigger)) { trigger.disabled = false; trigger.innerHTML = '<i data-lucide="file-search"></i><span>Previsualizar</span>'; if (window.lucide) lucide.createIcons(); }
        }
    }
    async function issueQuoteGuide(trigger) {
        const errorBox = document.getElementById('quoteGuideConfirmError');
        const confirmation = (document.getElementById('quoteGuideIssueConfirmation') || {}).value || '';
        if (trigger) { trigger.disabled = true; trigger.innerHTML = '<i data-lucide="loader" class="spin-icon"></i><span>Emitiendo...</span>'; if (window.lucide) lucide.createIcons(); }
        try {
            const payload = await postBilling('/modules/billing/dispatch_guide_issue_ajax.php', { guide_id: QUOTE_GUIDE_STATE.guideId, confirmation: confirmation });
            const data = payload.data || payload.result || payload;
            renderQuoteGuideResult(data);
        } catch (error) {
            if (errorBox) { errorBox.textContent = error.message || 'No fue posible emitir la guía.'; errorBox.classList.remove('hidden'); }
            if (trigger) { trigger.disabled = false; trigger.innerHTML = '<i data-lucide="send"></i><span>Confirmar guía sandbox</span>'; if (window.lucide) lucide.createIcons(); }
        }
    }
    function quoteGuideBack() {
        if (QUOTE_GUIDE_STATE.step === 'preview') { renderQuoteGuidePrepare(); return; }
        if (QUOTE_GUIDE_STATE.step === 'confirm') { renderQuoteGuidePreview(QUOTE_GUIDE_STATE.preview || {}); return; }
        const qid = window.ICY_QUOTES_STATE.currentQuoteId;
        if (qid) window.openQuotePreview(qid); else setLayer('quoteLayerPreview');
    }

    window.openQuoteBilling = function (id) { const q = quoteById(id || window.ICY_QUOTES_STATE.currentQuoteId); if (!q) return; window.ICY_QUOTES_STATE.currentQuoteId = Number(q.id); QUOTE_BILLING_STATE.readiness = null; QUOTE_BILLING_STATE.readinessKey = ''; setLayer('quoteLayerBilling'); openModal(); renderBillingPrepare(q); if (!String(q.billing_recovery_document_id || '').trim()) refreshQuoteBillingReadiness(q); };
    async function saveQuoteBillingDraft(trigger) {
        const q = quoteById(window.ICY_QUOTES_STATE.currentQuoteId);
        if (!q) return;
        const cfg = billingConfig();
        if (trigger) {
            trigger.disabled = true;
            trigger.innerHTML = '<i data-lucide="loader" class="spin-icon"></i><span>Guardando...</span>';
            if (window.lucide) lucide.createIcons();
        }
        try {
            const data = await postBilling('/modules/billing/save_draft_ajax.php', billingRequestPayload(q));
            const result = data && data.result ? data.result : null;
            const draft = result && result.draft ? result.draft : {};
            q.billing_draft_log_id = draft.log_id || q.billing_draft_log_id || 0;
            q.billing_draft_document_key = draft.document_type_key || cfg.documentKey;
            q.billing_draft_environment = draft.environment || cfg.environment;
            q.billing_draft_mode = draft.mode || 'dry_run';
            q.billing_draft_status = draft.status || 'draft';
            q.billing_draft_issue_date = cfg.issueDate;
            q.billing_draft_payment_terms = cfg.paymentTerms;
            q.billing_draft_error_message = (draft.errors && draft.errors.length) ? draft.errors.join(' | ') : '';
            renderBillingPrepare(q, (result && result.message) ? result.message : 'Borrador tributario guardado.');
        } catch (err) {
            renderBillingError(err.message || 'No fue posible guardar el borrador tributario.');
        }
    }
    async function validateQuoteBilling(trigger) { const q = quoteById(window.ICY_QUOTES_STATE.currentQuoteId); if (!q) return; if (trigger) { trigger.disabled = true; trigger.innerHTML = '<i data-lucide="loader" class="spin-icon"></i><span>Validando...</span>'; if (window.lucide) lucide.createIcons(); } try { const data = await postBilling('/modules/billing/validate_issue_ajax.php', billingRequestPayload(q)); QUOTE_BILLING_STATE.validation=data; renderValidationResult(q, data); } catch (err) { renderBillingError(err.message || 'No fue posible validar.'); } }
    async function previewQuoteBilling(trigger) { const q = quoteById(window.ICY_QUOTES_STATE.currentQuoteId); if (!q) return; if (trigger) { trigger.disabled = true; trigger.innerHTML = '<i data-lucide="loader" class="spin-icon"></i><span>Generando...</span>'; if (window.lucide) lucide.createIcons(); } try { const data = await postBilling('/modules/billing/preview_issue_ajax.php', billingRequestPayload(q)); QUOTE_BILLING_STATE.preview=data; renderBillingPreview(q, data); } catch (err) { renderBillingError(err.message || 'No fue posible generar preview.'); } }
    function selectedOptionData(selectId) {
        const select = document.getElementById(selectId);
        const option = select && select.selectedOptions && select.selectedOptions.length ? select.selectedOptions[0] : null;
        return {
            value: select ? (select.value || '') : '',
            key: option ? (option.dataset.key || '') : '',
            label: option ? (option.dataset.label || option.textContent || '') : ''
        };
    }
    async function confirmQuoteBilling(trigger) {
        const q = quoteById(window.ICY_QUOTES_STATE.currentQuoteId);
        if (!q) return;
        const cfg = billingConfig();
        const confirmation = (document.getElementById('quoteBillingIssueConfirmation') || {}).value || '';
        if (confirmation.trim().toUpperCase() !== billingConfirmationText()) { updateIssueConfirmButton(); return; }
        const markPaid = document.getElementById('quoteBillingMarkAsPaid');
        const paymentDateValue = (document.getElementById('quoteBillingPaymentDate') || {}).value || '';
        const paymentMethod = selectedOptionData('quoteBillingPaymentMethod');
        const cashAccount = selectedOptionData('quoteBillingCashAccount');
        if (markPaid && markPaid.checked && (!paymentMethod.value || !cashAccount.value || !paymentDateValue)) { updateIssueConfirmButton(); return; }
        if (trigger) { trigger.disabled = true; trigger.innerHTML = '<i data-lucide="loader" class="spin-icon"></i><span>Emitiendo...</span>'; if (window.lucide) lucide.createIcons(); }
        try {
            const data = await postBilling('/modules/billing/confirm_issue_ajax.php', Object.assign(billingRequestPayload(q), {
                confirmation: confirmation,
                mark_as_paid: markPaid && markPaid.checked ? '1' : '0',
                payment_date: paymentDateValue,
                payment_method_id: /^[0-9]+$/.test(paymentMethod.value) ? paymentMethod.value : '',
                payment_method: paymentMethod.key || paymentMethod.value,
                payment_method_label: paymentMethod.label,
                cash_account_id: /^[0-9]+$/.test(cashAccount.value) ? cashAccount.value : '',
                cash_account_key: cashAccount.key || cashAccount.value,
                cash_account_label: cashAccount.label,
                payment_reference: (document.getElementById('quoteBillingPaymentReference') || {}).value || '',
                payment_notes: (document.getElementById('quoteBillingPaymentNotes') || {}).value || ''
            }));
            renderIssueResult(q, data);
        } catch (err) { renderBillingError(err.message || 'No fue posible emitir.'); }
    }
    async function reconcileQuoteBilling(trigger) {
        const q = quoteById(window.ICY_QUOTES_STATE.currentQuoteId);
        if (!q || !String(q.billing_recovery_document_id || '').trim()) return;
        if (!confirm('Se consultará y vinculará el documento FACTO existente ' + q.billing_recovery_document_id + '. No se emitirá una nueva factura. ¿Continuar?')) return;
        if (trigger) {
            trigger.disabled = true;
            trigger.innerHTML = '<i data-lucide="loader" class="spin-icon"></i><span>Conciliando...</span>';
            if (window.lucide) lucide.createIcons();
        }
        try {
            const data = await postBilling('/modules/billing/receivable_repair_origin_ajax.php', {
                id: 'document_id:' + q.billing_recovery_document_id,
                quote_number: q.quote_number
            });
            if (data && data.result && data.result.ok) q.billing_recovery_document_id = '';
            renderIssueResult(q, data);
        } catch (err) {
            renderBillingError(err.message || 'No fue posible conciliar el documento existente. No se emitió una nueva factura.');
        }
    }
    async function sendBillingDocumentEmail(trigger) {
        const q = quoteById(window.ICY_QUOTES_STATE.currentQuoteId);
        if (!q) return;
        const to = (document.getElementById('quoteBillingEmailTo') || {}).value || '';
        const subject = (document.getElementById('quoteBillingEmailSubject') || {}).value || '';
        const body = (document.getElementById('quoteBillingEmailBody') || {}).value || '';
        const attachPdf = document.getElementById('quoteBillingAttachPdf');
        const attachXml = document.getElementById('quoteBillingAttachXml');
        if (!String(to).trim()) {
            renderBillingEmail(q, 'Indica un email destinatario antes de enviar.');
            return;
        }
        if ((!attachPdf || !attachPdf.checked) && (!attachXml || !attachXml.checked)) {
            renderBillingEmail(q, 'Selecciona al menos PDF o XML para adjuntar.');
            return;
        }
        if (trigger) {
            trigger.disabled = true;
            trigger.innerHTML = '<i data-lucide="loader" class="spin-icon"></i><span>Enviando...</span>';
            if (window.lucide) lucide.createIcons();
        }
        try {
            const data = await postBilling('/modules/billing/send_document_email_ajax.php', {
                quote_id: q.id,
                log_id: q.billing_issue_log_id || 0,
                recipient_email: to,
                cc_email: (document.getElementById('quoteBillingEmailCc') || {}).value || '',
                subject: subject,
                body: body.replace(/\n/g, '<br>'),
                attach_pdf: attachPdf && attachPdf.checked ? '1' : '0',
                attach_xml: attachXml && attachXml.checked ? '1' : '0'
            });
            renderBillingEmailResult(q, data);
        } catch (err) {
            renderBillingError(err.message || 'No fue posible enviar el documento por correo.');
        }
    }
    window.openQuoteCreate = function () { clearForm(); window.ICY_QUOTES_STATE.currentQuoteId = 0; window.ICY_QUOTES_STATE.formMode = 'create'; document.getElementById('quoteFormTitle').textContent = 'Nueva Cotización'; document.getElementById('quoteFormSubtitle').textContent = 'Completa cliente, productos y condiciones'; document.getElementById('quoteSaveBtn').querySelector('span').innerHTML = 'Crear&nbsp;<span class="mobile-hidden">cotización</span>'; const deleteBtn = document.getElementById('quoteFormDeleteBtn'); const cancelBtn = document.getElementById('quoteFormCancelBtn'); if (deleteBtn) { deleteBtn.classList.add('hidden'); deleteBtn.dataset.quoteId = '0'; } if (cancelBtn) cancelBtn.classList.remove('hidden'); setLayer('quoteLayerForm'); openModal(); };
    window.openQuoteEdit = function (id) { const q = quoteById(id || window.ICY_QUOTES_STATE.currentQuoteId); if (!q) return; if (q.is_deleted) { alert('Esta cotización está eliminada. Restáurala antes de editar.'); return; } if (isSystemManagedStatus(q.status)) { alert('Esta cotización ya está vinculada al ciclo de facturación. Sus fechas y estado financiero se administran desde Facturación y Cuentas por Cobrar.'); return; } clearForm(); window.ICY_QUOTES_STATE.currentQuoteId = Number(q.id); window.ICY_QUOTES_STATE.formMode = 'edit'; const deleteBtn = document.getElementById('quoteFormDeleteBtn'); const cancelBtn = document.getElementById('quoteFormCancelBtn'); if (deleteBtn) { deleteBtn.classList.remove('hidden'); deleteBtn.dataset.quoteId = q.id; } if (cancelBtn) cancelBtn.classList.remove('hidden'); document.getElementById('quoteFormTitle').textContent = 'Editar Cotización'; document.getElementById('quoteFormSubtitle').textContent = `${q.quote_number} · ${q.customer_display_name || q.customer_name}`; document.getElementById('quoteSaveBtn').querySelector('span').innerHTML = 'Guardar&nbsp;<span class="mobile-hidden">cambios</span>'; document.getElementById('formQuoteId').value = q.id; document.getElementById('formCustomerId').value = q.customer_id; document.getElementById('formOpportunityId').value = q.opportunity_id || 0; document.getElementById('opportunitySelect').value = q.opportunity_id || 0; document.getElementById('opportunitySelect').disabled = true; document.getElementById('formStatus').value = q.status || 'Borrador'; document.getElementById('formDiscount').value = q.discount_percent || 0; document.getElementById('formNotes').value = q.notes || ''; setCustomerChip({ id: q.customer_id, display_name: q.customer_display_name || q.customer_name, customer_tax_id: q.customer_tax_id }, true, 'Cliente original de la cotización'); applyCustomerLock(true); (q.items || []).forEach(function (item) { addItem({ product_id: item.product_id || 'manual', name: item.display_name || String(item.name || '').replace(/\s*\(SKU:\s*[^)]+\)\s*$/i, ''), sku: item.sku || '', quantity: item.quantity || 1, unit: item.unit || 'unidad', price_unit: item.price_unit || 0 }); }); calculateQuote(); setLayer('quoteLayerForm'); openModal(); };
    function addItem(data) { data = data || {}; const idx = window.ICY_QUOTES_STATE.itemIndex++; const row = document.createElement('div'); row.className = 'eq-row'; row.innerHTML = `<input type="hidden" name="items[${idx}][product_id]" value="${esc(data.product_id || 'manual')}"><div class="eq-col-prod"><span class="mobile-label">Producto</span><input class="input it-name" name="items[${idx}][name]" value="${esc(data.name || '')}" placeholder="Producto" required></div><div class="eq-col-sku"><span class="mobile-label">SKU</span><input class="input" name="items[${idx}][sku]" value="${esc(data.sku || '')}" placeholder="SKU"></div><div class="eq-col-qty"><span class="mobile-label">Cant.</span><input class="input it-qty" type="number" min="0.001" step="0.001" inputmode="decimal" name="items[${idx}][quantity]" value="${esc(data.quantity || 1)}"></div><div class="eq-col-unit"><span class="mobile-label">Unidad</span>${unitSelectHtml(`items[${idx}][unit]`, data.unit || 'unidad')}</div><div class="eq-col-price"><span class="mobile-label">Unitario</span><input class="input it-price" type="number" min="0" step="0.01" name="items[${idx}][price_unit]" value="${esc(data.price_unit || 0)}"></div><div class="eq-col-total"><span class="mobile-label">Total</span><div class="it-total-text">$0</div></div><div class="eq-col-del"><button type="button" class="btn btn-outline remove-row-btn" title="Quitar producto"><i data-lucide="trash-2"></i></button></div>`; document.getElementById('quoteItems').appendChild(row); calculateQuote(); if (window.lucide) lucide.createIcons(); }
    function calculateQuote() { let subtotal = 0; document.querySelectorAll('#quoteItems .eq-row').forEach(function (row) { const qty = Number(row.querySelector('.it-qty').value || 0); const price = Number(row.querySelector('.it-price').value || 0); const total = qty * price; subtotal += total; row.querySelector('.it-total-text').textContent = money(total); }); const discountPercent = Number(document.getElementById('formDiscount').value || 0); const discountAmount = subtotal * (discountPercent / 100); const net = subtotal - discountAmount; const iva = net * 0.19; const total = net + iva; document.getElementById('sumSubtotal').textContent = money(subtotal); document.getElementById('sumNet').textContent = money(net); document.getElementById('sumIva').textContent = money(iva); document.getElementById('sumTotal').textContent = money(total); }
    function rowHtml(q) {
        const haystack = q.search_text || [q.quote_number, q.customer_name, q.customer_base_name, q.customer_commercial_name, q.customer_legal_name, q.customer_tax_id, q.email, q.phone, q.opportunity_title, q.status, q.invoice_number].join(' ').toLowerCase();
        const fullFolio = esc(q.quote_number || '');
        const mobileFolio = esc(compactQuoteNumber(q.quote_number || ''));
        const customerName = q.customer_display_name || q.customer_commercial_name || q.customer_name || '';
        const origin = q.opportunity_title || 'Venta directa';
        const createdRaw = String(q.created_at_raw || '').slice(0, 10);

        return `<tr class="quote-row" data-quote-id="${q.id}" data-search="${esc(haystack)}" data-status="${esc(q.status)}" data-created-date="${esc(createdRaw)}">
            <td data-label="Folio" class="crm-mobile-primary" data-sort-value="${fullFolio}">
                <button type="button" class="quote-link quote-open-preview" data-quote-id="${q.id}" title="${fullFolio}">
                    <span class="quote-folio-full">${fullFolio}</span>
                    <span class="quote-folio-mobile">${mobileFolio}</span>
                </button>
            </td>
            <td data-label="Cliente" class="crm-mobile-primary" data-sort-value="${esc(customerName)}"><div class="quote-client-cell"><strong>${esc(customerName)}</strong><span>${esc(q.customer_tax_id || '')}</span></div></td>
            <td data-label="Origen" class="crm-mobile-hidden" data-sort-value="${esc(origin)}"><div class="quote-origin-cell"><span>${esc(origin)}</span></div></td>
            <td data-label="Estado" data-sort-value="${esc(q.status||'')}">${statusSelectHtml(q)}</td>
            <td data-label="Total" class="crm-mobile-hidden" data-sort-value="${Number(q.total||0)}"><strong class="metric-value">${money(q.total)}</strong></td>
            <td data-label="Fecha" class="crm-mobile-hidden" data-sort-value="${esc(createdRaw)}">${esc(q.created_at || '-')}</td>
            <td data-label="Acción" class="actions-cell-align crm-ui-action-cell"><div class="actions-btn-group"><button type="button" class="btn btn-sm btn-outline action-icon-btn quote-open-preview crm-ui-action-btn" data-quote-id="${q.id}" title="Ver cotización"><i data-lucide="eye"></i></button>${q.is_deleted ? `<form method="POST" action="/modules/crm/quote_restore.php" data-no-spa="true" class="quote-restore-inline-form" style="margin:0;"><input type="hidden" name="id" value="${q.id}"><input type="hidden" name="redirect" value="/modules/crm/quotes.php?view=deleted"><button type="submit" class="btn btn-sm btn-outline action-icon-btn quote-restore-btn" title="Restaurar cotización"><i data-lucide="rotate-ccw"></i></button></form>` : ''}</div></td>
        </tr>`;
    }
    function renderTable() { const tbody = document.getElementById('quotesTableBody'); if (!tbody) return; const data = window.ICY_QUOTES_DATA || []; if (!data.length) tbody.innerHTML = '<tr class="empty-state-row"><td colspan="7" class="text-muted"><?php echo $showDeletedQuotes ? 'No hay cotizaciones eliminadas.' : 'No hay cotizaciones registradas.'; ?></td></tr>'; else tbody.innerHTML = data.map(rowHtml).join(''); renderKpis(); filterQuotes(); if (window.lucide) lucide.createIcons(); }
    function renderKpis() { const data = window.ICY_QUOTES_DATA || []; const total = data.reduce(function (sum, q) { return sum + Number(q.total || 0); }, 0); const accepted = data.filter(function (q) { return q.status === 'Aceptada'; }).reduce(function (sum, q) { return sum + Number(q.total || 0); }, 0); const sent = data.filter(function (q) { return q.status === 'Enviada'; }).reduce(function (sum, q) { return sum + Number(q.total || 0); }, 0); if (document.getElementById('kpiCount')) document.getElementById('kpiCount').textContent = data.length; if (document.getElementById('kpiTotal')) document.getElementById('kpiTotal').textContent = money(total); if (document.getElementById('kpiAccepted')) document.getElementById('kpiAccepted').textContent = money(accepted); if (document.getElementById('kpiSent')) document.getElementById('kpiSent').textContent = money(sent); }
    function normalizeQuoteSearch(value) {
        if (window.ICY_NORMALIZE_SEARCH) return window.ICY_NORMALIZE_SEARCH(value);
        return String(value || '').toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').trim();
    }
    function normalizeQuoteRut(value) {
        if (window.ICY_NORMALIZE_RUT_SEARCH) return window.ICY_NORMALIZE_RUT_SEARCH(value);
        return normalizeQuoteSearch(value).replace(/[^0-9k]/g, '');
    }
    function filterQuotes() {
        const rawQuery = document.getElementById('quoteSearch')?.value || '';
        const query = normalizeQuoteSearch(rawQuery);
        const rutQuery = normalizeQuoteRut(rawQuery);
        const status = document.getElementById('quoteStatusFilter')?.value || '';
        const from = document.getElementById('quoteCreatedFrom')?.value || '';
        const to = document.getElementById('quoteCreatedTo')?.value || '';
        let visibleCount = 0;

        document.querySelectorAll('#quotesTableBody .quote-row').forEach(function (row) {
            const createdDate = row.dataset.createdDate || '';
            const rawHaystack = row.dataset.search || '';
            const haystack = normalizeQuoteSearch(rawHaystack);
            const rutHaystack = normalizeQuoteRut(rawHaystack);
            const matchesText = !query || haystack.indexOf(query) !== -1 || (rutQuery.length >= 2 && rutHaystack.indexOf(rutQuery) !== -1);
            const visible = matchesText
                && (!status || row.dataset.status === status)
                && (!from || (createdDate && createdDate >= from))
                && (!to || (createdDate && createdDate <= to));

            row.classList.toggle('is-filter-hidden', !visible);
            row.hidden = !visible;
            row.style.display = visible ? '' : 'none';
            if (visible) visibleCount++;
        });

        const tbody = document.getElementById('quotesTableBody');
        const oldEmpty = document.getElementById('quoteFilterEmptyRow');
        if (oldEmpty) oldEmpty.remove();
        if (tbody && visibleCount === 0 && (window.ICY_QUOTES_DATA || []).length > 0) {
            tbody.insertAdjacentHTML('beforeend', '<tr id="quoteFilterEmptyRow"><td colspan="7" class="text-muted">Sin resultados para el filtro aplicado.</td></tr>');
        }
    }
    function syncQuoteDatePlaceholders() {
        ['quoteCreatedFrom', 'quoteCreatedTo'].forEach(function (id) {
            const input = document.getElementById(id);
            if (!input) return;

            const field = input.closest('.quote-date-filter-field');
            if (!field) return;

            field.classList.toggle('has-value', !!input.value);
        });
    }

    function bindQuoteFiltersDirect() {
        ['quoteSearch', 'quoteCreatedFrom', 'quoteCreatedTo'].forEach(function (id) {
            const el = document.getElementById(id);
            if (!el || el.dataset.quoteFilterBound === '1') return;
            el.dataset.quoteFilterBound = '1';
            el.addEventListener('input', function () {
                if (id === 'quoteCreatedFrom' || id === 'quoteCreatedTo') syncQuoteDatePlaceholders();
                filterQuotes();
            }, { passive: true });
            el.addEventListener('change', function () {
                if (id === 'quoteCreatedFrom' || id === 'quoteCreatedTo') syncQuoteDatePlaceholders();
                filterQuotes();
            }, { passive: true });
        });
        const statusEl = document.getElementById('quoteStatusFilter');
        if (statusEl && statusEl.dataset.quoteFilterBound !== '1') {
            statusEl.dataset.quoteFilterBound = '1';
            statusEl.addEventListener('change', filterQuotes, { passive: true });
            statusEl.addEventListener('input', filterQuotes, { passive: true });
        }
        syncQuoteDatePlaceholders();
    }

    function upsertQuote(q) { const data = window.ICY_QUOTES_DATA || []; const idx = data.findIndex(function (item) { return Number(item.id) === Number(q.id); }); if (idx >= 0) data[idx] = q; else data.unshift(q); window.ICY_QUOTES_DATA = data; }
    function removeQuoteFromState(id) { window.ICY_QUOTES_DATA = (window.ICY_QUOTES_DATA || []).filter(function (q) { return Number(q.id) !== Number(id); }); }

    // Compatibilidad con el listener histórico: las fechas ya no pertenecen al formulario.
    function toggleInvoiceFields() { return; }

    function updateQuickStatus(select) { const id = Number(select.dataset.quoteId || 0); const previous = select.dataset.current || ''; const status = select.value || 'Borrador'; if (!id) return; select.disabled = true; const form = new FormData(); form.append('id', id); form.append('status', status); fetch('/modules/crm/quote_status_ajax.php', { method: 'POST', body: form, credentials: 'same-origin' }).then(function (r) { return r.json(); }).then(function (res) { if (!res || !res.success) throw new Error((res && (res.message || res.error)) ? (res.message || res.error) : 'No se pudo actualizar el estado.'); if (res.quote) { upsertQuote(res.quote); renderTable(); filterQuotes(); } }).catch(function (err) { alert(err.message || 'Error actualizando estado.'); select.value = previous; }).finally(function () { select.disabled = false; if (window.lucide) lucide.createIcons(); }); }
    function deleteCurrentQuote(trigger) { const id = Number((trigger && trigger.dataset && trigger.dataset.quoteId) || window.ICY_QUOTES_STATE.currentQuoteId || 0); if (!id) return; const q = quoteById(id); const label = q && q.quote_number ? q.quote_number : ('ID ' + id); if (!window.confirm('¿Eliminar la cotización ' + label + '?\n\nSe ocultará del listado activo, pero se conservarán cabecera, productos, documento asociado e historial.')) return; const originalHtml = trigger ? trigger.innerHTML : ''; if (trigger) { trigger.disabled = true; trigger.innerHTML = '<i data-lucide="loader" class="spin-icon"></i>'; if (window.lucide) lucide.createIcons(); } const form = new FormData(); form.append('id', id); form.append('ajax', '1'); fetch('/modules/crm/quote_delete.php', { method: 'POST', body: form, credentials: 'same-origin' }).then(function (r) { return r.json(); }).then(function (res) { if (!res || !res.success) { throw new Error((res && (res.message || res.error)) ? (res.message || res.error) : 'No se pudo eliminar la cotización.'); } removeQuoteFromState(id); renderTable(); window.closeQuotesModal(); }).catch(function (err) { alert(err.message || 'Error de conexión al eliminar.'); }).finally(function () { if (trigger) { trigger.disabled = false; trigger.innerHTML = originalHtml; if (window.lucide) lucide.createIcons(); } }); }
    function searchCustomers(query) { const box = document.getElementById('customerResults'); if (!box) return; if (query.length < 2) { box.innerHTML = ''; box.classList.remove('show'); return; } box.innerHTML = '<div class="search-result-item is-muted"><span>Buscando clientes...</span></div>'; box.classList.add('show'); fetch('/modules/crm/api_customers.php?q=' + encodeURIComponent(query), { credentials: 'same-origin' }).then(function (r) { return r.json(); }).then(function (rows) { box.innerHTML = ''; if (!Array.isArray(rows) || rows.length === 0) { box.innerHTML = '<div class="search-result-item is-muted"><span>Sin coincidencias. Puedes crear un cliente nuevo.</span></div>'; box.classList.add('show'); return; } rows.forEach(function (customer) { const btn = document.createElement('button'); btn.type = 'button'; btn.className = 'search-result-item quote-select-customer'; btn.dataset.customer = JSON.stringify(customer); btn.innerHTML = `<strong>${esc(customer.display_name || customer.legal_name || customer.customer_name)}</strong><span>${esc(customer.customer_tax_id || 'Sin RUT')} · ${esc(customer.email || 'Sin email')}</span>`; box.appendChild(btn); }); box.classList.add('show'); }).catch(function () { box.innerHTML = '<div class="search-result-item is-muted"><span>Error buscando clientes.</span></div>'; box.classList.add('show'); }); }
    function searchProducts(query) { const box = document.getElementById('productResults'); if (!box) return; if (query.length < 2) { box.innerHTML = ''; box.classList.remove('show'); return; } box.innerHTML = '<div class="search-result-item is-muted"><span>Buscando productos...</span></div>'; box.classList.add('show'); fetch('/modules/crm/api_products.php?q=' + encodeURIComponent(query), { credentials: 'same-origin' }).then(function (r) { return r.json(); }).then(function (rows) { box.innerHTML = ''; if (!Array.isArray(rows) || rows.length === 0) { box.innerHTML = '<div class="search-result-item is-muted"><span>Sin productos encontrados.</span></div>'; box.classList.add('show'); return; } rows.forEach(function (product) { const btn = document.createElement('button'); btn.type = 'button'; btn.className = 'search-result-item quote-select-product'; btn.dataset.product = JSON.stringify(product); btn.innerHTML = `<strong>${esc(product.name || '')}</strong><span>${esc(product.sku || 'S/S')} · ${money(product.price || product.price_unit || 0)} · ${esc(product.unit || 'unidad')}</span>`; box.appendChild(btn); }); box.classList.add('show'); }).catch(function () { box.innerHTML = '<div class="search-result-item is-muted"><span>Error buscando productos.</span></div>'; box.classList.add('show'); }); }
    function submitQuoteForm(form) { const btn = document.getElementById('quoteSaveBtn'); const originalHtml = btn ? btn.innerHTML : ''; if (btn) { btn.disabled = true; btn.innerHTML = '<i data-lucide="loader" class="spin-icon"></i><span>Guardando...</span>'; if (window.lucide) lucide.createIcons(); } fetch('/modules/crm/quote_save_ajax.php', { method: 'POST', body: new FormData(form), credentials: 'same-origin', headers: { 'Accept': 'application/json' } }).then(function (r) { return r.text().then(function (text) { let payload = null; try { payload = text ? JSON.parse(text) : null; } catch (e) { throw new Error('El servidor devolvió una respuesta no válida (HTTP ' + r.status + '). ' + String(text || '').replace(/<[^>]*>/g, ' ').trim().slice(0, 240)); } if (!payload) throw new Error('El servidor devolvió una respuesta vacía (HTTP ' + r.status + ').'); if (!r.ok) throw new Error(payload.message || payload.error || ('Error HTTP ' + r.status)); return payload; }); }).then(function (res) { if (!res || !res.success) { alert((res && (res.message || res.error)) ? (res.message || res.error) : 'Error al guardar la cotización.'); return; } if (!res.quote) { return fetch('/modules/crm/quote_get_ajax.php?id=' + encodeURIComponent(res.quote_id), { credentials: 'same-origin', headers: { 'Accept': 'application/json' } }).then(function (r) { return r.text().then(function (text) { let fresh = null; try { fresh = text ? JSON.parse(text) : null; } catch (e) { throw new Error('No se pudo interpretar la cotización guardada.'); } if (!fresh) throw new Error('La consulta de la cotización guardada devolvió una respuesta vacía.'); return fresh; }); }).then(function (fresh) { if (!fresh.success) throw new Error(fresh.message || 'No se pudo cargar la cotización guardada.'); return fresh.quote; }); } return res.quote; }).then(function (quote) { if (!quote) return; upsertQuote(quote); renderTable(); const mode = window.ICY_QUOTES_STATE.formMode; const msg = mode === 'edit' ? 'Cotización actualizada correctamente.' : 'Cotización creada correctamente.'; window.openQuotePreview(quote.id, msg); }).catch(function (err) { alert(err.message || 'Error de conexión al guardar.'); }).finally(function () { if (btn) { btn.disabled = false; btn.innerHTML = originalHtml; if (window.lucide) lucide.createIcons(); } }); }
    function formatRutInputValue(input) {
        if (!input) return;
        if (window.ICY_CUSTOMER_LOCATION && window.ICY_CUSTOMER_LOCATION.countryCode(input.closest('form')) !== 'cl') return;
        var value = String(input.value || '').replace(/[^0-9kK]/g, '');
        if (!value) { input.value = ''; return; }
        if (value.length === 1) { input.value = value.toUpperCase(); return; }
        var body = value.slice(0, -1).replace(/\B(?=(\d{3})+(?!\d))/g, '.');
        var dv = value.slice(-1).toUpperCase();
        input.value = body + '-' + dv;
    }
    function syncQuickCustomerPhone() {
        var prefix = document.getElementById('quickCustomerPhonePrefix');
        var number = document.getElementById('quickCustomerPhoneNumber');
        var real = document.getElementById('quickCustomerPhone');
        if (!prefix || !number || !real) return;
        var clean = String(number.value || '').replace(/[^0-9\s-]/g, '');
        number.value = clean;
        real.value = clean.replace(/[^0-9]/g, '').length > 0 ? prefix.value + ' ' + clean : '';
    }
    function bindBillingReadinessHandlersOnce() {
        if (window.__ICY_QUOTES_BILLING_READINESS_BOUND) return;
        window.__ICY_QUOTES_BILLING_READINESS_BOUND = true;
        document.addEventListener('click', function (e) {
            if (!root() || !e.target.closest('.quote-billing-readiness-retry')) return;
            e.preventDefault();
            const q = quoteById(window.ICY_QUOTES_STATE.currentQuoteId);
            if (q) refreshQuoteBillingReadiness(q);
        });
        document.addEventListener('change', function (e) {
            if (!root() || !e.target || e.target.id !== 'quoteBillingDocumentType') return;
            window.setTimeout(function () {
                const q = quoteById(window.ICY_QUOTES_STATE.currentQuoteId);
                if (q) refreshQuoteBillingReadiness(q);
            }, 0);
        });
    }
    function bindCustomerQuickHelpersOnce() {
        bindBillingReadinessHandlersOnce();
        if (window.__ICY_QUOTES_CUSTOMER_HELPERS_BOUND) return;
        window.__ICY_QUOTES_CUSTOMER_HELPERS_BOUND = true;
        document.addEventListener('input', function (e) {
            if (!root()) return;
            if (e.target && e.target.id === 'quickCustomerTaxId') {
                formatRutInputValue(e.target);
                return;
            }
            if (e.target && e.target.id === 'quickCustomerPhoneNumber') {
                syncQuickCustomerPhone();
            }
        });
        document.addEventListener('change', function (e) {
            if (!root()) return;
            if (e.target && e.target.id === 'quickCustomerPhonePrefix') {
                syncQuickCustomerPhone();
            }
        });
    }
    function submitCustomerForm(form) { syncQuickCustomerPhone(); const submitBtn = form.querySelector('button[type="submit"]'); const originalHtml = submitBtn ? submitBtn.innerHTML : ''; if (submitBtn) { submitBtn.disabled = true; submitBtn.innerHTML = '<i data-lucide="loader" class="spin-icon"></i><span>Creando...</span>'; if (window.lucide) lucide.createIcons(); } fetch('/modules/crm/quote_customer_save_ajax.php', { method: 'POST', body: new FormData(form), credentials: 'same-origin' }).then(function (r) { return r.json(); }).then(function (res) { if (!res || !res.success) { alert((res && res.message) ? res.message : 'Error creando cliente.'); return; } setCustomerChip(res.customer, false, ''); applyCustomerLock(false); document.getElementById('customerSearch').value = ''; document.getElementById('customerResults').classList.remove('show'); form.reset(); setLayer('quoteLayerForm'); }).catch(function () { alert('Error de conexión al crear cliente.'); }).finally(function () { if (submitBtn) { submitBtn.disabled = false; submitBtn.innerHTML = originalHtml; if (window.lucide) lucide.createIcons(); } }); }
    function bindGlobalHandlersOnce() { if (window.__ICY_QUOTES_GLOBAL_HANDLERS_BOUND) return; window.__ICY_QUOTES_GLOBAL_HANDLERS_BOUND = true; document.addEventListener('click', function (e) { const appRoot = root(); if (!appRoot) return; const spaLink = e.target.closest('a'); if (spaLink && !spaLink.target && !spaLink.hasAttribute('data-no-spa')) window.closeQuotesModal(); const openPreviewBtn = e.target.closest('.quote-open-preview'); if (openPreviewBtn) { e.preventDefault(); window.openQuotePreview(openPreviewBtn.dataset.quoteId); return; } if (e.target.closest('.quote-open-create')) { e.preventDefault(); window.openQuoteCreate(); return; } if (e.target.closest('.quote-modal-close')) { e.preventDefault(); window.closeQuotesModal(); return; } if (e.target.closest('.quote-open-edit')) { e.preventDefault(); window.openQuoteEdit(); return; } const guideOpenBtn = e.target.closest('#quoteDispatchGuideBtn'); if (guideOpenBtn) { e.preventDefault(); if (guideOpenBtn.disabled) return; window.openQuoteDispatchGuide(guideOpenBtn.dataset.quoteId || window.ICY_QUOTES_STATE.currentQuoteId); return; } if (e.target.closest('.quote-guide-back')) { e.preventDefault(); quoteGuideBack(); return; } if (e.target.closest('.quote-guide-return-quote')) { e.preventDefault(); const qid = window.ICY_QUOTES_STATE.currentQuoteId; if (qid) window.openQuotePreview(qid); else setLayer('quoteLayerPreview'); return; } const guidePreviewBtn = e.target.closest('.quote-guide-preview'); if (guidePreviewBtn) { e.preventDefault(); previewQuoteGuide(guidePreviewBtn); return; } if (e.target.closest('.quote-guide-confirm-layer')) { e.preventDefault(); renderQuoteGuideConfirm(); return; } const guideIssueBtn = e.target.closest('.quote-guide-confirm-issue'); if (guideIssueBtn) { e.preventDefault(); if (guideIssueBtn.disabled) return; issueQuoteGuide(guideIssueBtn); return; } const billingOpenBtn = e.target.closest('#quoteBillingBtn'); if (billingOpenBtn) { e.preventDefault(); if (billingOpenBtn.disabled) return; window.openQuoteBilling(billingOpenBtn.dataset.quoteId || window.ICY_QUOTES_STATE.currentQuoteId); return; } if (e.target.closest('.quote-billing-back')) { e.preventDefault(); const qid = window.ICY_QUOTES_STATE.currentQuoteId; if (qid) window.openQuotePreview(qid); else setLayer('quoteLayerPreview'); return; } if (e.target.closest('.quote-billing-prepare')) { e.preventDefault(); const q = quoteById(window.ICY_QUOTES_STATE.currentQuoteId); if (q) renderBillingPrepare(q); return; } const billingSaveDraftBtn = e.target.closest('.quote-billing-save-draft'); if (billingSaveDraftBtn) { e.preventDefault(); saveQuoteBillingDraft(billingSaveDraftBtn); return; } if (e.target.closest('.quote-billing-continue') || e.target.closest('.quote-billing-detail')) { e.preventDefault(); const q = quoteById(window.ICY_QUOTES_STATE.currentQuoteId); if (q) renderBillingDetail(q); return; } const billingValidateBtn = e.target.closest('.quote-billing-validate'); if (billingValidateBtn) { e.preventDefault(); validateQuoteBilling(billingValidateBtn); return; } const billingPreviewBtn = e.target.closest('.quote-billing-preview'); if (billingPreviewBtn) { e.preventDefault(); previewQuoteBilling(billingPreviewBtn); return; } if (e.target.closest('.quote-billing-confirm-layer')) { e.preventDefault(); const q = quoteById(window.ICY_QUOTES_STATE.currentQuoteId); if (q) renderBillingConfirm(q); return; } const billingConfirmIssueBtn = e.target.closest('.quote-billing-confirm-issue'); if (billingConfirmIssueBtn) { e.preventDefault(); if (billingConfirmIssueBtn.disabled) return; confirmQuoteBilling(billingConfirmIssueBtn); return; } const billingReconcileBtn = e.target.closest('.quote-billing-reconcile'); if (billingReconcileBtn) { e.preventDefault(); if (billingReconcileBtn.disabled) return; reconcileQuoteBilling(billingReconcileBtn); return; } if (e.target.closest('.quote-billing-email-layer')) { e.preventDefault(); const q = quoteById(window.ICY_QUOTES_STATE.currentQuoteId); if (q) renderBillingEmail(q); return; } const billingSendEmailBtn = e.target.closest('.quote-billing-send-email'); if (billingSendEmailBtn) { e.preventDefault(); sendBillingDocumentEmail(billingSendEmailBtn); return; } const restoreQuoteForm = e.target.closest('.quote-restore-inline-form'); if (restoreQuoteForm) { e.preventDefault(); if (!confirm('¿Restaurar esta cotización?')) return; const fd = new FormData(restoreQuoteForm); fd.append('ajax', '1'); fetch(restoreQuoteForm.action, { method: 'POST', body: fd, credentials: 'same-origin' }).then(function (r) { return r.json(); }).then(function (res) { if (!res || !res.success) { alert((res && res.error) ? res.error : 'No fue posible restaurar.'); return; } (window.ICY_SPA_GO ? window.ICY_SPA_GO('/modules/crm/quotes.php?view=deleted', { replace: true, force: true }) : (window.location.href = '/modules/crm/quotes.php?view=deleted')); }).catch(function () { alert('Error de conexión al restaurar.'); }); return; } const deleteQuoteBtn = e.target.closest('.quote-delete-btn'); if (deleteQuoteBtn) { e.preventDefault(); deleteCurrentQuote(deleteQuoteBtn); return; } if (e.target.closest('.quote-form-back')) { e.preventDefault(); if (window.ICY_QUOTES_STATE.currentQuoteId) window.openQuotePreview(window.ICY_QUOTES_STATE.currentQuoteId); else window.closeQuotesModal(); return; } if (e.target.closest('.quote-back-form')) { e.preventDefault(); setLayer('quoteLayerForm'); return; } if (e.target.closest('.quote-open-customer-create')) { e.preventDefault(); setLayer('quoteLayerCustomer'); return; } if (e.target.closest('.quote-add-manual')) { e.preventDefault(); addItem({}); return; } const removeRow = e.target.closest('.remove-row-btn'); if (removeRow) { e.preventDefault(); const row = removeRow.closest('.eq-row'); if (row) row.remove(); calculateQuote(); return; } const selectCustomer = e.target.closest('.quote-select-customer'); if (selectCustomer) { e.preventDefault(); const customer = JSON.parse(selectCustomer.dataset.customer || '{}'); setCustomerChip(customer, false, ''); document.getElementById('customerSearch').value = ''; document.getElementById('customerResults').classList.remove('show'); return; } const selectProduct = e.target.closest('.quote-select-product'); if (selectProduct) { e.preventDefault(); const product = JSON.parse(selectProduct.dataset.product || '{}'); addItem({ product_id: product.id || product.product_id || 'manual', name: product.name || '', sku: product.sku || '', quantity: 1, unit: product.unit || 'unidad', price_unit: product.price || product.price_unit || 0 }); document.getElementById('productSearch').value = ''; document.getElementById('productResults').classList.remove('show'); return; } if (e.target.id === 'quotesModalBackdrop') window.closeQuotesModal(); }); document.addEventListener('input', function (e) { if (!root()) return; if (e.target && (e.target.id === 'quoteSearch' || e.target.id === 'quoteCreatedFrom' || e.target.id === 'quoteCreatedTo')) { if (e.target.id === 'quoteCreatedFrom' || e.target.id === 'quoteCreatedTo') syncQuoteDatePlaceholders(); filterQuotes(); return; } if (e.target && e.target.id === 'quoteBillingIssueConfirmation') { updateIssueConfirmButton(); return; } if (e.target && e.target.id === 'quoteGuideIssueConfirmation') { updateQuoteGuideConfirmButton(); return; } if (e.target && e.target.id === 'customerSearch') { clearTimeout(window.ICY_QUOTES_STATE.customerTimer); const value = e.target.value.trim(); window.ICY_QUOTES_STATE.customerTimer = setTimeout(function () { searchCustomers(value); }, 220); return; } if (e.target && e.target.id === 'productSearch') { clearTimeout(window.ICY_QUOTES_STATE.productTimer); const value = e.target.value.trim(); window.ICY_QUOTES_STATE.productTimer = setTimeout(function () { searchProducts(value); }, 220); return; } if (e.target && (e.target.id === 'formDiscount' || e.target.classList.contains('it-qty') || e.target.classList.contains('it-price'))) calculateQuote(); }); document.addEventListener('change', function (e) { if (!root()) return; if (e.target && e.target.classList && e.target.classList.contains('quote-status-quick')) { updateQuickStatus(e.target); return; } if (e.target && (e.target.id === 'quoteStatusFilter' || e.target.id === 'quoteCreatedFrom' || e.target.id === 'quoteCreatedTo')) { if (e.target.id === 'quoteCreatedFrom' || e.target.id === 'quoteCreatedTo') syncQuoteDatePlaceholders(); filterQuotes(); return; } if (e.target && e.target.id === 'quoteBillingDocumentType') { syncBillingConfigFromForm(); toggleQuoteExportFields(); if(isExportDocumentKey(billingConfig().documentKey)) window.setTimeout(refreshQuoteExchangeRate,0); return; } if (e.target && (e.target.id === 'quoteBillingCurrency' || e.target.id === 'quoteBillingIssueDate')) { syncBillingConfigFromForm(); refreshQuoteExchangeRate(); return; } if (e.target && e.target.matches && e.target.matches('#quoteBillingExportFields input, #quoteBillingExportFields select')) { syncBillingConfigFromForm(); return; } if (e.target && e.target.id === 'quoteGuideType') { syncQuoteGuideTransferField(); return; } if (e.target && e.target.classList && e.target.classList.contains('quote-guide-line-enabled')) { const index = e.target.dataset.index; const quantity = document.querySelector('#quoteGuideContent .quote-guide-line-qty[data-index="' + index + '"]'); if (quantity) quantity.disabled = !e.target.checked; return; } if (e.target && e.target.id === 'quoteBillingMarkAsPaid') { const fields = document.getElementById('quoteBillingPaidFields'); if (fields) fields.style.display = e.target.checked ? 'grid' : 'none'; updateIssueConfirmButton(); return; } if (e.target && (e.target.id === 'quoteBillingPaymentMethod' || e.target.id === 'quoteBillingCashAccount' || e.target.id === 'quoteBillingPaymentDate')) { updateIssueConfirmButton(); return; } if (e.target && e.target.id === 'formStatus') { toggleInvoiceFields(); return; } if (e.target && e.target.id === 'opportunitySelect') { const oppId = Number(e.target.value || 0); document.getElementById('formOpportunityId').value = oppId; if (oppId > 0) { const opp = opportunityById(oppId); if (opp && opp.customer) { setCustomerChip(opp.customer, true, 'Cliente asociado a oportunidad'); applyCustomerLock(true); } } else if (window.ICY_QUOTES_STATE.formMode !== 'edit') { document.getElementById('formCustomerId').value = 0; document.getElementById('selectedCustomerChip').classList.add('hidden'); applyCustomerLock(false); } } }); document.addEventListener('submit', function (e) { if (!root()) return; if (e.target && e.target.id === 'quoteForm') { e.preventDefault(); submitQuoteForm(e.target); return; } if (e.target && e.target.id === 'customerQuickForm') { e.preventDefault(); submitCustomerForm(e.target); } }); document.addEventListener('keydown', function (e) { if (e.key === 'Escape' && root()) window.closeQuotesModal(); }); }
    function openBillingRequestedByUrl() {
        const appRoot = root();
        if (!appRoot) return;
        let quoteId = 0;
        try { quoteId = Number(new URL(window.location.href).searchParams.get('billing_quote_id') || 0); } catch (e) { quoteId = 0; }
        if (!quoteId || appRoot.dataset.billingAutostarted === String(quoteId)) return;
        appRoot.dataset.billingAutostarted = String(quoteId);
        const quote = quoteById(quoteId);
        if (!quote) {
            window.alert('La cotizaciÃ³n solicitada no estÃ¡ disponible o no tienes acceso.');
            return;
        }
        window.setTimeout(function () { window.openQuoteBilling(quoteId); }, 0);
    }
    window.initQuotesPage = function () { if (!root()) return; initState(); bindGlobalHandlersOnce(); bindCustomerQuickHelpersOnce(); bindQuoteFiltersDirect(); renderTable(); syncQuoteDatePlaceholders(); openBillingRequestedByUrl(); if (!modal() || modal().getAttribute('aria-hidden') !== 'false') document.body.style.overflow = ''; if (window.lucide) lucide.createIcons(); };
    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', window.initQuotesPage); else window.initQuotesPage();
})();
</script>

</main>
<?php require_once '../../layouts/footer.php'; ?>
