// Archivo: CotizCreatedIng.js
// Ruta: src\static\js\Operaciones\Ingenieria\Cotiz\CotizCreatedIng.js
// Lenguaje: JavaScript


class GlobalVariables_Class {
	UserID = null;
	// lista de objetos completos { id, file, addedAt }
	FilesToUpload = [];
	// opcional: lista plana de File
	PlainFiles = [];

	setUser(id) {
		this.UserID = id;
	}

	// Reemplaza TODO con la lista completa que emite el componente
	setAllFiles(objectsArray = [], plainFiles = []) {
		this.FilesToUpload = Array.from(objectsArray);
		this.PlainFiles = Array.from(plainFiles);
	}

	clearFiles() {
		this.FilesToUpload = [];
		this.PlainFiles = [];
	}

	getFilesObjects() {
		return this.FilesToUpload;
	}

	getFiles() {
		return this.PlainFiles;
	}
}

const globalVariables = new GlobalVariables_Class();
window.globalVariables = globalVariables; // si necesitas acceso global


let downloadFormat = null;
let fileInput = null;

/** @type {CostingForm | null} */
let activeCase = null;

// Calculo financiero
let isNeodata = null;
let isTender = null;



// Se usa cuando se recupera la task del enpoint
let task_TaskID = null
let task_SellerUserID = null
let docsID = null


async function getUserID() {
	try {
		const res = await fetch("/api/Operaciones/Ingenieria/Cotiz/CotizCreatedIng/user_id", {
			credentials: "include",
		});
		const data = await res.json();
		return data.user_id; // 👈 importante
	} catch (err) {
		console.error("❌ Error al obtener el UserID:", err);
		return null;
	}
}


const ExcelJson = (() => {
	// --- helpers ---
	const normVal = (v) => {
		if (v === null || v === undefined) return "";
		if (v instanceof Date) return v.toISOString();
		if (typeof v === "object" && v && "text" in v && typeof v.text === "string")
			return v.text;
		return String(v);
	};
	const isCellEmpty = (v) => normVal(v).trim() === "";
	const trimRightEmptyCells = (rowVals) => {
		if (!rowVals) return [];
		let end = rowVals.length - 1;
		while (end >= 0 && isCellEmpty(rowVals[end])) end--;
		return rowVals.slice(0, end + 1);
	};
	const parseDecimal = (raw) => {
		if (raw === null || raw === undefined) return null;
		let s = String(raw).trim();
		if (!s) return null;
		const lastComma = s.lastIndexOf(",");
		const lastDot = s.lastIndexOf(".");
		if (lastComma > lastDot) {
			s = s.replace(/\./g, ""); // quita miles
			s = s.replace(",", "."); // coma -> punto
		} else {
			s = s.replace(/,/g, ""); // quita miles
		}
		const n = Number(s);
		return Number.isFinite(n) ? n : null;
	};

	// --- encuentra encabezados ---
	const findHeaderMap = (sheet, required) => {
		for (let r = 1; r <= sheet.rowCount; r++) {
			const row = sheet.getRow(r);
			const vals = (row.values || []).slice(1).map(normVal);
			if (vals.some((v) => v.trim() !== "")) {
				const map = {};
				required.forEach((req) => {
					const idx = vals.findIndex(
						(h) => h.trim().toLowerCase() === req.toLowerCase()
					);
					map[req] = idx >= 0 ? idx : -1;
				});
				const allFound = Object.values(map).every((i) => i >= 0);
				return { headerRow: r, map, allFound };
			}
		}
		return { headerRow: -1, map: {}, allFound: false };
	};

	// --- lector principal ---
	const parseWorkbookFirstSheet = async (file) => {
		const arrayBuffer = await file.arrayBuffer();
		const workbook = new ExcelJS.Workbook();
		await workbook.xlsx.load(arrayBuffer);

		const sheet = workbook.worksheets[0];
		if (!sheet) return { rowsRaw: [], parts: [], sheetName: "(sin hoja)" };

		const REQUIRED = [
			"PartNum",
			"PartDescription",
			"Qty",
			"UOM",
			"UnitPrice",
			"Amount",
		];
		const { headerRow, map, allFound } = findHeaderMap(sheet, REQUIRED);
		if (!allFound) {
			throw new Error("Faltan encabezados requeridos: " + REQUIRED.join(", "));
		}

		const rowsRaw = [];
		const parts = [];
		const partRejected = [];
		let emptyStreak = 0;

		for (let r = headerRow + 1; r <= sheet.rowCount; r++) {
			const row = sheet.getRow(r);
			const values = (row.values || []).slice(1);
			const trimmed = trimRightEmptyCells(values);
			const allEmpty = trimmed.every(isCellEmpty);

			if (allEmpty) {
				emptyStreak++;
				if (emptyStreak >= 2) break;
				continue;
			} else {
				emptyStreak = 0;
			}

			const get = (name) => normVal(trimmed[map[name]] ?? "");
			const obj = {
				PartNum: get("PartNum"),
				PartDescription: get("PartDescription"),
				Qty: get("Qty"),
				UOM: get("UOM"),
				UnitPrice: get("UnitPrice"),
				//Amount: get('Amount'),
			};
			rowsRaw.push(obj);

			const hasAll = Object.values(obj).every((v) => v.trim() !== "");
			if (hasAll) {
				const dto = new ItemDTO({
				origin: "Excel",
				partNum: typeof obj.PartNum === "string" ? obj.PartNum.toUpperCase() : obj.PartNum,
				description: typeof obj.PartDescription === "string" ? obj.PartDescription.toUpperCase() : obj.PartDescription,
				quantity: parseDecimal(obj.Qty), // 🔹 numérico, sin afectar
				unit: typeof obj.UOM === "string" ? obj.UOM.toUpperCase() : obj.UOM,
				unitPrice: parseDecimal(obj.UnitPrice),
				amount: parseDecimal(obj.UnitPrice) * parseDecimal(obj.Qty),
			});

				// validaciones
				if (dto.quantity < 1 || dto.unitPrice < 1 || dto.amount < 1) {
					partRejected.push(dto);
				} else {
					parts.push(dto);
				}
			} else {
				partRejected.push(obj);
			}
		}

		return { rowsRaw, parts, sheetName: sheet.name || "(sin nombre)" };
	};

	// --- API pública ---
	const readSelectedFileToPartDTOs = async (input) => {
		const file = input.files?.[0];
		if (!file) return { rowsRaw: [], parts: [] };
		const res = await parseWorkbookFirstSheet(file);

		res.total = res.parts.reduce((a, p) => a + (p.amount || 0), 0);
		res.noPartRejected = (res.partRejected || []).length;

		return res;
	};

	return { readSelectedFileToPartDTOs };
})();



class PartDTO {
	constructor(data = {}) {
		this.Origin = data.Origin || "";
		this.PartNum = data.PartNum || "";
		this.PartDescription = data.PartDescription || "";
		this.IsServices = Boolean(data.IsServices);
		this.LastCost =
			data.LastCost != null
				? Number(parseFloat(data.LastCost).toFixed(2))
				: null;
		this.LastPrice =
			data.LastPrice != null
				? Number(parseFloat(data.LastPrice).toFixed(2))
				: null;
		this.CostDate = data.CostDate ? new Date(data.CostDate) : null;
		this.PriceDate = data.PriceDate ? new Date(data.PriceDate) : null;
	}
	static fromArray(arr) {
		return Array.isArray(arr) ? arr.map((o) => new PartDTO(o)) : [];
	}
	toDict() {
		return {
			Origin: this.Origin,
			PartNum: this.PartNum,
			PartDescription: this.PartDescription,
			IsServices: this.IsServices,
			LastCost: this.LastCost,
			LastPrice: this.LastPrice,
			CostDate: this.CostDate ? this.CostDate.toISOString() : null,
			PriceDate: this.PriceDate ? this.PriceDate.toISOString() : null,
		};
	}
}



class LocalDb {
	constructor(key = "concepts") {
		this.key = key;
	}

	getAll() {
		const raw = localStorage.getItem(this.key);
		if (!raw) return [];
		try {
			return ItemDTO.fromArray(JSON.parse(raw));
		} catch (e) {
			console.error("Error parseando localStorage:", e);
			return [];
		}
	}
	_save(arr) {
		localStorage.setItem(this.key, JSON.stringify(arr.map((p) => p.toDict())));
	}


	add(item) {
		const arr = this.getAll();
		arr.push(item);
		this._save(arr);
	}


	addItems(items) {
		const arr = this.getAll();
		arr.push(...items);
		this._save(arr);
	}

	update(item) {
		const arr = this.getAll();
		const idx = arr.findIndex((p) => p.uuid === item.uuid);
		if (idx >= 0) arr[idx] = item;
		else {
			console.error("No se encontró uuid, se agrega:", item.uuid);
			arr.push(item);
		}
		this._save(arr);
	}


	remove(uuid) {
		this._save(this.getAll().filter((p) => p.uuid !== uuid));
	}

	clear() {
		localStorage.removeItem(this.key);
	}

	to_Json_Q_CostingDetail(costingID, opts = {}) {
		const { startIndex = 1 } = opts;
		const items = this.getAll();
		const r2 = (n) => Number(Number(n).toFixed(2));

		return items.map((item, idx) => ({
			CostingID: costingID,
			CostingLine: startIndex + idx,
			PartNum: item.partNum,
			PartDescription: item.description,
			Qty: Number(item.quantity),
			UOMCode: item.unit, //'PZA', //item.unit,
			UnitPrice: r2(item.unitPrice),
			Amount: r2(item.amount ?? (item.quantity * item.unitPrice)),
		}));
	}


}



class ItemDTO {
	constructor(data = {}) {
		this.origin = data.origin || "Elephant";
		this.partNum = data.partNum || "";
		this.description = data.description || "";
		this.quantity = Number(data.quantity ?? 1);
		this.unit = data.unit || "";
		this.unitPrice =
			data.unitPrice != null
				? Number(parseFloat(data.unitPrice).toFixed(2))
				: 0;
		this.amount =
			data.amount != null ? Number(parseFloat(data.amount).toFixed(2)) : 0;
		this.date = data.date ? new Date(data.date) : new Date();
		if (!data.uuid) {
			this.assignUUID();
		} else {
			this.uuid = data.uuid;
		}
	}

	static fromArray(arr) {
		return Array.isArray(arr) ? arr.map((o) => new ItemDTO(o)) : [];
	}

	toDict() {
	return {
		uuid: this.uuid,
		origin: this.origin,
		partNum: this.partNum.toUpperCase(),
		description: this.description.toUpperCase(),
		quantity: this.quantity.toString().toUpperCase(),
		unit: this.unit.toUpperCase(),
		unitPrice: this.unitPrice,
		amount: this.amount,
	};
}

	assignUUID() {
		this.uuid = crypto.randomUUID();
	}
}


const btn_agregar = document.getElementById("btn_agregar");
let modalEdicionParte = null;
let isModalInEdition = false;
let uuidEditionElement = null;

let localDatabase = new LocalDb();
/** @type {ItemDTO[]} */
let itemList = null;

// --- helper: limpiar formulario al crear nuevo ---
function resetFormNuevo() {
	const $ = (id, v) => {
		const el = document.getElementById(id);
		if (el) el.value = v;
	};
	$("ai_partNum", "");
	$("ai_description", "");
	$("ai_quantity", "1");
	$("ai_unit", "");
	$("ai_unitPrice", "0");
	$("ai_amount", "0");
}

// --- recolectar datos del formulario ---
function collectItemFormData() {
	return {
		origin: "Elephant",
		partNum: document.getElementById("ai_partNum").value.trim(),
		description: document.getElementById("ai_description").value.trim(),
		quantity: parseFloat(document.getElementById("ai_quantity").value),
		unit: document.getElementById("ai_unit").value.trim(),
		unitPrice: parseFloat(document.getElementById("ai_unitPrice").value),
		amount: parseFloat(document.getElementById("ai_amount").value),
	};
}

// --- validar datos (todos obligatorios y válidos) ---
function validateItemData(data) {
	const missing = [];
	const invalid = [];

	if (!data.partNum) missing.push("No. de Parte");
	if (!data.description) missing.push("Descripción");
	if (!data.unit) missing.push("Unidad");

	if (!Number.isFinite(data.quantity) || data.quantity < 0)
		invalid.push("Cantidad mayor que 0 unidades");
	if (!Number.isFinite(data.unitPrice) || data.unitPrice < 1)
		invalid.push("Precio Unitario mayor que 0");
	if (!Number.isFinite(data.amount) || data.amount < 1)
		invalid.push("Importe mayor que 0");

	const expected =
		Math.round(Number(data.quantity || 0) * Number(data.unitPrice || 0) * 100) /
		100;
	const sameAmount = Math.abs(expected - Number(data.amount || 0)) < 0.01;

	return {
		ok: missing.length === 0 && invalid.length === 0,
		missing,
		invalid,
		expectedAmount: expected,
		sameAmount,
	};
}

// --- calcular importe ---
function calcularImporte() {
	const qtyEl = document.getElementById("ai_quantity");
	const priceEl = document.getElementById("ai_unitPrice");
	const amtEl = document.getElementById("ai_amount");

	let qty = parseFloat(qtyEl?.value) || 0;
	let price = parseFloat(priceEl?.value) || 0;

	if (qty < 0) {
		qty = 1;
		qtyEl.value = qty;
	}
	if (price < 0) {
		price = 0;
		priceEl.value = price;
	}

	const importe = Math.round(qty * price * 100) / 100;
	amtEl.value = importe.toFixed(2);
}

// --- cargar item a modal para edición ---
function loadPartIntoModal(uuid) {
	isModalInEdition = true;
	uuidEditionElement = uuid;

	let item = localDatabase.getAll().find((p) => p.uuid === uuid);
	if (!item) return;

	document.getElementById("ai_partNum").value = item.partNum ?? "";
	document.getElementById("ai_description").value = item.description ?? "";
	document.getElementById("ai_quantity").value = item.quantity ?? 1;
	document.getElementById("ai_unit").value = item.unit ?? "";
	document.getElementById("ai_unitPrice").value = item.unitPrice ?? 0.0;
	document.getElementById("ai_amount").value = item.amount ?? 0.0;

	modalEdicionParte?.show();
}

// --- pintar tabla ---
let total = 0;
function updateTable() {
    const data = localDatabase.getAll();
    const tableContainer = document.getElementById("ApprovalAccessDistributorsContainer"); // 🔹 contenedor padre del <table>
    const tableBody = document.getElementById("ApprovalAccessDistributorsbody");
    if (!tableBody) return;

    // 🔹 Calcula el total
    total = data.reduce((acc, item) => {
        const quantity = parseFloat(item.quantity) || 0;
        const amount = parseFloat(item.unitPrice) || 0;
        return acc + quantity * amount;
    }, 0);

    console.log("Total:", total);
    initCaseBySwitches();

    // 🔹 Si no hay datos, muestra el mensaje vacío
    if (!Array.isArray(data) || data.length === 0) {
        tableBody.innerHTML = `
            <tr class="sizer-row">
                <td colspan="7" class="text-center text-muted py-3">Sin resultados</td>
            </tr>`;
        return;
    }

    // 🔹 Genera todas las filas
    const rowsHTML = data.map(item => `
        <tr>
            <td>${item.partNum ?? ""}</td>
            <td>${item.description ?? ""}</td>
            <td>${item.quantity ?? ""}</td>
            <td>${item.unit ?? ""}</td>
            <td class="currency">${item.unitPrice ?? ""}</td>
            <td class="currency">${item.amount ?? ""}</td>
            <td>
                <button class="btn btn-sm btn-outline-primary me-1" onclick="loadPartIntoModal('${item.uuid}')">
                    <i class="fa fa-pencil-alt"></i>
                </button>
                <button class="btn btn-sm btn-outline-danger" onclick="confirmDelete('${item.partNum}', '${item.uuid}')">
                    <i class="fa fa-trash"></i>
                </button>
            </td>
        </tr>
    `).join('');

    // 🔹 1. Destruye y elimina completamente cualquier wrapper previo de DataTable
    const tableSelector = '#ApprovalAccessDistributorsTable';
    if ($.fn.DataTable.isDataTable(tableSelector)) {
        $(tableSelector).DataTable().destroy(true);
    }

    // 🔹 2. Limpia el contenedor de DataTable (por seguridad)
    const wrapper = document.querySelector('#ApprovalAccessDistributorsTable_wrapper');
    if (wrapper) wrapper.remove();

    // 🔹 3. Recrea la tabla base si fue eliminada por destroy(true)
    if (!document.querySelector(tableSelector)) {
        const newTableHTML = `
            <table id="ApprovalAccessDistributorsTable" class="table table-striped table-bordered" style="width:100%">
                <thead>
                    <tr>
                        <th>No. de Parte</th>
                        <th>Descripción</th>
                        <th>Cantidad</th>
                        <th>Unidad</th>
                        <th>Precio Unitario</th>
                        <th>Importe</th>
                        <th>Acciones</th>
                    </tr>
                    <tr>
                        <th><input type="text" placeholder="Buscar número de parte"></th>
                        <th><input type="text" placeholder="Buscar descripción"></th>
                        <th></th>
                        <th></th>
                        <th></th>
                        <th></th>
                        <th></th>
                    </tr>
                </thead>
                <tbody id="ApprovalAccessDistributorsbody"></tbody>
            </table>`;
        tableContainer.innerHTML = newTableHTML;
    }

    // 🔹 4. Inserta las nuevas filas
    document.getElementById("ApprovalAccessDistributorsbody").innerHTML = rowsHTML;

    // 🔹 5. Reinicializa DataTable desde cero
    initializeDataTable2();

    // 🔹 6. Aplica formato visual (por ejemplo, $)
    updateCurrencyVisuals();
}
/**
 * Inicializa, o reinicializa, la tabla de líneas de cotización con DataTables.
 * Esta versión CORREGIDA asegura que los encabezados se exporten correctamente a Excel y PDF.
 */
function initializeDataTable2() {
    const tableSelector = '#ApprovalAccessDistributorsTable';

    // 🔹 1. Destruir la instancia previa de DataTable para evitar conflictos.
    if ($.fn.DataTable.isDataTable(tableSelector)) {
        $(tableSelector).DataTable().destroy();
        // Limpiar los inputs de filtro manualmente, ya que destroy() no siempre lo hace.
        $('#ApprovalAccessDistributorsTable thead tr:eq(1) th input').val('');
    }

    // 🔹 2. Inicializar DataTable con la configuración completa.
    const table = $(tableSelector).DataTable({
        responsive: true,
        scrollX: true,
        dom: 'Blfrtip',
        buttons: [
            {
                extend: 'excel',
                text: 'Exportar a Excel',
                className: 'btn btn-success',
                exportOptions: {
                    columns: ':visible:not(:last-child)',
                    format: {
                        // ✅ CORRECCIÓN CLAVE: Se utiliza un selector robusto para encontrar el encabezado correcto,
                        // incluso cuando DataTables reestructura la tabla con scrollX.
                        header: function (data, columnIdx) {
                            let headerSelector = $('#ApprovalAccessDistributorsTable_wrapper .dataTables_scrollHead thead tr:first-child th');
                            if (headerSelector.length === 0) {
                                headerSelector = $('#ApprovalAccessDistributorsTable thead tr:first-child th');
                            }
                            return headerSelector.eq(columnIdx).text();
                        }
                    }
                }
            },
            {
                extend: 'pdf',
                text: 'Exportar a PDF',
                className: 'btn btn-danger',
                orientation: 'landscape',
                pageSize: 'LEGAL',
                exportOptions: {
                    columns: ':visible:not(:last-child)',
                    format: {
                        // ✅ CORRECCIÓN CLAVE: Se aplica la misma lógica de selección de encabezado para el PDF.
                        header: function (data, columnIdx) {
                            let headerSelector = $('#ApprovalAccessDistributorsTable_wrapper .dataTables_scrollHead thead tr:first-child th');
                            if (headerSelector.length === 0) {
                                headerSelector = $('#ApprovalAccessDistributorsTable thead tr:first-child th');
                            }
                            return headerSelector.eq(columnIdx).text();
                        }
                    }
                }
            }
        ],
        pageLength: 10,
        searching: true,
        ordering: true,
        lengthMenu: [[10, 25, 50, -1], [10, 25, 50, "Todos"]],
        columnDefs: [
            {
                targets: -1, // Última columna (acciones)
                orderable: false,
                searchable: false
            }
        ],
        order: [[0, 'asc']],
        initComplete: function () {
            this.api().columns().every(function () {
                const column = this;
                // Seleccionar el input de la segunda fila del thead
                const input = $('input', $(column.header()).parent().parent().find('tr:eq(1) th').eq(column.index()));
                
                if (input.length > 0) {
                    input.off('keyup change').on('keyup change', function () {
                        if (column.search() !== this.value) {
                            column.search(this.value).draw();
                        }
                    });
                }
            });
        }
    });

    return table;
}


function updateCurrencyVisuals() {
  const formatter = new Intl.NumberFormat("es-MX", {
    minimumFractionDigits: 2,
    maximumFractionDigits: 2
  });

  document.querySelectorAll(".currency").forEach(td => {
    const rawText = td.textContent.trim();
    const num = parseFloat(rawText.replace(/[^\d.-]/g, ""));
    if (isNaN(num)) return;

    // Mantenemos el valor real
    td.dataset.realValue = rawText;

    // Mostramos solo la versión formateada
    td.textContent = formatter.format(num);
  });
}




function confirmDelete(partNum, uuid) {
	Swal.fire({
		title: `¿Deseas eliminar el ítem <u>${partNum}</u>?`,
		icon: "warning",
		html: `<p>Se eliminará este registro.</p><p class="text-danger fw-bold">Esta acción no se puede deshacer.</p>`,
		showCloseButton: true,
		showCancelButton: true,
		focusConfirm: true,
		confirmButtonText: `<i class="fa fa-trash"></i> Sí, eliminar`,
		cancelButtonText: `<i class="fa fa-times"></i> No, cancelar`,
	}).then((r) => {
		if (r.isConfirmed) {
			localDatabase.remove(uuid);
			updateTable();
			Swal.fire({
				icon: "success",
				title: "Eliminado",
				timer: 2000,
				showConfirmButton: false,
			});
		}
	});
}

document.addEventListener("DOMContentLoaded", () => {
	downloadFormat = document.getElementById("downloadFormat");
	if (downloadFormat) {
		downloadFormat.addEventListener("click", downloadFile);
	}

	fileInput = document.getElementById("fileInput");
	fileInput.addEventListener("change", async (event) => {
		const file = event.target.files[0]; // primer archivo seleccionado
		if (file) {
			try {
				const { rowsRaw, parts, sheetName, total, noPartRejected } =
					await ExcelJson.readSelectedFileToPartDTOs(fileInput);
				let localDb = new LocalDb();
				if (parts.length > 0) {
					Swal.fire({
						title: "Importación exitosa",
						html: `
							<ul style="text-align:left;">
							<li><b>No de conceptos agregados:</b> ${parts.length}</li>
							<li><b>Importe total:</b> $${total.toFixed(2)}</li>
							</ul>
							`,
						icon: "success",
					});
					localDb.addItems(parts);
					updateTable();
				} else {
					Swal.fire({
						title: "No se importaron partes",
						text: `No se importaron partes. Verifica que el archivo tenga datos válidos.`,
						icon: "warning",
					});
				}
			} catch (err) {
				console.error(err);
				out.textContent = "Error: " + (err?.message || err);
			} finally {
				// Limpia el input para permitir re-seleccionar el mismo archivo si quieres
				fileInput.value = "";
			}
			// 👉 Aquí puedes lanzar tu función de importación
		} else {
			console.log("No se seleccionó ningún archivo");
		}
	});

	const modalEl = document.getElementById("editItemModal");
	if (modalEl) {
		modalEdicionParte = new bootstrap.Modal(modalEl, {
			backdrop: true,
			keyboard: true,
			focus: true,
		});
		modalEl.addEventListener("hidden.bs.modal", () => {
			isModalInEdition = false;
			btn_agregar?.focus();
		});
	}

	// Listeners de cálculo
	const qtyEl = document.getElementById("ai_quantity");
	const priceEl = document.getElementById("ai_unitPrice");
	qtyEl?.addEventListener("input", calcularImporte, { passive: true });
	priceEl?.addEventListener("input", calcularImporte, { passive: true });
	qtyEl?.addEventListener("blur", calcularImporte);
	priceEl?.addEventListener("blur", calcularImporte);

	// Guardar (crear/editar) con VALIDACIÓN y **SweetAlert2**
	document.getElementById("btn_agregar2")?.addEventListener("click", () => {
		calcularImporte(); // asegura amount actualizado
		const data = collectItemFormData();
		const { ok, missing, invalid, expectedAmount, sameAmount } =
			validateItemData(data);

		if (!ok || !sameAmount) {
			// Build HTML para Swal
			const li = (arr) => arr.map((x) => `<li>${x}</li>`).join("");
			const missingHtml = missing.length
				? `<h6 class="mt-2 mb-1">Falta:</h6><ul>${li(missing)}</ul>`
				: "";
			const invalidHtml = invalid.length
				? `<h6 class="mt-2 mb-1">Revisa:</h6><ul>${li(invalid)}</ul>`
				: "";
			const tipHtml = !sameAmount
				? `<p class="mt-2"><b>Tip:</b> El Importe esperado es <code>${expectedAmount.toFixed(
					2
				)}</code> (Cantidad × Precio).</p>`
				: "";

			const html = `
					<div style="text-align:left">
					<p>Por favor completa la información requerida.</p>
					${missingHtml}
					${invalidHtml}
					${tipHtml}
					</div>
				`;

			Swal.fire({
				title: "Falta información",
				html,
				icon: "error",
				confirmButtonText: "Entendido",
			}).then(() => {
				const mapFieldToId = {
					"No. de Parte": "ai_partNum",
					Descripción: "ai_description",
					Unidad: "ai_unit",
					"Cantidad (> 0)": "ai_quantity",
					"Precio Unitario (≥ 0)": "ai_unitPrice",
					"Importe (≥ 0)": "ai_amount",
				};
				const first = missing[0] || invalid[0] || null;
				if (first && mapFieldToId[first])
					document.getElementById(mapFieldToId[first])?.focus();

				if (!ok && !sameAmount) {
					document.getElementById("ai_amount").value =
						expectedAmount.toFixed(2);
				}
			});

			if (!ok) return;
			if (!sameAmount) {
				data.amount = expectedAmount;
				document.getElementById("ai_amount").value = expectedAmount.toFixed(2);
			}
		}

		// Asignar UUID si es edición
		data.uuid = isModalInEdition ? uuidEditionElement : null;

		const item = new ItemDTO(data);

		if (isModalInEdition) {
			localDatabase.update(item);
		} else {
			localDatabase.add(item);
		}

		isModalInEdition = false;
		modalEdicionParte?.hide();
		updateTable();
	});
});






// Abrir modal para nuevo
btn_agregar.addEventListener("click", () => {
	isModalInEdition = false;
	resetFormNuevo();
	modalEdicionParte?.show();
});

// Exponer funciones usadas inline
window.loadPartIntoModal = loadPartIntoModal;
window.confirmDelete = confirmDelete;

document.addEventListener("DOMContentLoaded", () => {
	localDatabase.clear();
	updateTable();
});




// ===============================
// 🔹 CONSTANTES Y VARIABLES
// ===============================
let table; // Variable para almacenar la instancia de DataTable
let selectedDistributorId = null; // ID del distribuidor seleccionado
let selectedDistributorIdForReject = null;

function mostrarAlerta(mensaje, tipo = "info") {
	const iconMap = {
		success: "success",
		error: "error",
		warning: "warning",
		info: "info",
	};

	const titleMap = {
		success: "¡Éxito!",
		error: "Error",
		warning: "Advertencia",
		info: "Información",
	};

	Swal.fire({
		icon: iconMap[tipo] || "info",
		title: titleMap[tipo] || "Información",
		text: mensaje,
		confirmButtonText: "Entendido",
		timer: tipo === "success" ? 5000 : null,
		timerProgressBar: tipo === "success",
	});
}

let parts = null;
let concepts = [];

async function fetchParts({ timeoutMs = 10000 } = {}) {
	const url = `/api/quotation/parts`; // <-- coincide con el backend

	const controller = new AbortController();
	const t = setTimeout(() => controller.abort(), timeoutMs);

	try {
		const resp = await fetch(url, {
			headers: { Accept: "application/json" },
			signal: controller.signal,
		});
		if (!resp.ok) throw new Error(`HTTP ${resp.status} en ${url}`);
		const data = await resp.json();
		if (!Array.isArray(data))
			throw new Error("El endpoint no devolvió un arreglo JSON.");
		return PartDTO.fromArray(data);
	} finally {
		clearTimeout(t);
	}
}

/* Funcionalidad formulario costeo */

/* === Funcionalidad formulario costeo === */
/**
 * @class CostingForm
 * @description Clase base que gestiona la funcionalidad común del formulario de costeo.
 * Se encarga de inicializar los elementos del DOM, manejar el comportamiento de los switches
 * y proporcionar métodos utilitarios compartidos por las subclases.
 */
class CostingForm {
	constructor() {
		// Referencias a elementos del DOM para los títulos y switches
		this.typeCase = "";
		this.title = document.getElementById("costing-case-title");
		this.switchNeodataOrigin = document.getElementById("switchNeodataOrigin");
		this.switchTender = document.getElementById("switchTender");

		// Referencias a todos los inputs del formulario
		this.inputDirectCost = document.getElementById("inputDirectCost");
		this.inputIndirectValue = document.getElementById("inputIndirectValue");
		this.inputIndirectPercent = document.getElementById("inputIndirectPercent");
		this.inputFinancingValue = document.getElementById("inputFinancingValue");
		this.inputFinancingPercent = document.getElementById(
			"inputFinancingPercent"
		);
		this.inputProfitValue = document.getElementById("inputProfitValue");
		this.inputProfitPercent = document.getElementById("inputProfitPercent");
		this.inputMinSalePrice = document.getElementById("inputMinSalePrice");
		this.inputOperatingExpensesValue = document.getElementById(
			"inputOperatingExpensesValue"
		);
		this.inputOperatingExpensesPercent = document.getElementById(
			"inputOperatingExpensesPercent"
		);
		this.inputMaxDiscountValue = document.getElementById(
			"inputMaxDiscountValue"
		);
		this.inputMaxDiscountPercent = document.getElementById(
			"inputMaxDiscountPercent"
		);
		this.inputListPrice = document.getElementById("inputListPrice");
		this.inputProposedAmountValue = document.getElementById(
			"inputProposedAmountValue"
		);

		// Array que contiene todas las referencias a los inputs para un manejo más sencillo
		this.allInputs = [
			this.inputDirectCost,
			this.inputIndirectValue,
			this.inputIndirectPercent,
			this.inputFinancingValue,
			this.inputFinancingPercent,
			this.inputProfitValue,
			this.inputProfitPercent,
			this.inputOperatingExpensesValue,
			this.inputOperatingExpensesPercent,
			this.inputMaxDiscountValue,
			this.inputMaxDiscountPercent,
			this.inputMinSalePrice,
			this.inputListPrice,
			this.inputProposedAmountValue,
		];

		// Inicializa la lógica de los switches al crear la instancia
		this.initSwitchBehavior();
	}

	toJson() {
		const toNumber = (val) => {
			if (val === null || val === undefined) return 0;
			const num = parseFloat(val.toString().replace(/,/g, "").trim());
			return isNaN(num) ? 0 : num;
		};

		return {
			directCost: toNumber(this.inputDirectCost.value),
			indirect: {
				amount: toNumber(this.inputIndirectValue.value),
				percent: toNumber(this.inputIndirectPercent.value),
			},
			financing: {
				amount: toNumber(this.inputFinancingValue.value),
				percent: toNumber(this.inputFinancingPercent.value),
			},
			profit: {
				amount: toNumber(this.inputProfitValue.value),
				percent: toNumber(this.inputProfitPercent.value),
			},
			proposalAmount: toNumber(this.inputProposedAmountValue.value),
			operatingExpenses: {
				amount: toNumber(this.inputOperatingExpensesValue.value),
				percent: toNumber(this.inputOperatingExpensesPercent.value),
			},
			minSalePrice: toNumber(this.inputMinSalePrice.value),
			maxDiscount: {
				amount: toNumber(this.inputMaxDiscountValue.value),
				percent: toNumber(this.inputMaxDiscountPercent.value),
			},
			listPrice: toNumber(this.inputListPrice.value),
		};

	}



	updateDirectCost() { }

	/**
	 * @method getValues
	 * @description Retorna un objeto con los valores actuales de todos los inputs y el estado de los switches.
	 * Convierte los valores de los inputs a números flotantes y maneja valores no numéricos.
	 * @returns {object} Un objeto que contiene todos los valores y estados del formulario.
	 */
	getValues() {
		return {
			directCost: parseFloat(this.inputDirectCost.value) || 0,
			indirectValue: parseFloat(this.inputIndirectValue.value) || 0,
			indirectPercent: parseFloat(this.inputIndirectPercent.value) || 0,
			financingValue: parseFloat(this.inputFinancingValue.value) || 0,
			financingPercent: parseFloat(this.inputFinancingPercent.value) || 0,
			profitValue: parseFloat(this.inputProfitValue.value) || 0,
			profitPercent: parseFloat(this.inputProfitPercent.value) || 0,
			minSalePrice: parseFloat(this.inputMinSalePrice.value) || 0,
			operatingExpensesValue:
				parseFloat(this.inputOperatingExpensesValue.value) || 0,
			operatingExpensesPercent:
				parseFloat(this.inputOperatingExpensesPercent.value) || 0,
			maxDiscountValue: parseFloat(this.inputMaxDiscountValue.value) || 0,
			maxDiscountPercent: parseFloat(this.inputMaxDiscountPercent.value) || 0,
			listPrice: parseFloat(this.inputListPrice.value) || 0,
			neodataOrigin: this.switchNeodataOrigin.checked,
			tender: this.switchTender.checked,
			ProposedAmount: parseFloat(this.inputProposedAmountValue.value) || 0,
		};
	}

	updateProposedAmount() {
		const direct = parseFloat(this.inputDirectCost.value) || 0;
		const indirect = parseFloat(this.inputIndirectValue.value) || 0;
		const financing = parseFloat(this.inputFinancingValue.value) || 0;
		const profit = parseFloat(this.inputProfitValue.value) || 0;

		const proposedAmount = direct + indirect + financing + profit;
		if (this.inputProposedAmountValue) {
			this.inputProposedAmountValue.value = proposedAmount.toFixed(2);
		}
	}

	/**
	 * @method initSwitchBehavior
	 * @description Configura la lógica de interacción entre los switches. Si el switch 'Neodata'
	 * está apagado, el switch 'Tender' se deshabilita y se desmarca automáticamente.
	 */
	initSwitchBehavior() {
		const updateTenderSwitch = () => {
			this.switchTender.disabled = !this.switchNeodataOrigin.checked;
			if (this.switchTender.disabled) {
				this.switchTender.checked = false;
			}
		};
		updateTenderSwitch();
		this.switchNeodataOrigin.addEventListener("change", updateTenderSwitch);
	}

	// Actualiza los inputs si total cambia:

	/**
	 * @method resetInputs
	 * @description Reinicia el estado de todos los inputs del formulario. Borra sus valores
	 * y los deshabilita. Es fundamental para cambiar de caso sin conflictos.
	 */
	resetInputs() {
		this.allInputs.forEach((input) => {
			if (input) {
				input.value = "";
				input.disabled = true;
			}
		});
	}

	/**
	 * @method validateNonNegative
	 * @description Valida que el input no tenga valores negativos. Se dispara al escribir.
	 */
	validateNonNegative(event) {
		const input = event.target;
		const value = parseFloat(input.value);

		// Validar que sea un número positivo
		if (isNaN(value) || value < 0) {
			input.value = ""; // resetear inmediatamente
			input.disabled = false; // asegurar que siga activo para que el usuario pueda corregir

			// Deshabilitar siguiente input si existe en secuencia
			if (this.sequentialInputs) {
				const index = this.sequentialInputs.indexOf(input);
				for (let i = index + 1; i < this.sequentialInputs.length; i++) {
					this.sequentialInputs[i].value = "";
					this.sequentialInputs[i].disabled = true;
				}
			}

		}
	}


	/**
	 * @method attachNonNegativeValidation
	 * @description Adjunta automáticamente la validación de números negativos a todos los inputs numéricos del formulario.
	 */
	attachNonNegativeValidation() {
		this.allInputs.forEach((input) => {
			if (input) {
				input.addEventListener("input", (e) => this.validateNonNegative(e));
			}
		});
	}
}

/**
 * @class CaseA
 * @description Subclase para el caso de costeo 'A' (Neodata: true, Tender: true).
 * Su lógica principal es la validación de la suma de componentes contra un precio de venta fijo.
 */ ///   ///  //
class CaseA extends CostingForm {
	constructor() {
		super();
		this.typeCase = "A";
		this.title.textContent = "Cálculo Financiero";
		this.listeners = []; // Añadido: array para almacenar los listeners
		this.applyCaseARules();
		this.setInitialPrice();
		this.initListeners();
		// <-- Activar validación global de números negativos
		this.attachNonNegativeValidation();
		// <-- Actualizar los porcentajes automáticamente cuando cambien los valores
		this.initPercentFromValues();

		// this.initScreenMovementValidation(500);
	}

	updateDirectCost() {
		this.inputMinSalePrice.value = total;
	}
	/**
	 * @method applyCaseARules
	 * @description Aplica las reglas específicas del caso A, habilitando únicamente los inputs
	 * de costo directo, indirecto, financiamiento y utilidad.
	 */
	applyCaseARules() {
		this.allInputs.forEach((input) => (input.disabled = true));
		this.inputDirectCost.disabled = false;
	}

	/**
	 * @method setInitialPrice
	 * @description Establece el valor inicial del precio de venta mínimo a partir de la
	 * variable global 'total'.
	 */
	setInitialPrice() {
		if (typeof total !== "undefined" && this.inputMinSalePrice) {
			this.inputMinSalePrice.value = total.toFixed(2);
		}
	}

	/**
	 * @method initListeners
	 * @description Configura los event listeners para los inputs de valor del caso A.
	 * La lógica de secuenciación y la validación se manejan en este método.
	 */
	initListeners() {
		const valueInputs = [
			this.inputDirectCost,
			this.inputIndirectValue,
			this.inputFinancingValue,
			this.inputProfitValue,
		];

		const inputHandler = (event) => {
			const currentInput = event.target;
			const currentIndex = valueInputs.indexOf(currentInput);
			const nextInput = valueInputs[currentIndex + 1];

			const isValid = this.checkSumLimit(currentInput); // <-- Validación controlada

			if (!isValid) {
				// ❌ Si se excede, limpiar el valor actual y desactivar los siguientes
				currentInput.value = "";
				for (let i = currentIndex + 1; i < valueInputs.length; i++) {
					if (valueInputs[i]) {
						valueInputs[i].value = "";
						valueInputs[i].disabled = true;
					}
				}
				return; // No habilitamos el siguiente input
			}

			// ✅ Si no excede y el valor es válido, habilitamos el siguiente input
			if (currentInput.value !== "" && !isNaN(currentInput.value)) {
				if (nextInput) nextInput.disabled = false;
			}
		};

		// Asignamos listeners a cada input
		valueInputs.forEach((input) => {
			if (input) {
				input.addEventListener("input", inputHandler);
				this.listeners.push({
					element: input,
					eventType: "input",
					handler: inputHandler,
				});
			}
		});

		// Validación final diferida (3 segundos)
		let debounceTimeout;
		this.inputProfitValue.addEventListener("input", () => {
			clearTimeout(debounceTimeout);
			debounceTimeout = setTimeout(() => {
				if (this.inputProfitValue.value !== "") {
					this.showFinalBalanceMessage();
				}
			}, 3000);
		});
	}

	/**
	 * @method destroy
	 * @description Remueve todos los listeners de esta instancia para evitar fugas de memoria.
	 */
	destroy() {
		this.listeners.forEach((listener) => {
			listener.element.removeEventListener(
				listener.eventType,
				listener.handler
			);
		});
		this.listeners = [];
	}

	/**
	 * @method checkSumLimit
	 * @description Compara la suma de los componentes de costeo con el precio de venta mínimo.
	 * Si la suma excede el precio de venta, lanza una alerta.
	 */
	checkSumLimit() {
		const direct = parseFloat(this.inputDirectCost.value) || 0;
		const indirect = parseFloat(this.inputIndirectValue.value) || 0;
		const financing = parseFloat(this.inputFinancingValue.value) || 0;
		const profit = parseFloat(this.inputProfitValue.value) || 0;
		const minSalePrice = parseFloat(this.inputMinSalePrice.value) || 0;

		const sumaComponentes = direct + indirect + financing + profit;
		const tolerance = 0.01;

		// 🚨 Validar si el importe de la propuesta no está definido o es cero
		if (!minSalePrice || minSalePrice <= 0) {
			Swal.fire({
				icon: "warning",
				title: "Advertencia: Montos incorrectos",
				html: `
        <p style="font-size:15px; margin-bottom:10px;">
          La suma de los costos no puede exceder el importe del precio de venta.
        </p>
        <span style="color:#e74c3c; font-weight:bold;">Suma de Costos:</span> ${sumaComponentes.toFixed(
					2
				)}<br>
        <span style="color:#3498db; font-weight:bold;">Precio de Venta:</span> ${minSalePrice.toFixed(
					2
				)}
        </p>
      `,
				confirmButtonText: "Entendido",
				confirmButtonColor: "#d33",
				timer: 10000000,
				timerProgressBar: true,
			});

			return false; // ❌ No permitir continuar
		}

		if (minSalePrice > 0 && sumaComponentes - minSalePrice > tolerance) {
			Swal.fire({
				icon: "warning",
				title: "Advertencia: Montos incorrectos",
				html: `
					<p style="font-size:15px; margin-bottom:10px;">
					La suma de los costos no puede exceder el importe del precio de venta.
					</p>
					<span style="color:#e74c3c; font-weight:bold;">Suma de Costos:</span> ${sumaComponentes.toFixed(
								2
							)}<br>
					<span style="color:#3498db; font-weight:bold;">Precio de Venta:</span> ${minSalePrice.toFixed(
								2
							)}
				`,
				confirmButtonText: "Entendido",
				confirmButtonColor: "#d33",
				timer: 3000,
				timerProgressBar: true,
			});

			// 🚫 Devolver falso para evitar habilitar el siguiente input
			return false;
		}

		return true; // ✅ Todo correcto
	}

	checkFinalBalance() {
		const direct = parseFloat(this.inputDirectCost.value) || 0;
		const indirect = parseFloat(this.inputIndirectValue.value) || 0;
		const financing = parseFloat(this.inputFinancingValue.value) || 0;
		const profit = parseFloat(this.inputProfitValue.value) || 0;
		const minSalePrice = parseFloat(this.inputMinSalePrice.value) || 0;

		const sumaComponentes = direct + indirect + financing + profit;
		const tolerance = 0.01;

		if (minSalePrice > 0) {
			if (Math.abs(sumaComponentes - minSalePrice) > tolerance) {
				this.inputProfitValue.value = 0;
				profit = 0;
				alert(
					`Los montos finales no cuadran.\nSuma de Componentes: ${sumaComponentes.toFixed(
						2
					)}\nPrecio de Venta: ${minSalePrice.toFixed(2)}`
				);
			}
		}
	}

	showFinalBalanceMessage() {
		const direct = parseFloat(this.inputDirectCost.value) || 0;
		const indirect = parseFloat(this.inputIndirectValue.value) || 0;
		const financing = parseFloat(this.inputFinancingValue.value) || 0;
		const profit = parseFloat(this.inputProfitValue.value) || 0;
		const minSalePrice = parseFloat(this.inputMinSalePrice.value) || 0;

		const sumaComponentes = direct + indirect + financing + profit;
		const tolerance = 0.01;

		// Si no cuadran, mostrar mensaje temporal
		if (Math.abs(sumaComponentes - minSalePrice) > tolerance) {
			Swal.fire({
				icon: "error",
				title: "Montos incorrectos",
				html: `
					<p style="font-size:15px; margin-bottom:10px;">
					La suma de los costos está por debajo del precio de venta.
					</p>
					<span style="color:#e74c3c; font-weight:bold;">Suma de Costos:</span> ${sumaComponentes.toFixed(
									2
								)}<br>
					<span style="color:#3498db; font-weight:bold;">Precio de Venta:</span> ${minSalePrice.toFixed(
									2
								)}
				`,
				confirmButtonText: "Entendido",
				confirmButtonColor: "#d33",
				timer: 10000000,
				timerProgressBar: true,
			});
			this.inputProfitValue.value = 0;
			//profit = 0;
		}
	}

	// Validar al movimiento de pantalla

	initScreenMovementValidation(minScroll = 30, intervalMs = 1500) {
    this.lastScrollY = window.scrollY;
    this.minScroll = minScroll;
    this.validationInterval = null;

    const triggerValidation = () => {
        if (typeof this.validateSalePrice === "function") {
            this.validateSalePrice();
        }
    };

    const scrollHandler = () => {
        const distance = Math.abs(window.scrollY - this.lastScrollY);
        if (distance >= this.minScroll) {
            this.lastScrollY = window.scrollY;
            triggerValidation();
        }
    };

    const blurHandler = triggerValidation;

    // 🔹 Validación periódica, no invasiva
    const startRepeatingValidation = () => {
        if (!this.validationInterval) {
            this.validationInterval = setInterval(triggerValidation, intervalMs);
        }
    };

    const stopRepeatingValidation = () => {
        if (this.validationInterval) {
            clearInterval(this.validationInterval);
            this.validationInterval = null;
        }
    };

    // Escucha eventos principales
    window.addEventListener("scroll", scrollHandler);
    window.addEventListener("blur", blurHandler);
    window.addEventListener("focus", startRepeatingValidation);
    window.addEventListener("beforeunload", stopRepeatingValidation);

    // Inicia validación periódica
    startRepeatingValidation();

    // Guarda los listeners para poder limpiarlos después
    this.listeners.push(
        { element: window, eventType: "scroll", handler: scrollHandler },
        { element: window, eventType: "blur", handler: blurHandler },
        { element: window, eventType: "focus", handler: startRepeatingValidation },
        { element: window, eventType: "beforeunload", handler: stopRepeatingValidation }
    );
}


	validateSalePrice() {
		const direct = parseFloat(this.inputDirectCost.value) || 0;
		const indirect = parseFloat(this.inputIndirectValue.value) || 0;
		const financing = parseFloat(this.inputFinancingValue.value) || 0;
		const profit = parseFloat(this.inputProfitValue.value) || 0;
		const minSalePrice = parseFloat(this.inputMinSalePrice.value) || 0;

		const sumaComponentes = direct + indirect + financing + profit;
		const tolerance = 0.01;

		if (
			minSalePrice > 0 &&
			Math.abs(sumaComponentes - minSalePrice) > tolerance
		) {
			Swal.fire({
				icon: "error",
				title: "Montos incorrectos",
				html: `
          <p style="font-size:15px; margin-bottom:10px;">
            La suma de los costos no coincide con el precio de venta.
          </p>
          <span style="color:#e74c3c; font-weight:bold;">Suma de Costos:</span> ${sumaComponentes.toFixed(
					2
				)}<br>
          <span style="color:#3498db; font-weight:bold;">Precio de Venta:</span> ${minSalePrice.toFixed(
					2
				)}
        `,
				confirmButtonText: "Entendido",
				confirmButtonColor: "#d33",
				timer: 10000000,
				timerProgressBar: true,
			});
		}
	}

	destroy() {
		this.listeners.forEach(({ element, eventType, handler }) => {
			element.removeEventListener(eventType, handler);
		});
		this.listeners = [];
	}

	/**
	 * @method initPercentFromValues
	 * @description Configura listeners para que cada vez que se cambien los valores, se actualicen los porcentajes correspondientes.
	 */
	initPercentFromValues() {
		const valueInputs = [
			this.inputIndirectValue,
			this.inputFinancingValue,
			this.inputProfitValue,
			this.inputOperatingExpensesValue,
			this.inputMaxDiscountValue,
		];

		const handler = () => this.updatePercentFromValues();

		valueInputs.forEach((input) => {
			if (input) {
				input.addEventListener("input", handler);
				this.listeners.push({ element: input, eventType: "input", handler });
			}
		});
	}

	/**
	 * @method updatePercentFromValues
	 * @description Calcula los porcentajes a partir de los valores ingresados sobre el precio de venta.
	 */
	updatePercentFromValues() {
		const direct = parseFloat(this.inputDirectCost.value) || 0;
		const indirectValue = parseFloat(this.inputIndirectValue.value) || 0;
		const financingValue = parseFloat(this.inputFinancingValue.value) || 0;
		const profitValue = parseFloat(this.inputProfitValue.value) || 0;

		// Cálculos progresivos
		const indirectPercent = (indirectValue / direct) * 100;
		const financingPercent = (financingValue / (direct + indirectValue)) * 100;
		const profitPercent =
			(profitValue / (direct + indirectValue + financingValue)) * 100;

		this.inputIndirectPercent.value = indirectPercent.toFixed(2);
		this.inputFinancingPercent.value = financingPercent.toFixed(2);
		this.inputProfitPercent.value = profitPercent.toFixed(2);
	}
}

/**
 * @class CaseB
 * @description Subclase para el caso de costeo 'B' (Neodata: false).
 * Se centra en el cálculo de valores a partir de porcentajes.
 */
class CaseB extends CostingForm {
	constructor() {
		super();
		this.typeCase = "B";
		this.title.textContent = "Cálculo Financiero";
		this.listeners = []; // Añadido: array para almacenar los listeners
		this.applyCaseBRules();
		this.setInitialDirectCost();
		this.initSequentialInputs();
		this.updateCalculations();
		// <-- Activar validación global de números negativos
		this.attachNonNegativeValidation();
	}

	updateDirectCost() {
		this.inputDirectCost.value = total;
		this.inputMinSalePrice.value = total;
		this.inputListPrice.value = total;
		this.inputProposedAmountValue = total;
	}

	/**
	 * @method setInitialDirectCost
	 * @description Establece el costo directo inicial a partir de la variable 'total' y lo deshabilita.
	 */
	setInitialDirectCost() {
		if (typeof total !== "undefined") {
			this.inputDirectCost.value = total.toFixed(2);
			this.inputDirectCost.disabled = true;
		}
	}

	/**
	 * @method applyCaseBRules
	 * @description Aplica las reglas del caso B: se habilitan los inputs de porcentaje
	 * mientras los de valor se calculan automáticamente.
	 */
	applyCaseBRules() {
		// Todo deshabilitado
		this.allInputs.forEach((input) => (input.disabled = true));

		// El DirectCost lo pone automático con total, se queda deshabilitado
		this.inputDirectCost.disabled = false;

		// Solo el primer porcentaje habilitado
		this.inputIndirectPercent.disabled = false;
	}

	/**
	 * @method initSequentialInputs
	 * @description Configura los listeners para los inputs de porcentaje y el costo directo.
	 * Cualquier cambio en estos inputs dispara una actualización de todos los cálculos.
	 */
	initSequentialInputs() {
		const sequentialOrder = [
			this.inputIndirectPercent, // 1. % indirectos
			this.inputFinancingPercent, // 2. % financiamiento
			this.inputProfitPercent, // 3. % utilidad
			this.inputOperatingExpensesPercent, // 4. % gastos
			this.inputMaxDiscountPercent, // 5. % descuento
		];

		const inputHandler = (event) => {
			const currentIndex = sequentialOrder.indexOf(event.target);
			const nextInput = sequentialOrder[currentIndex + 1];

			// 🚨 Validar cuando se intenta escribir en "% Indirecto"
			if (event.target === this.inputIndirectPercent) {
				const directValue = parseFloat(this.inputDirectCost.value);
				if (!directValue || directValue <= 0 || isNaN(directValue)) {
					Swal.fire({
						icon: "warning",
						text: "Tu catálogo de conceptos está vacío",
						confirmButtonText: "Aceptar",
					}).then(() => {
						const catalogSection = document.getElementById("conceptsCatalog");
						if (catalogSection) {
							catalogSection.scrollIntoView({
								behavior: "smooth",
								block: "center",
							});
						}
					});

					// Limpiar el campo actual e impedir avanzar
					event.target.value = "";
					for (let i = currentIndex; i < sequentialOrder.length; i++) {
						sequentialOrder[i].value = "";
						sequentialOrder[i].disabled = true;
					}
					return;
				}
			}

			// 🔁 Ejecutar cálculos
			const isValidCalc = this.updateCalculations();

			// ⚠️ Si los cálculos o la suma de porcentajes son inválidos, bloquear
			const isValidPercent = this.validateTotalPercent();

			if (!isValidCalc || !isValidPercent) {
				event.target.value = "";
				for (let i = currentIndex + 1; i < sequentialOrder.length; i++) {
					sequentialOrder[i].value = "";
					sequentialOrder[i].disabled = true;
				}
				return;
			}

			// ✅ Si todo está correcto, habilitar el siguiente input
			if (nextInput) nextInput.disabled = false;
		};

		sequentialOrder.forEach((input) => {
			if (input) {
				input.addEventListener("input", inputHandler);
				this.listeners.push({
					element: input,
					eventType: "input",
					handler: inputHandler,
				});
			}
		});
	}

	/**
	 * @method destroy
	 * @description Remueve todos los listeners de esta instancia para evitar fugas de memoria.
	 */
	destroy() {
		this.listeners.forEach((listener) => {
			listener.element.removeEventListener(
				listener.eventType,
				listener.handler
			);
		});
		this.listeners = [];
	}

	/**
	 * @method updateCalculations
	 * @description Realiza todos los cálculos en cascada para el caso B, basándose en el
	 * costo directo y los porcentajes ingresados.
	 */
	/**
	 * @method updateCalculations
	 * @description Realiza todos los cálculos en cascada para el caso B, basándose en el
	 * costo directo y los porcentajes ingresados.
	 */
	updateCalculations() {
		const direct = parseFloat(this.inputDirectCost.value) || 0;

		// Usa los porcentajes para calcular los valores en pesos
		const indirectPercent = parseFloat(this.inputIndirectPercent.value) || 0;
		const indirectValue = (direct * indirectPercent) / 100;
		this.inputIndirectValue.value = indirectValue.toFixed(2);

		const financingPercent = parseFloat(this.inputFinancingPercent.value) || 0;
		const subtotal1 = direct + indirectValue;
		const financingValue = (subtotal1 * financingPercent) / 100;
		this.inputFinancingValue.value = financingValue.toFixed(2);

		const profitPercent = parseFloat(this.inputProfitPercent.value) || 0;
		const subtotal2 = subtotal1 + financingValue;
		const profitValue = (subtotal2 * profitPercent) / 100;
		this.inputProfitValue.value = profitValue.toFixed(2);

		const opPercent = parseFloat(this.inputOperatingExpensesPercent.value) || 0;
		const subtotal3 = subtotal2 + profitValue;
		const opValue = (subtotal3 * opPercent) / 100;
		this.inputOperatingExpensesValue.value = opValue.toFixed(2);

		const salePrice = subtotal3 + opValue;
		this.inputMinSalePrice.value = salePrice.toFixed(2);

		const discountPercent = parseFloat(this.inputMaxDiscountPercent.value) || 0;
		const discountValue = (salePrice * discountPercent) / 100;
		this.inputMaxDiscountValue.value = discountValue.toFixed(2);

		const listPrice = salePrice + discountValue;
		this.inputListPrice.value = listPrice.toFixed(2);

		if (opPercent >= 100) {
			Swal.fire({
				icon: "warning",
				title: "Advertencia: Gastos de operación",
				text: `El porcentaje de gastos de operación (${opPercent.toFixed(
					2
				)}%) no puede superar el 100%.`,
				confirmButtonText: "Entendido",
			});
			return false;
		}

		// Actualiza el importe de la propuesta
		this.updateProposedAmount();

		return true;
	}

	/**
	 * @method validateTotalPercent
	 * @description Valida que la suma de los porcentajes (indirectos, financiamiento,
	 * utilidad, gastos de operación) no exceda el 100% con respecto al costo directo.
	 * Si excede, muestra una advertencia y deshabilita el último input modificado.
	 */
	validateTotalPercent() {
		const indirect = parseFloat(this.inputIndirectPercent.value) || 0;
		const financing = parseFloat(this.inputFinancingPercent.value) || 0;
		const profit = parseFloat(this.inputProfitPercent.value) || 0;
		const op = parseFloat(this.inputOperatingExpensesPercent.value) || 0;

		const totalPercent = indirect + financing + profit;

		if (totalPercent > 100) {
			Swal.fire({
				icon: "warning",
				title: "Advertencia: Montos incorrectos",
				text: `La suma de los porcentajes (${totalPercent.toFixed(
					2
				)}%) no puede exceder el 100% respecto al costo directo.`,
				confirmButtonText: "Entendido",
			});

			return false;
		}
		return true;
	}
}

/**
 * @class CaseC
 * @description Subclase para el caso de costeo 'C' (Neodata: true, Tender: false).
 * Combina inputs de valor y porcentaje para los cálculos.
 */
class CaseC extends CostingForm {
	constructor() {
		super();
		this.typeCase = "C";
		this.title.textContent = "Cálculo Financiero";
		this.listeners = [];
		this.debounceTimeout = null;
		this.applyCaseCRules();
		this.setInitialProposedAmount(); // Fijar importe de la propuesta
		this.initSequentialInputs();
		// // 🚀 Listener con debounce para ProfitValue
		// this.inputProfitValue.addEventListener("input", () => {
		//   clearTimeout(this.debounceTimeout);
		//   this.debounceTimeout = setTimeout(() => {
		//     if (this.inputProfitValue.value.trim() !== "") {
		//       this.showFinalBalanceMessage();
		//     }
		//   }, 500); // 500ms de espera tras dejar de escribir
		// });
		this.attachNonNegativeValidation();
		this.initPercentFromValues();
	}

	/**
	 * Fija el importe de la propuesta igual al total global y lo bloquea.
	 */
	setInitialProposedAmount() {
		if (typeof total !== "undefined") {
			this.inputProposedAmountValue.value = total.toFixed(2);
			this.inputProposedAmountValue.disabled = true;
		}
	}

	/**
	 * En este caso, el costo directo se ingresa manualmente.
	 */
	applyCaseCRules() {
		this.allInputs.forEach((input) => (input.disabled = true));

		// Solo se habilita el Direct Cost al inicio
		this.inputDirectCost.disabled = false;
	}

	/**
	 * Define la secuencia de inputs para cálculo progresivo.
	 */
	initSequentialInputs() {
		const sequentialOrder = [
			this.inputDirectCost,
			this.inputIndirectValue,
			this.inputFinancingValue,
			this.inputProfitValue,
			this.inputOperatingExpensesPercent,
			this.inputMaxDiscountPercent,
		];

		const inputHandler = (event) => {
			const currentIndex = sequentialOrder.indexOf(event.target);
			const nextInput = sequentialOrder[currentIndex + 1];

			// Cancelamos debounce previo
			clearTimeout(this.debounceTimeout);
			this.debounceTimeout = setTimeout(() => {
				const isValidPartial = this.updateSalePrice(); // Validación parcial
				const isValidFinal = this.showFinalBalanceMessage(); // Validación final de suma

				if (!isValidPartial || !isValidFinal) {
					// ❌ Si no es válido, resetear input actual y desactivar los siguientes
					event.target.value = "";
					for (let i = currentIndex + 1; i < sequentialOrder.length; i++) {
						sequentialOrder[i].value = "";
						sequentialOrder[i].disabled = true;
					}
					return;
				}

				// ✅ Solo si es válido, habilitar siguiente input
				if (nextInput) nextInput.disabled = false;
			}, 500); // 500ms de espera tras último dígito
		};

		sequentialOrder.forEach((input) => {
			if (input) {
				input.addEventListener("input", inputHandler);
				this.listeners.push({
					element: input,
					eventType: "input",
					handler: inputHandler,
				});
			}
		});

		const percentHandler = () => this.updateListPrice();
		this.inputOperatingExpensesPercent.addEventListener(
			"input",
			percentHandler
		);
		this.inputMaxDiscountPercent.addEventListener("input", percentHandler);
	}

	/**
	 * Calcula el precio de venta y valida que no supere el importe de la propuesta.
	 */
	updateSalePrice() {
		const proposed = parseFloat(this.inputProposedAmountValue.value) || 0;
		const direct = parseFloat(this.inputDirectCost.value) || 0;
		const indirect = parseFloat(this.inputIndirectValue.value) || 0;
		const financing = parseFloat(this.inputFinancingValue.value) || 0;
		const profit = parseFloat(this.inputProfitValue.value) || 0;
		const opPercent = parseFloat(this.inputOperatingExpensesPercent.value) || 0;

		// 🚫 Si algún campo crítico está vacío o no es número, no hacemos la validación todavía
		if (
			isNaN(proposed) ||
			isNaN(direct) ||
			isNaN(indirect) ||
			isNaN(financing) ||
			isNaN(profit)
		) {
			return true; // considerado válido hasta que todos los valores estén completos
		}

		const subtotal = direct + indirect + financing + profit;

		if (subtotal > proposed) {
			Swal.fire({
				icon: "warning",
				title: "Advertencia: Montos incorrectos",
				text: "La suma de los costos no puede exceder el importe de la propuesta.",
				confirmButtonText: "Entendido",
			});
			return false; // ⚠️ Indica que hubo error
		}

		if (opPercent > 100) {
			Swal.fire({
				icon: "error",
				title: "Gastos de operación inválidos",
				text: "El porcentaje de gastos de operación no puede ser mayor al 100%.",
				confirmButtonText: "Entendido",
			});
			return false; // ⚠️ Indica que hubo error
		}

		const opValue = (proposed * opPercent) / 100;
		this.inputOperatingExpensesValue.value = opValue.toFixed(2);

		const minSalePrice = proposed + opValue;
		this.inputMinSalePrice.value = minSalePrice.toFixed(2);

		this.updateListPrice();
		return true; // ✅ Todo bien
	}

	showFinalBalanceMessage() {
		// Leer los valores en crudo
		const directValue = this.inputDirectCost.value.trim();
		const indirectValue = this.inputIndirectValue.value.trim();
		const financingValue = this.inputFinancingValue.value.trim();
		const profitValue = this.inputProfitValue.value.trim();
		const proposedValue = this.inputProposedAmountValue.value.trim();

		// 🚫 Si algún campo está vacío, no hacer nada
		if (
			!directValue ||
			!indirectValue ||
			!financingValue ||
			!profitValue ||
			!proposedValue
		) {
			return true;
		}

		// Convertir a números
		const direct = parseFloat(directValue);
		const indirect = parseFloat(indirectValue);
		const financing = parseFloat(financingValue);
		const profit = parseFloat(profitValue);
		const proposed = parseFloat(proposedValue);

		const sumaComponentes = direct + indirect + financing + profit;
		const tolerance = 0.01;

		// Si la suma no coincide con el propuesto, mostrar alerta
		if (Math.abs(sumaComponentes - proposed) > tolerance) {
			Swal.fire({
				icon: "error",
				title: "Montos incorrectos",
				html: `
        <p style="font-size:15px; margin-bottom:10px;">
          La suma de los costos está por debajo del precio de venta.
        </p>
        <span style="color:#e74c3c; font-weight:bold;">Suma de Costos:</span> ${sumaComponentes.toFixed(
					2
				)}<br>
        <span style="color:#3498db; font-weight:bold;">Precio de Venta:</span> ${proposed.toFixed(
					2
				)}
      `,
				confirmButtonText: "Entendido",
				confirmButtonColor: "#d33",
				timer: 10000000,
				timerProgressBar: true,
			});
			return false;
		}
		return true;
	}

	/**
	 * Recalcula el precio de lista (venta final con descuento)
	 */
	updateListPrice() {
		const minSalePrice = parseFloat(this.inputMinSalePrice.value) || 0;
		const discountPercent = parseFloat(this.inputMaxDiscountPercent.value) || 0;

		const discountValue = (minSalePrice * discountPercent) / 100;
		this.inputMaxDiscountValue.value = discountValue.toFixed(2);

		const listPrice = minSalePrice + discountValue;
		this.inputListPrice.value = listPrice.toFixed(2);
	}

	/**
	 * Actualiza porcentajes según los valores ingresados (opcional)
	 */
	initPercentFromValues() {
		const valueInputs = [
			this.inputIndirectValue,
			this.inputFinancingValue,
			this.inputProfitValue,
			this.inputOperatingExpensesValue,
			this.inputMaxDiscountValue,
		];

		const handler = () => this.updatePercentFromValues();

		valueInputs.forEach((input) => {
			if (input) {
				input.addEventListener("input", handler);
				this.listeners.push({ element: input, eventType: "input", handler });
			}
		});
	}

	updatePercentFromValues() {
		const direct = parseFloat(this.inputDirectCost.value) || 0;
		const indirect = parseFloat(this.inputIndirectValue.value) || 0;
		const financing = parseFloat(this.inputFinancingValue.value) || 0;
		const profit = parseFloat(this.inputProfitValue.value) || 0;

		if (direct > 0)
			this.inputIndirectPercent.value = ((indirect / direct) * 100).toFixed(2);
		if (direct + indirect > 0)
			this.inputFinancingPercent.value = (
				(financing / (direct + indirect)) *
				100
			).toFixed(2);
		if (direct + indirect + financing > 0)
			this.inputProfitPercent.value = (
				(profit / (direct + indirect + financing)) *
				100
			).toFixed(2);
	}

	destroy() {
		this.listeners.forEach((listener) => {
			listener.element.removeEventListener(
				listener.eventType,
				listener.handler
			);
		});
		this.listeners = [];
	}
}

const initCaseBySwitches = () => {
	// Si ya hay un caso activo, se eliminan sus listeners y se resetean los inputs
	if (typeof total === "undefined" || total <= 0) {

		document
			.querySelectorAll(
				".costeo input,.costeo input[type='checkbox'],.costeo select, .costeo textarea, .adjuntos input[type='file'], .condiciones input, .condiciones textarea, .boton-final boton-final-enviar, .adjuntos .file-input-container"
			)
			.forEach((el) => {
				el.value = "";
				el.checked = false;
				el.disabled = true;
			});

		// Aplicar estilo gris solo a los botones del formulario de cotización
		document.querySelectorAll(".btn-styles-cotizForm").forEach((btn) => {
			btn.style.backgroundColor = "#ccc";
			btn.style.color = "#666";
			btn.style.cursor = "not-allowed";
			btn.style.pointerEvents = "none";
			btn.style.transition = "none";
		});

		activeCase = null; // evitar inicialización
		return;
	}

	if (activeCase) {
		if (typeof activeCase.destroy === "function") {
			activeCase.destroy();
		}
		activeCase.resetInputs();
	}

	// Habilitamos los selects
	document
		.querySelectorAll(
			".costeo .form-switch-Neodata, .adjuntos input[type='file'], .condiciones input, .condiciones textarea, .boton-final boton-final-enviar, .adjuntos .file-input-container"
		)
		.forEach((el) => {
			el.disabled = false;
			el.style.transition = ""; // opcional
		});

	document.querySelectorAll(".btn-styles-cotizForm").forEach((btn) => {
		btn.style.backgroundColor = "";
		btn.style.color = "";
		btn.style.cursor = "pointer";
		btn.style.pointerEvents = "auto";
		btn.style.transition = "";
	});
	const selectUnidad = document.getElementById("unidadTiempo");
	selectUnidad.style.backgroundColor = "white";

	isNeodata = switchNeodataOrigin.checked;
	isTender = switchTender.checked;

	// Lógica condicional para determinar qué caso instanciar
	if (isNeodata && isTender) {
		activeCase = new CaseA();
	} else if (!isNeodata) {
		activeCase = new CaseB();
	} else if (isNeodata && !isTender) {
		activeCase = new CaseC();
	} else {
	}
};

/**
 * @description Lógica de inicialización principal que se ejecuta cuando el DOM está completamente cargado.
 * Maneja la creación de la instancia de la clase de costeo correcta según los estados iniciales
 * y los cambios en los switches.
 */
document.addEventListener("DOMContentLoaded", () => {
	initCaseBySwitches();

	switchNeodataOrigin.addEventListener("change", initCaseBySwitches);
	switchTender.addEventListener("change", initCaseBySwitches);
});

async function downloadFile() {
	const id = "1";
	try {
		const response = await fetch(`/api/ftp/download/${id}`);
		if (!response.ok) throw new Error("No se pudo descargar el archivo");

		// 1. Tomar el nombre desde el header Content-Disposition
		let filename = "archivo_descargado";
		const disposition = response.headers.get("Content-Disposition");
		if (disposition && disposition.includes("filename=")) {
			// Extraer el nombre después de filename=
			const match = disposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/);
			if (match && match[1]) {
				filename = match[1].replace(/['"]/g, ""); // quitar comillas
			}
		}

		// 2. Obtener el blob
		const blob = await response.blob();

		// 3. Crear URL temporal para descarga
		const url = window.URL.createObjectURL(blob);
		const a = document.createElement("a");
		a.href = url;
		a.download = filename; // 👈 ahora usamos el nombre original
		document.body.appendChild(a);
		a.click();

		// 4. Limpiar
		a.remove();
		window.URL.revokeObjectURL(url);
	} catch (error) {
		console.error(error);
		alert("Error al descargar el archivo.");
	}
}
// JS
function initTiempoEjecucionForm() {
	const form = document.getElementById("formTiempo");
	const inputTiempo = document.getElementById("tiempoEjecucion");
	const selectUnidad = document.getElementById("unidadTiempo");
	const btnEnviar = document.getElementById("boton-final-enviar");

	// Inicial: deshabilitado y forzamos el color
	setSelectState(true);

	function setSelectState(disabled) {
		selectUnidad.disabled = !!disabled;
		if (disabled) {
			selectUnidad.style.setProperty(
				"background-color",
				"#CCCCCC",
				"important"
			);
			selectUnidad.style.setProperty("color", "#666666", "important");
			selectUnidad.style.setProperty("cursor", "not-allowed", "important");
			selectUnidad.value = "";
		} else {
			selectUnidad.style.setProperty("background-color", "white", "important");
			selectUnidad.style.setProperty("color", "#000000", "important");
			selectUnidad.style.removeProperty("cursor");
		}
	}

	function evaluateAndSync() {
		const value = inputTiempo.value.trim();

		// Si alguien deshabilitó el input desde otro sitio, forzamos select gris
		if (inputTiempo.disabled) {
			setSelectState(true);
			return;
		}

		// Si hay valor > 0 activamos select, si no lo desactivamos
		if (value !== "" && parseFloat(value) > 0) {
			setSelectState(false);
		} else {
			setSelectState(true);
		}
	}

	// Impedir valores negativos o cero
	inputTiempo.addEventListener("input", () => {
		// Si el usuario escribe algo que no sea número, lo limpia
		const val = inputTiempo.value;

		// Bloquear signos negativos o ceros al inicio
		if (parseFloat(val) <= 0) {
			inputTiempo.value = ""; // lo limpia
		}

		evaluateAndSync();
	});

	// Observador por si algún otro script cambia el atributo disabled del input
	const mo = new MutationObserver((muts) => {
		for (const m of muts) {
			if (m.type === "attributes" && m.attributeName === "disabled") {
				evaluateAndSync();
			}
		}
	});
	mo.observe(inputTiempo, { attributes: true, attributeFilter: ["disabled"] });

	// Validación y envío
	btnEnviar.addEventListener("click", () => {
		const tiempo = inputTiempo.value.trim();
		const unidad = selectUnidad.value;

		const validador = new buttonSendCotiz()

		if (validador.is_valid_CotizCreatedIng) {
			enviarArchivosCotizCreatedIng();
		}
	});


	// Ejecutar una vez al inicio
	evaluateAndSync();

}

initTiempoEjecucionForm();

// La función `initTaskSearch` es la que inicia toda la lógica de búsqueda.
function initTaskSearch() {
	console.log("initTaskSearch se está ejecutando correctamente.");

	const opnumberInput = document.getElementById("opnumber");
	const opbtn = document.getElementById("opbtn");

	opbtn.addEventListener("click", async () => {
		const taskId = opnumberInput.value.trim();

		if (!taskId) {
			Swal.fire({
				icon: "warning",
				title: "Entrada requerida",
				text: "Por favor, ingrese un número de tarea.",
				confirmButtonText: "Aceptar",
			});
			return;
		}

		// Llamar a la función del backend para obtener la tarea
		const { task, message } = await fetchQuotationTaskById(taskId);

		if (task) {
			console.log("Tarea encontrada:", task);
			Swal.fire({
				icon: "success",
				html: message,
				confirmButtonText: "Aceptar",
			});
			populateOpportunityForm(task);
		} else {
			Swal.fire({
				icon: "error",
				text: message,
				confirmButtonText: "Aceptar",
			});
		}
	});
}

function populateOpportunityForm(taskData) {
	// Si los datos de la oportunidad existen en el objeto de la tarea
	if (taskData && taskData.crm_data) {
		const crmData = taskData.crm_data;

		// Asignar datos de la oportunidad a los inputs
		document.getElementById("CRM_OpportunityNumber").value =
			crmData.CRM_OpportunityNumber || "";
		document.getElementById("CRM_ContactName").value =
			crmData.CRM_ContactName || "";
		document.getElementById("CRM_ContactType").value =
			crmData.CRM_ContactType || "";
		document.getElementById("CRM_AssignedSalesperson").value =
			crmData.CRM_AssignedSalesperson || "";
		document.getElementById("CRM_ContactAdress").value =
			crmData.CRM_ContactAdress || "";
		document.getElementById("CRM_ContactColonia").value =
			crmData.CRM_ContactColonia || "";
		document.getElementById("CRM_ContactCity").value =
			crmData.CRM_ContactCity || "";
		document.getElementById("CRM_ContactNumber").value =
			crmData.CRM_ContactNumber || "";
		document.getElementById("CRM_ContactCountry").value =
			crmData.CRM_ContactCountry || "";
		document.getElementById("CRM_ContactLegalIdentifier").value =
			crmData.CRM_ContactLegalIdentifier || "";
		document.getElementById("CRM_ContactZip").value =
			crmData.CRM_ContactZip || "";
		document.getElementById("CRM_ContactState").value =
			crmData.CRM_ContactState || "";
		document.getElementById("CRM_ContactEmail").value =
			crmData.CRM_ContactEmail || "";
	}
}



// Esta es la función que realiza la llamada a la API
async function fetchQuotationTaskById(taskId) {
	const url = `/api/quotation/cotiz/tasks/${taskId}`;

	try {
		const response = await axios.get(url, {
			headers: {
				Accept: "application/json",
			},
		});

		// El API debe devolver un único objeto, no un array
		const task = response.data;
		task_SellerUserID = task.SellerUserID
		task_CRM_OpportunityID = task.CRM_OpportunityID
		task_FormID = task.FormID
		task_TaskID = task.TaskID
		task_Status = task.Status

		// Si la respuesta es exitosa pero el objeto está vacío, se retorna null
		if (!task || Object.keys(task).length === 0) {
			return {
				message: "No se encontró la tarea",
				task: null
			}
		}

		// Verifica que esta atendido
		if (task_Status === 'COMPLETADO') {
			return {
				message: "La tarea está completada",
				task: null
			}
		}
		else {
			return {
				message: "Tarea encontrada con éxito",
				task: task
			}
		}

	} catch (error) {
		if (
			axios.isAxiosError(error) &&
			error.response &&
			error.response.status === 404
		) {
			console.log(`Tarea con ID ${taskId} no encontrada.`);
		} else {
			console.error(
				"Error al obtener la tarea:",
				error.response ? error.response.data : error.message
			);
		}
		return {
			message: "No se encontro la tarea",
			task: null
		}
	}
}

// cuando la página se cargue por completo.
initTaskSearch();





// DTO para el estado de validación del botón de enviar cotización
class buttonSendCotiz_StatusValidationMessageDTO {
	constructor(valid, message) {
		this.isValid = valid;
		this.message = message;
	}
}


// Lógica para el botón de enviar cotización
class buttonSendCotiz {

	is_valid_Forms_opotunity() {
		if (document.getElementById("CRM_OpportunityNumber").value !== "") {
			return new buttonSendCotiz_StatusValidationMessageDTO(true, "OK");
		}
		return new buttonSendCotiz_StatusValidationMessageDTO(false, "Falta el número de oportunidad");
	}


	is_valid_Forms_ConceptsCatalog() {
		local = new LocalDb();
		if (local.isEmpty()) {
			return new buttonSendCotiz_StatusValidationMessageDTO(false, "El catálogo de conceptos está vacío");
		}
		return new buttonSendCotiz_StatusValidationMessageDTO(true, "OK");
	}


	is_valid_Forms_FinancialCalculations() {
		console.log("activeCase:", activeCase);
		if (activeCase === null) {
			return new buttonSendCotiz_StatusValidationMessageDTO(false, "No hay cálculo financiero activo");
		}

		if (activeCase) {

			//valiacion de suma de costos
			if (activeCase.typeCase === 'C' || activeCase.typeCase === 'A') {
					// Validacion suma correta
				const directCost = parseFloat(document.getElementById("inputDirectCost").value) || 0;
				const indirectCost = parseFloat(document.getElementById("inputIndirectValue").value) || 0;
				const financingCost = parseFloat(document.getElementById("inputFinancingValue").value) || 0;
				const profitCost = parseFloat(document.getElementById("inputProfitValue").value) || 0;
				const operatingExpensesCost = parseFloat(document.getElementById("inputOperatingExpensesValue").value) || 0;

				console.log("Costos:", {
					directCost,
					indirectCost,
					financingCost,
					profitCost,
					precioVenta: document.getElementById("inputMinSalePrice").value,
				});

					if ((directCost + indirectCost + financingCost + profitCost + operatingExpensesCost) !== parseFloat(precioVenta)) {
						console.error("La suma de los costos no coincide con el precio de venta");
						return new buttonSendCotiz_StatusValidationMessageDTO(false, "La suma de los costos no coincide con el precio de venta");
					}
				}


			if (activeCase.typeCase === 'A') {
				// Validaciones específicas para el tipo A
				let precioVenta = document.getElementById("inputMinSalePrice").value;
				if (precioVenta === "" || Number(precioVenta) <= 0) {
					return new buttonSendCotiz_StatusValidationMessageDTO(false, "Precio de venta inválido");
				}
				
				


			} else if (activeCase.typeCase === 'B' || activeCase.typeCase === 'C') {
				// Validaciones específicas para el tipo B y C
				const precioLista = document.getElementById("inputListPrice").value;
				const descuentoMaximo = document.getElementById("inputMaxDiscountPercent").value;
				const precioVenta = document.getElementById("inputMinSalePrice").value;

				// Todos son numeros  validacion
				if (isNumber(precioLista) && isNumber(descuentoMaximo) && isNumber(precioVenta)) {
					if (!(precioLista > 0 && descuentoMaximo >= 0 && precioVenta > 0)) {
						return new buttonSendCotiz_StatusValidationMessageDTO(false, "Hay campos que no son números válidos en los cálculos financieros");
					}
				}
				else {
					return new buttonSendCotiz_StatusValidationMessageDTO(false, "Hay campos que no son números válidos en los cálculos financieros");
				}

				
			}
		}
	}


	is_valid_CotizCreatedIng() {

		validations = [
			this.is_valid_Forms_opotunity(),
			this.is_valid_Forms_ConceptsCatalog(),
			this.is_valid_Forms_FinancialCalculations()
		]

		console.log("Validaciones:", validations);

		for (let i = 0; i < validations.length; i++) {
			if (validations[i].isValid === false) {
				Swal.fire({
					icon: "warning",
					title: "Validación fallida",
					text: validations[i].message
				});
				return false;
			}
		}
		return true;

	}

}


class TerminosYCondiciones {
	toJson() {
		const adress = document.getElementById("address")?.value;
		return adress
	}
}

class TiempoDeEjecucion {
	/**
	 * Convierte los valores del formulario en un objeto JSON serializable.
	 * @returns {{ RunTimeNumber: string, RunTimeType: string }}
	 */
	toJson() {
		const tiempoEjecucion = document.getElementById("tiempoEjecucion")?.value || "";
		const unidadTiempo = document.getElementById("unidadTiempo")?.value || "";

		return {
			RunTimeNumber: tiempoEjecucion,
			RunTimeType: unidadTiempo
		};
	}
}



function datosOportunidadToJson() {
	// Helper para asegurar que siempre devuelva string limpio
	const toStr = (id) => {
		const el = document.getElementById(id);
		return el && el.value !== undefined ? String(el.value).trim() : "";
	};

	return {
		CRM_OpportunityNumber: toStr("CRM_OpportunityNumber"),
		CRM_ContactName: toStr("CRM_ContactName"),
		CRM_ContactType: toStr("CRM_ContactType"),
		CRM_AssignedSalesperson: toStr("CRM_AssignedSalesperson"),
		CRM_ContactAdress: toStr("CRM_ContactAdress"),
		CRM_ContactColonia: toStr("CRM_ContactColonia"),
		CRM_ContactCity: toStr("CRM_ContactCity"),
		CRM_ContactNumber: toStr("CRM_ContactNumber"),
		CRM_ContactCountry: toStr("CRM_ContactCountry"),
		CRM_ContactLegalIdentifier: toStr("CRM_ContactLegalIdentifier"),
		CRM_ContactZip: toStr("CRM_ContactZip"),
		CRM_ContactState: toStr("CRM_ContactState"),
		CRM_ContactEmail: toStr("CRM_ContactEmail"),
	};
}

async function getUserID() {
	try {
		const res = await fetch("/api/Operaciones/Ingenieria/Cotiz/CotizCreatedIng/user_id", {
			credentials: "include" // 🔹 Importante para que Flask lea la sesión
		});
		const data = await res.json();

		if (res.ok) {
			console.log("✅ UserID:", data.user_id);
			return data.user_id;
		} else {
			console.warn("⚠️ Error:", data.error);
			return null;
		}
	} catch (err) {
		console.error("❌ Error al obtener el UserID:", err);
		return null;
	}
}






async function cotizCreatedIng_ToJson(docs_id) {
	console.warn("es")
	console.warn(docs_id)
	let localDb = new LocalDb();

	const htmlData = {
		opportunity: datosOportunidadToJson(),
		items: localDb.getAll(),
		financialCalculation: activeCase.toJson(),
		termsAndConditions: new TerminosYCondiciones().toJson(),
		executionTime: new TiempoDeEjecucion().toJson()
	}


	console.log("HTML Data:", htmlData);

	const res = await fetch("/api/Operaciones/Ingenieria/Cotiz/CotizCreatedIng/user_id", {
		credentials: "include",
	});
	const data = await res.json();

	if (!res.ok) throw new Error(data?.error || "Respuesta no OK");

	Q_CostingHead_data = JSON.stringify({
		// 🔹 Automático: generado como CONCAT(CostingNum, '-', Version)
		// "CostingID": "1000-1",

		// 🔹 Automático: valor por defecto del sistema (fecha actual)
		// "CostingDate": "2025-10-14T10:00:00",

		// 🔹 Automático: IDENTITY(1000,1)
		// "CostingNum": 1000,
		"TaskID": task_TaskID,

		"Version": 1,
		// "QuotationTypeID": sub1.QuotationTypeID,
		"UserID": data.user_id,
		// "RoleID": sub2.RoleID,
		"SellerUserID": task_SellerUserID,
		// "CompanyID": sub2.CompanyID,
		// "DivisionID": sub2.DivisionID,
		// "DepartamentID": sub2.DepartamentID,

		"CaseCost": activeCase.typeCase,

		// 🔹 Datos de financialCalculation (tomados desde htmlData)
		"DirectCost": htmlData.financialCalculation.directCost,
		"IndirectAmount": htmlData.financialCalculation.indirect.amount,
		"IndirectPercent": htmlData.financialCalculation.indirect.percent,
		"FinanceAmount": htmlData.financialCalculation.financing.amount,
		"FinancePercent": htmlData.financialCalculation.financing.percent,
		"UtilityAmount": htmlData.financialCalculation.profit.amount,
		"UtilityPercent": htmlData.financialCalculation.profit.percent,
		"OperationAmount": htmlData.financialCalculation.operatingExpenses.amount,
		"OperationPercent": htmlData.financialCalculation.operatingExpenses.percent,
		"SalePriceMin": htmlData.financialCalculation.minSalePrice,
		"DiscountMaxAmount": htmlData.financialCalculation.maxDiscount.amount,
		"DiscountMaxPercent": htmlData.financialCalculation.maxDiscount.percent,
		"SalePriceList": htmlData.financialCalculation.listPrice,
		"OvercostFactor": 0,//htmlData.financialCalculation.proposalAmount ?? 0,

		"Active": true,

		// 🔹 CRM y control de proceso
		"CRM_OpportunityID": task_CRM_OpportunityID,
		"FormID": task_FormID,
		"ApprovalProcessID": null,
		"PreviusDocsID": null,
		"DocsID": docs_id,

		// 🔹 Bloques Terms y Execution Time desde htmlData
		"TechnicalTermsAndConditions": htmlData.termsAndConditions,
		"RunTimeNumber": htmlData.executionTime.RunTimeNumber,
		"RunTimeType": htmlData.executionTime.RunTimeType,
		"Q_CostingDetail": localDb.to_Json_Q_CostingDetail(1)
	});


	console.log(Q_CostingHead_data);

	return await enviarCostingCompleto(Q_CostingHead_data);
}


// 🔹 Función para enviar al backend
async function enviarCostingCompleto(data) {
	try {
		const response = await fetch('/api/Operaciones/Ingenieria/Cotiz/CotizCreatedIng/Q_CostingHead/Completo', {
			method: 'POST',
			headers: {
				'Content-Type': 'application/json'
			},
			body: data
		});

		if (!response.ok) {
			const error = await response.json();
			throw new Error(error.error || 'Error al crear costing');
		}

		return await response.json();

	} catch (error) {
		console.error("Error al enviar costing:", error);
		alert("Error al crear la cotización: " + error.message);
		throw error;
	}
}


/**
 * Envía los archivos seleccionados al backend Flask para CotizCreatedIng.
 * Usa el endpoint: /api/Operaciones/Ingenieria/Cotiz/CotizCreatedIng/uploadFile
 */
/**
* Envía la cotización y, si existen, los archivos seleccionados al backend.
* 1. Crea la entrada de cotización (Q_CostingHead).
* 2. Si hay archivos, los sube.
* 3. Si la subida es exitosa, actualiza la cotización con el DocsID.
*/

async function enviarArchivosCotizCreatedIng() {

	const validador = new buttonSendCotiz()

	if (validador.is_valid_CotizCreatedIng) {

	
		try {
			// 🔹 1. Obtener archivos (no falla si está vacío)
			const uploader = document.getElementById('uploader');
			const archivos = (globalVariables?.getFiles && globalVariables.getFiles()) || uploader?.files || [];

			// 🔹 2. Mostrar mensaje de carga genérico con advertencia
			Swal.fire({
				title: 'Guardando cotización',
				html: `
					<p>Por favor, espere...</p>
					<div style="margin-top: 15px; padding: 10px; background-color: #fff3cd; border-left: 4px solid #ffc107; border-radius: 4px;">
						<strong style="color: #856404;">⚠️ Importante:</strong>
						<p style="color: #856404; margin: 5px 0 0 0; font-size: 0.9em;">
							No cierre ni actualice esta ventana mientras el proceso esté en curso.
						</p>
					</div>
				`,
				allowOutsideClick: false,
				showConfirmButton: false,
				
				didOpen: () => Swal.showLoading(),
			});

			// 🔹 3. Crear la cotización (Q_CostingHead) PRIMERO
			let docsID_tmp = null;


			
			let responseData = await cotizCreatedIng_ToJson(docsID_tmp);
			let TaskID = responseData.data.TaskID;
			const newCostingID = responseData.data.CostingID;
			const newCostingNum = responseData.data.CostingNum;


			if (!newCostingID) {
				throw new Error("No se pudo obtener el CostingID después de crear la cotización.");
			}

			// 🔹 4. Enviar correo automáticamente
			try {
				const emailResponse = await fetch(`/Ingenieria/Cotiz/CotizCreatedIng/get_data_for_email?task_id=${task_TaskID}`, {
					method: 'GET',
					credentials: 'include'
				});
				const emailResult = await emailResponse.json();

				if (emailResponse.ok && emailResult.success) {
					console.log("📧 Correo enviado correctamente");
				} else {
					console.warn("⚠️ El correo no se envió:", emailResult.mensaje || emailResult);
				}
			} catch (emailError) {
				console.error("💥 Error al enviar correo:", emailError);
			}

			// 🔹 5. Verificar si hay archivos para subir
			if (archivos && archivos.length > 0) {
				Swal.update({
					html: `
						<p>Cotización creada (ID: ${newCostingID}).</p>
						<p>Subiendo <strong>${archivos.length}</strong> archivo(s)...</p>
						<div style="margin-top: 15px; padding: 10px; background-color: #fff3cd; border-left: 4px solid #ffc107; border-radius: 4px;">
							<strong style="color: #856404;">⚠️ Importante:</strong>
							<p style="color: #856404; margin: 5px 0 0 0; font-size: 0.9em;">
								No cierre ni actualice esta ventana mientras el proceso esté en curso.
							</p>
						</div>
					`
				});

				const config = configurarTimeoutDinamico(archivos.length);
				const controller = new AbortController();
				const timeout = setTimeout(() => controller.abort(), config.timeoutMs);

				const formData = new FormData();
				for (const file of archivos) {
					formData.append('archivos[]', file);
				}
				formData.append('categoria', 'CotizCreatedIng');
				formData.append("CostingID", newCostingID);

				const response = await fetch(
					'/api/Operaciones/Ingenieria/Cotiz/CotizCreatedIng/uploadFile',
					{
						method: 'POST',
						body: formData,
						// signal: controller.signal,
					}
				);

				const result = await response.json();
				clearTimeout(timeout);

				if (response.ok && result.success) {
					console.log("Actualizando DocsID en CostingHead...");
					const updateResponse = await fetch('/api/Operaciones/Ingenieria/Cotiz/CotizCreatedIng/updateCostingHead-docsID', {
						method: 'POST',
						headers: { 'Content-Type': 'application/json' },
						body: JSON.stringify({
							"CostingID": newCostingID,
							"DocsID": result.docs_id
						})
					});

					if (!updateResponse.ok) {
						const updateError = await updateResponse.json();
						throw new Error(updateError.error || "Error al actualizar el DocsID en la cotización.");
					}

					await Swal.fire({
						icon: 'success',
						title: '✅ Cotización y archivos guardados',
						html: `
							<p># Costeo <strong>${newCostingNum}</strong></p>
							<p>Archivos subidos: <strong>${result.archivos_subidos}</strong></p>
						`,
						confirmButtonText: 'Continuar',
					});

					// Recargar la página para limpiar el caché
					window.location.reload(true);

				} else {
					console.error('❌ Error al subir:', result);
					throw new Error(result.message || 'Error al subir los archivos.');
				}

			} else {
				// 🔹 6. No hay archivos que subir, mostrar mensaje simple
				await Swal.fire({
					icon: 'success',
					title: '✅ Cotización guardada',
					html: `
						<p>Cotización ID: <strong>${newCostingNum}</strong></p>
						<p>(No se adjuntaron archivos)</p>
					`,
					confirmButtonText: 'Continuar',
				});

				// Recargar la página para limpiar el caché
				window.location.reload(true);
			}

		} catch (error) {
			// 🔹 7. Manejo de errores (general)
			console.error('💥 Error en enviarArchivosCotizCreatedIng:', error);
			Swal.close();

			if (error.name === 'AbortError') {
				await Swal.fire({
					icon: 'error',
					title: '⏰ Tiempo excedido',
					text: 'La carga tardó demasiado y fue cancelada automáticamente.',
					confirmButtonText: 'Entendido',
				});
			} else {
				await Swal.fire({
					icon: 'error',
					title: '💥 Error al guardar',
					text: error.message || 'Ocurrió un error inesperado. Revisa la consola.',
					confirmButtonText: 'Entendido',
				});
			}
		}
	}
}


async function enviarEmailPorTask(taskId) {
    try {
        const res = await fetch(`/api/Operaciones/Ingenieria/Cotiz/CotizCreatedIng/get_data_for_email?task_id=${taskId}`, {
            method: 'GET',
            credentials: 'include'  // 🔐 importante si necesitas sesión
        });

        const data = await res.json();

        if (res.ok) {
            console.log("✅ Correo enviado:", data);
        } else {
            console.warn("⚠️ Error al enviar correo:", data);
        }
    } catch (err) {
        console.error("❌ Error de red o servidor:", err);
    }
}


/**
 * Calcula el timeout y tiempo estimado dinámicamente según número de archivos.
 */
function configurarTimeoutDinamico(numArchivos) {
	// 🔹 Cálculo basado en la lógica revisada: (n * 40s) + 30s base
	let tiempoEstimadoSegundos = (numArchivos * 40) + 30;

	// 🔹 Convertir a milisegundos
	let timeoutMs = tiempoEstimadoSegundos * 1000;

	// 🔹 Limitar entre 60s y 300s (1 a 5 minutos)
	timeoutMs = Math.min(Math.max(timeoutMs, 60_000), 300_000);

	// 🔹 Calcular minutos redondeados hacia arriba
	const estimadoMinutos = Math.ceil(timeoutMs / 60000);

	return { timeoutMs, estimadoMinutos };
}


document.addEventListener('DOMContentLoaded', async function () {

	document.getElementById('btnEnviar').addEventListener('click', enviarArchivosCotizCreatedIng);



	if (window.customElements?.whenDefined) {
		await customElements.whenDefined('upload-file-component');
	}

	const uploader = document.getElementById('uploader');
	if (!uploader) {
		console.error('No se encontró #uploader');
		return;
	}

	const reflectToGlobal = (ev) => {
		const { files = [], plainFiles = [], reason, count } = ev.detail || {};
		// 💾 Reemplaza TODO el estado global con la lista completa
		globalVariables.setAllFiles(files, plainFiles);
		console.log('Global actualizado:', { reason, count, files, plainFiles });
	};

	// Cualquiera de los dos; ambos emiten lista completa en tu componente
	uploader.addEventListener('filesselected', reflectToGlobal);
	// uploader.addEventListener('fileschanged', reflectToGlobal);

	// (opcional) inicializa vacío al cargar
	globalVariables.clearFiles();
});