Created
April 5, 2025 00:46
-
-
Save DeveloperArmando/5bee01bd3d133748c63c0e2b9c2f11ae to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * Función principal para calcular fechas de tareas con manejo avanzado de dependencias | |
| * | |
| * Esta función realiza el cálculo de fechas de inicio y fin para un conjunto de tareas, | |
| * teniendo en cuenta dependencias, horarios laborales, días festivos y vacaciones de desarrolladores. | |
| * | |
| * Proceso: | |
| * 1. Obtiene datos de la hoja de cálculo 'Lista de tareas' | |
| * 2. Obtiene la fecha de inicio por defecto de la hoja 'Fecha inicio' | |
| * 3. Valida las columnas requeridas y prepara los datos de las tareas | |
| * 4. Procesa las tareas en tres flujos: | |
| * - Flujo 1: Tareas sin dependencias | |
| * - Flujo 2: Tareas con dependencias resueltas | |
| * - Flujo 3: Tareas sin dependencias ni dependientes | |
| * 5. Para cada tarea: | |
| * - Calcula fechas de inicio y fin considerando dependencias | |
| * - Ajusta fechas según horario laboral, festivos y vacaciones | |
| * - Actualiza la hoja de cálculo con las fechas calculadas | |
| * | |
| * Requisitos de la hoja de cálculo: | |
| * - Hoja 'Lista de tareas' con columnas: Id, assignee, Fecha y hora de inicio, | |
| * Estimación original en horas, duedate, Dependencia, errores, Orden | |
| * - Hoja 'Fecha inicio' con la fecha de inicio por defecto en A2 | |
| * | |
| * @throws {Error} Si hay problemas con las columnas requeridas o datos inválidos | |
| * @throws {Error} Si hay problemas con el cálculo de fechas o dependencias | |
| */ | |
| function calculateTaskScheduleWithDependencies() { | |
| const startTime = Date.now(); | |
| try { | |
| const ss = SpreadsheetApp.getActiveSpreadsheet(); | |
| const sheet = ss.getSheetByName('Lista de tareas'); | |
| const data = sheet.getDataRange().getValues(); | |
| const headers = data[0]; | |
| // Obtener fecha de inicio por defecto (en timestamp) | |
| const defaultStartTimestamp = obtenerFechaInicioPorDefecto(ss); | |
| // Obtener índices de columnas | |
| const columns = obtenerIndicesColumnas(headers, sheet); | |
| // Validar columnas requeridas | |
| validarColumnasRequeridas(headers); | |
| // Limpiar columna de errores | |
| limpiarColumnaErrores(sheet, data, columns); | |
| // Configuración de horario laboral, festivos y vacaciones | |
| const workHoursByDay = obtenerHorarioLaboral(); | |
| const holidays = obtenerFestivos(); | |
| const vacationsByDeveloper = obtenerVacacionesDesarrolladores(); | |
| // Preparar datos de tareas (almacenar fechas como timestamp) | |
| const tasks = prepararDatosTareas(data, columns); | |
| // Validar tareas | |
| validarTareas(tasks, sheet, columns); | |
| // Inicializar estructuras para dependencias y resultados | |
| const taskCompletionDates = {}; | |
| const tasksCompleted = []; | |
| let orderIndex = 0; | |
| let taskGraph = buildDependencyGraph(tasks); | |
| // --- Flujo 1: Tareas sin dependencias --- | |
| let startTasks = taskGraph.filter(t => t.dependencies.length === 0 && t.dependents.length > 0); | |
| startTasks.forEach(startTask => { | |
| const task = tasks.find(t => t.id === startTask.id); | |
| if (!task) throw new Error(`No se encontró la tarea con ID ${startTask.id}`); | |
| let startTS, endTS; | |
| if (task.hasPredefinedDates) { | |
| startTS = task.startDate; | |
| endTS = task.dueDate; | |
| } else { | |
| startTS = task.startDate || defaultStartTimestamp; | |
| startTS = adjustToWorkingHoursAndVacations(startTS, task.assignee, workHoursByDay, holidays, vacationsByDeveloper); | |
| endTS = calculateEndDate(startTS, task.estimate, workHoursByDay, holidays, task.assignee, vacationsByDeveloper); | |
| } | |
| if (taskOverlapsVacation(startTS, endTS, task.assignee, vacationsByDeveloper)) { | |
| sheet.getRange(task.rowIndex, columns.errors + 1).setValue(`La tarea ${task.id} se superpone con vacaciones de ${task.assignee}`); | |
| } | |
| // Formatear fechas y guardar en la hoja | |
| actualizarFechasYOrdenTarea(sheet, task, startTS, endTS, ++orderIndex, columns); | |
| taskCompletionDates[task.id] = endTS; | |
| tasksCompleted.push(task.id); | |
| // Actualizar grafo: remover la tarea de las dependencias de otras | |
| taskGraph.forEach(t => { | |
| t.dependencies = t.dependencies.filter(dep => dep !== task.id); | |
| }); | |
| }); | |
| // --- Flujo 2: Tareas pendientes con dependencias resueltas --- | |
| let startTasksCycle; | |
| do { | |
| startTasksCycle = taskGraph.filter(t => t.dependencies.length === 0 && !tasksCompleted.includes(t.id) && t.dependenciesOriginal.length > 0); | |
| if (startTasksCycle.length === 0) break; | |
| let orderOffset = tasksCompleted.length; | |
| startTasksCycle.forEach((startTask, idx) => { | |
| const task = tasks.find(t => t.id === startTask.id); | |
| if (!task) throw new Error(`No se encontró la tarea con ID ${startTask.id}`); | |
| const missingDeps = startTask.dependenciesOriginal.filter(dep => !taskCompletionDates[dep]); | |
| if (missingDeps.length) { | |
| sheet.getRange(task.rowIndex, columns.errors + 1) | |
| .setValue(`No se pueden resolver las dependencias para la tarea ${task.id}: faltan ${missingDeps.join(', ')}`); | |
| return; | |
| } | |
| let startTS, endTS; | |
| if (task.hasPredefinedDates) { | |
| startTS = task.startDate; | |
| endTS = task.dueDate; | |
| } else { | |
| // Tomar la fecha final más tardía entre dependencias | |
| startTS = Math.max(...startTask.dependenciesOriginal.map(dep => taskCompletionDates[dep].getTime ? taskCompletionDates[dep] : taskCompletionDates[dep])); | |
| // Ajuste por tareas previas del mismo desarrollador | |
| const otherTasks = Object.keys(taskCompletionDates) | |
| .filter(id => { | |
| const t = tasks.find(x => x.id === id); | |
| return t && t.assignee === task.assignee && !startTask.dependenciesOriginal.includes(id); | |
| }); | |
| if (otherTasks.length) { | |
| const latestAssigneeTS = Math.max(...otherTasks.map(id => taskCompletionDates[id])); | |
| startTS = Math.max(startTS, latestAssigneeTS); | |
| } | |
| startTS = adjustToWorkingHoursAndVacations(startTS, task.assignee, workHoursByDay, holidays, vacationsByDeveloper); | |
| endTS = calculateEndDate(startTS, task.estimate, workHoursByDay, holidays, task.assignee, vacationsByDeveloper); | |
| } | |
| if (taskOverlapsVacation(startTS, endTS, task.assignee, vacationsByDeveloper)) { | |
| sheet.getRange(task.rowIndex, columns.errors + 1) | |
| .setValue(`La tarea ${task.id} se superpone con vacaciones de ${task.assignee}`); | |
| } | |
| actualizarFechasYOrdenTarea(sheet, task, startTS, endTS, orderOffset + idx + 1, columns); | |
| taskCompletionDates[task.id] = endTS; | |
| tasksCompleted.push(task.id); | |
| taskGraph.forEach(t => { | |
| t.dependencies = t.dependencies.filter(dep => dep !== task.id); | |
| }); | |
| }); | |
| } while (startTasksCycle.length > 0); | |
| // --- Flujo 3: Tareas sin dependencias ni dependientes --- | |
| let remainingTasks = taskGraph.filter(t => !tasksCompleted.includes(t.id)); | |
| remainingTasks.sort((a, b) => a.id.localeCompare(b.id)); | |
| let currentOrderIndex = tasksCompleted.length; | |
| remainingTasks.forEach(taskGraphItem => { | |
| const task = tasks.find(t => t.id === taskGraphItem.id); | |
| if (!task) throw new Error(`No se encontró la tarea con ID ${taskGraphItem.id}`); | |
| let startTS, endTS; | |
| if (task.hasPredefinedDates) { | |
| startTS = task.startDate; | |
| endTS = task.dueDate; | |
| } else { | |
| if (!task.startDate) { | |
| const completedForAssignee = Object.keys(taskCompletionDates) | |
| .filter(id => { | |
| const t = tasks.find(x => x.id === id); | |
| return t && t.assignee === task.assignee; | |
| }); | |
| startTS = completedForAssignee.length ? Math.max(...completedForAssignee.map(id => taskCompletionDates[id])) : defaultStartTimestamp; | |
| } else { | |
| startTS = task.startDate; | |
| const completedForAssignee = Object.keys(taskCompletionDates) | |
| .filter(id => { | |
| const t = tasks.find(x => x.id === id); | |
| return t && t.assignee === task.assignee; | |
| }); | |
| if (completedForAssignee.length) { | |
| startTS = Math.max(startTS, Math.max(...completedForAssignee.map(id => taskCompletionDates[id]))); | |
| } | |
| } | |
| startTS = adjustToWorkingHoursAndVacations(startTS, task.assignee, workHoursByDay, holidays, vacationsByDeveloper); | |
| endTS = calculateEndDate(startTS, task.estimate, workHoursByDay, holidays, task.assignee, vacationsByDeveloper); | |
| } | |
| if (taskOverlapsVacation(startTS, endTS, task.assignee, vacationsByDeveloper)) { | |
| sheet.getRange(task.rowIndex, columns.errors + 1) | |
| .setValue(`La tarea ${task.id} se superpone con vacaciones de ${task.assignee}`); | |
| } | |
| actualizarFechasYOrdenTarea(sheet, task, startTS, endTS, ++currentOrderIndex, columns); | |
| taskCompletionDates[task.id] = endTS; | |
| tasksCompleted.push(task.id); | |
| }); | |
| SpreadsheetApp.flush(); | |
| } catch (error) { | |
| throw error; | |
| } finally { | |
| const endTime = Date.now(); | |
| Logger.log(`Tiempo total de ejecución: ${endTime - startTime} ms`); | |
| } | |
| } | |
| /** | |
| * Construye un grafo de dependencias a partir de las tareas. | |
| * | |
| * @param {Array} tasks - Array de objetos tarea con la siguiente estructura: | |
| * {id: string, dependencies: string[]} | |
| * | |
| * @returns {Array} Array de objetos con la estructura: | |
| * { | |
| * id: string, // ID de la tarea | |
| * dependencies: string[], // Lista de IDs de tareas de las que depende | |
| * dependenciesOriginal: string[], // Copia de las dependencias originales | |
| * dependents: string[] // Lista de IDs de tareas que dependen de esta | |
| * } | |
| * | |
| * @description | |
| * Esta función crea una representación en grafo de las dependencias entre tareas: | |
| * 1. Crea una copia de cada tarea con sus dependencias y un array vacío para dependientes | |
| * 2. Construye un índice para acceso rápido por ID de tarea | |
| * 3. Para cada tarea, agrega su ID a la lista de dependientes de cada tarea de la que depende | |
| * | |
| * @example | |
| * const tasks = [ | |
| * {id: "T1", dependencies: ["T2"]}, | |
| * {id: "T2", dependencies: []} | |
| * ]; | |
| * const graph = buildDependencyGraph(tasks); | |
| * // Resultado: | |
| * // [ | |
| * // {id: "T1", dependencies: ["T2"], dependenciesOriginal: ["T2"], dependents: []}, | |
| * // {id: "T2", dependencies: [], dependenciesOriginal: [], dependents: ["T1"]} | |
| * // ] | |
| */ | |
| function buildDependencyGraph(tasks) { | |
| const graph = tasks.map(task => ({ | |
| id: task.id, | |
| dependencies: [...task.dependencies], | |
| dependenciesOriginal: [...task.dependencies], | |
| dependents: [] | |
| })); | |
| const idIndex = {}; | |
| graph.forEach((t, i) => { | |
| idIndex[t.id] = i; | |
| }); | |
| graph.forEach(t => { | |
| t.dependencies.forEach(dep => { | |
| if (idIndex[dep] !== undefined) { | |
| graph[idIndex[dep]].dependents.push(t.id); | |
| } | |
| }); | |
| }); | |
| return graph; | |
| } | |
| /** | |
| * Ajusta un timestamp para que caiga en horario laboral y fuera de vacaciones/festivos. | |
| */ | |
| /** | |
| * Ajusta un timestamp para que caiga en horario laboral y fuera de vacaciones/festivos | |
| * | |
| * @param {number} ts - Timestamp a ajustar | |
| * @param {string} assignee - Email del desarrollador asignado | |
| * @param {Object} workHoursByDay - Configuración de horario laboral por día | |
| * @param {Array} holidays - Lista de timestamps de días festivos | |
| * @param {Object} vacationsByDeveloper - Configuración de vacaciones por desarrollador | |
| * @returns {number} Timestamp ajustado | |
| * | |
| * @description | |
| * Esta función ajusta recursivamente un timestamp para asegurar que: | |
| * 1. No caiga en días festivos | |
| * 2. No caiga en períodos de vacaciones del desarrollador | |
| * 3. Caiga dentro del horario laboral | |
| * | |
| * El proceso es recursivo porque cada ajuste puede resultar en una nueva fecha | |
| * que necesite ser ajustada nuevamente. | |
| */ | |
| function adjustToWorkingHoursAndVacations(ts, assignee, workHoursByDay, holidays, vacationsByDeveloper) { | |
| let adjusted = ts; | |
| if (isHoliday(adjusted, holidays) || isInVacationPeriod(adjusted, assignee, vacationsByDeveloper)) { | |
| adjusted = isInVacationPeriod(adjusted, assignee, vacationsByDeveloper) ? getDateAfterVacation(adjusted, assignee, workHoursByDay, holidays, vacationsByDeveloper) : findNextWorkingDay(adjusted, workHoursByDay, holidays); | |
| return adjustToWorkingHoursAndVacations(adjusted, assignee, workHoursByDay, holidays, vacationsByDeveloper); | |
| } | |
| adjusted = adjustToWorkingHours(adjusted, workHoursByDay, holidays); | |
| if (isHoliday(adjusted, holidays) || isInVacationPeriod(adjusted, assignee, vacationsByDeveloper)) { | |
| return adjustToWorkingHoursAndVacations(adjusted, assignee, workHoursByDay, holidays, vacationsByDeveloper); | |
| } | |
| return adjusted; | |
| } | |
| /** | |
| * Ajusta un timestamp para que caiga dentro del horario laboral. | |
| */ | |
| /** | |
| * Ajusta un timestamp para que caiga dentro del horario laboral | |
| * | |
| * @param {number} ts - Timestamp a ajustar | |
| * @param {Object} workHoursByDay - Configuración de horario laboral por día de la semana | |
| * @param {Array} holidays - Lista de timestamps de días festivos | |
| * @returns {number} Timestamp ajustado al horario laboral | |
| * | |
| * @description | |
| * Esta función ajusta un timestamp para asegurar que caiga dentro del horario laboral: | |
| * 1. Si el día no es laborable o es festivo, busca el siguiente día laborable | |
| * 2. Si la hora actual es antes del inicio del horario, ajusta al inicio | |
| * 3. Si la hora actual es después del fin del horario, busca el siguiente día | |
| * 4. Si la hora está dentro del horario, mantiene la hora actual | |
| */ | |
| function adjustToWorkingHours(ts, workHoursByDay, holidays) { | |
| const dt = new Date(ts); | |
| const day = dt.getDay() === 0 ? 7 : dt.getDay(); | |
| const workHours = workHoursByDay[day]; | |
| if (!workHours || isHoliday(ts, holidays)) { | |
| return findNextWorkingDay(ts, workHoursByDay, holidays); | |
| } | |
| const currentHour = dt.getHours(); | |
| if (currentHour < workHours.start) { | |
| dt.setHours(workHours.start, 0, 0, 0); | |
| } else if (currentHour >= workHours.end) { | |
| return findNextWorkingDay(ts, workHoursByDay, holidays); | |
| } | |
| return dt.getTime(); | |
| } | |
| /** | |
| * Encuentra el siguiente día laborable y retorna su timestamp. | |
| */ | |
| /** | |
| * Encuentra el siguiente día laborable a partir de una fecha dada | |
| * | |
| * @param {number} ts - Timestamp de la fecha desde donde buscar | |
| * @param {Object} workHoursByDay - Configuración de horario laboral por día de la semana | |
| * @param {Array} holidays - Lista de timestamps de días festivos | |
| * @returns {number} Timestamp del siguiente día laborable | |
| * | |
| * @description | |
| * Esta función busca el próximo día que cumpla con las siguientes condiciones: | |
| * 1. Sea un día laborable según workHoursByDay | |
| * 2. No sea un día festivo | |
| * 3. La hora de inicio se ajuste al horario laboral del día | |
| * | |
| * La búsqueda se realiza día a día hasta encontrar una fecha válida o | |
| * alcanzar el límite de seguridad (30 días). | |
| */ | |
| function findNextWorkingDay(ts, workHoursByDay, holidays) { | |
| let dt = new Date(ts); | |
| dt.setDate(dt.getDate() + 1); | |
| let safety = 0; | |
| while (safety < 30) { | |
| const day = dt.getDay() === 0 ? 7 : dt.getDay(); | |
| if (workHoursByDay[day] && !isHoliday(dt.getTime(), holidays)) { | |
| dt.setHours(workHoursByDay[day].start, 0, 0, 0); | |
| return dt.getTime(); | |
| } | |
| dt.setDate(dt.getDate() + 1); | |
| safety++; | |
| } | |
| return dt.getTime(); | |
| } | |
| /** | |
| * Retorna el timestamp del primer día laborable después del período de vacaciones. | |
| */ | |
| /** | |
| * Encuentra la primera fecha laborable después de un período de vacaciones | |
| * | |
| * @param {number} ts - Timestamp desde donde comenzar la búsqueda | |
| * @param {string} developer - Nombre del desarrollador | |
| * @param {Object} workHoursByDay - Configuración de horario laboral por día de la semana | |
| * @param {Array} holidays - Lista de timestamps de días festivos | |
| * @param {Object} vacationsByDeveloper - Mapeo de desarrolladores a sus días de vacaciones | |
| * @returns {number} Timestamp de la primera fecha laborable disponible | |
| * | |
| * @description | |
| * Esta función busca el primer día disponible para trabajar después de un período de vacaciones, | |
| * considerando: | |
| * 1. Horario laboral del día | |
| * 2. Días festivos | |
| * 3. Períodos de vacaciones del desarrollador | |
| * | |
| * La búsqueda se realiza día a día hasta encontrar una fecha válida o | |
| * alcanzar el límite de seguridad (30 días). | |
| */ | |
| function getDateAfterVacation(ts, developer, workHoursByDay, holidays, vacationsByDeveloper) { | |
| let dt = new Date(ts); | |
| const vacationDays = vacationsByDeveloper[developer] || []; | |
| let safety = 0; | |
| while (safety < 30) { | |
| const day = dt.getDay(); | |
| if (workHoursByDay[day] && !isHoliday(dt.getTime(), holidays) && !vacationDays.includes(dt.toDateString())) { | |
| dt.setHours(workHoursByDay[day].start, 0, 0, 0); | |
| return dt.getTime(); | |
| } | |
| dt.setDate(dt.getDate() + 1); | |
| safety++; | |
| } | |
| return dt.getTime(); | |
| } | |
| /** | |
| * Determina si un timestamp corresponde a un día festivo. | |
| */ | |
| /** | |
| * Verifica si una fecha específica corresponde a un día festivo | |
| * | |
| * @param {number} ts - Timestamp a verificar | |
| * @param {Array<number>} holidays - Lista de timestamps de días festivos | |
| * @returns {boolean} true si la fecha es un día festivo, false en caso contrario | |
| * | |
| * @description | |
| * Esta función normaliza el timestamp proporcionado eliminando la hora, | |
| * minutos y segundos para comparar solo la fecha, y verifica si esta | |
| * fecha normalizada existe en la lista de días festivos. | |
| */ | |
| function isHoliday(ts, holidays) { | |
| const dt = new Date(ts); | |
| dt.setHours(0, 0, 0, 0); | |
| return holidays.includes(dt.getTime()); | |
| } | |
| /** | |
| * Verifica si un timestamp se encuentra en un período de vacaciones para un desarrollador. | |
| */ | |
| /** | |
| * Verifica si un timestamp se encuentra en un período de vacaciones para un desarrollador | |
| * | |
| * @param {number} ts - Timestamp a verificar | |
| * @param {string} developer - Identificador del desarrollador | |
| * @param {Object.<string, Array<string>>} vacationsByDeveloper - Mapa de desarrolladores a sus períodos de vacaciones | |
| * @returns {boolean} true si la fecha corresponde a un día de vacaciones del desarrollador, false en caso contrario | |
| * | |
| * @description | |
| * Esta función verifica si una fecha específica corresponde a un día de vacaciones | |
| * para un desarrollador dado. Realiza las siguientes validaciones: | |
| * 1. Verifica que el desarrollador exista | |
| * 2. Verifica que exista el mapa de vacaciones | |
| * 3. Verifica que el desarrollador tenga períodos de vacaciones registrados | |
| * 4. Normaliza el timestamp eliminando la hora para comparar solo la fecha | |
| * 5. Verifica si la fecha normalizada está en el listado de vacaciones del desarrollador | |
| */ | |
| function isInVacationPeriod(ts, developer, vacationsByDeveloper) { | |
| if (!developer || !vacationsByDeveloper || !vacationsByDeveloper[developer]) return false; | |
| const dt = new Date(ts); | |
| dt.setHours(0, 0, 0, 0); | |
| return vacationsByDeveloper[developer].includes(dt.toDateString()); | |
| } | |
| /** | |
| * Calcula el timestamp de fin de tarea según duración en horas. | |
| */ | |
| /** | |
| * Calcula la fecha de finalización de una tarea basada en su duración y restricciones laborales | |
| * | |
| * @param {number} startTS - Timestamp de inicio de la tarea | |
| * @param {number} durationInHours - Duración de la tarea en horas | |
| * @param {Object.<number, {start: number, end: number}>} workHoursByDay - Horario laboral por día de la semana | |
| * @param {Array<number>} holidays - Lista de timestamps de días festivos | |
| * @param {string} assignee - Identificador del desarrollador asignado | |
| * @param {Object.<string, Array<string>>} vacationsByDeveloper - Mapa de desarrolladores a sus períodos de vacaciones | |
| * @returns {number} Timestamp de la fecha de finalización calculada | |
| * @throws {Error} Si la fecha de inicio es inválida | |
| * @throws {Error} Si la duración de la tarea es inválida | |
| * @throws {Error} Si se alcanza el límite de iteraciones | |
| * | |
| * @description | |
| * Esta función calcula la fecha de finalización de una tarea considerando: | |
| * 1. Horario laboral por día de la semana | |
| * 2. Días festivos | |
| * 3. Períodos de vacaciones del desarrollador | |
| * 4. Duración de la tarea en horas | |
| * | |
| * El cálculo se realiza iterativamente, avanzando minuto a minuto y ajustando | |
| * las fechas según las restricciones laborales. Si se alcanza el límite de | |
| * iteraciones (1000), se lanza un error. | |
| */ | |
| function calculateEndDate(startTS, durationInHours, workHoursByDay, holidays, assignee, vacationsByDeveloper) { | |
| if (!startTS) throw new Error('Fecha de inicio inválida'); | |
| if (typeof durationInHours !== 'number' || durationInHours <= 0) throw new Error('Duración de tarea inválida'); | |
| let current = adjustToWorkingHoursAndVacations(startTS, assignee, workHoursByDay, holidays, vacationsByDeveloper); | |
| let remainingMinutes = Math.round(durationInHours * 60); | |
| let safety = 0; | |
| while (remainingMinutes > 0 && safety < 1000) { | |
| safety++; | |
| const dt = new Date(current); | |
| const day = dt.getDay(); | |
| const workHours = workHoursByDay[day]; | |
| if (!workHours) { | |
| current = findNextWorkingDay(current, workHoursByDay, holidays); | |
| continue; | |
| } | |
| if (isHoliday(current, holidays) || isInVacationPeriod(current, assignee, vacationsByDeveloper)) { | |
| current = isInVacationPeriod(current, assignee, vacationsByDeveloper) ? getDateAfterVacation(current, assignee, workHoursByDay, holidays, vacationsByDeveloper) : findNextWorkingDay(current, workHoursByDay, holidays); | |
| continue; | |
| } | |
| const currentHour = dt.getHours(); | |
| const currentMinute = dt.getMinutes(); | |
| if (currentHour < workHours.start) { | |
| dt.setHours(workHours.start, 0, 0, 0); | |
| current = dt.getTime(); | |
| continue; | |
| } else if (currentHour >= workHours.end) { | |
| current = findNextWorkingDay(current, workHoursByDay, holidays); | |
| continue; | |
| } | |
| const minutesLeft = (workHours.end - currentHour) * 60 - currentMinute; | |
| const minutesToWork = Math.min(remainingMinutes, minutesLeft); | |
| remainingMinutes -= minutesToWork; | |
| const totalMinutes = currentHour * 60 + currentMinute + minutesToWork; | |
| dt.setHours(Math.floor(totalMinutes / 60), totalMinutes % 60, 0, 0); | |
| current = dt.getTime(); | |
| if (remainingMinutes > 0 && dt.getHours() >= workHours.end) { | |
| current = findNextWorkingDay(current, workHoursByDay, holidays); | |
| } | |
| } | |
| if (safety >= 1000) throw new Error('Límite de iteraciones alcanzado en cálculo de fecha de fin'); | |
| return current; | |
| } | |
| /** | |
| * Verifica si una tarea se superpone con vacaciones. | |
| */ | |
| /** | |
| * Verifica si una tarea se superpone con el período de vacaciones de un desarrollador | |
| * | |
| * @param {number} startTS - Timestamp de inicio de la tarea | |
| * @param {number} endTS - Timestamp de fin de la tarea | |
| * @param {string} assignee - Nombre del desarrollador asignado | |
| * @param {Object} vacationsByDeveloper - Mapeo de desarrolladores a sus días de vacaciones | |
| * @returns {boolean} true si la tarea se superpone con vacaciones, false en caso contrario | |
| * | |
| * @description | |
| * Esta función verifica si algún día de vacaciones del desarrollador cae dentro del período | |
| * de la tarea. Para esto: | |
| * 1. Normaliza las fechas de inicio y fin de la tarea al inicio y fin del día respectivamente | |
| * 2. Compara cada día de vacaciones con el rango de la tarea | |
| * 3. Retorna true si encuentra al menos una superposición | |
| */ | |
| function taskOverlapsVacation(startTS, endTS, assignee, vacationsByDeveloper) { | |
| if (!vacationsByDeveloper[assignee] || vacationsByDeveloper[assignee].length === 0) return false; | |
| const startDt = new Date(startTS); | |
| startDt.setHours(0, 0, 0, 0); | |
| const endDt = new Date(endTS); | |
| endDt.setHours(23, 59, 59, 999); | |
| return vacationsByDeveloper[assignee].some(vacStr => { | |
| const vacDt = new Date(vacStr); | |
| vacDt.setHours(0, 0, 0, 0); | |
| return vacDt.getTime() >= startDt.getTime() && vacDt.getTime() <= endDt.getTime(); | |
| }); | |
| } | |
| /** | |
| * Obtiene la fecha de inicio por defecto desde la hoja 'Fecha inicio' | |
| * | |
| * @param {Spreadsheet} ss - La hoja de cálculo activa | |
| * @return {number|null} - Timestamp de la fecha de inicio o null si no se encuentra | |
| * | |
| * @description | |
| * Esta función obtiene la fecha de inicio por defecto para el cálculo de tareas: | |
| * 1. Busca la hoja 'Fecha inicio' en el documento | |
| * 2. Lee el valor de la celda A2 que contiene la fecha | |
| * 3. Si existe una fecha, la normaliza a medianoche (00:00:00) | |
| * 4. Convierte la fecha a timestamp para facilitar cálculos posteriores | |
| * | |
| * @throws {Error} Si hay problemas al acceder a la hoja o leer la fecha | |
| */ | |
| function obtenerFechaInicioPorDefecto(ss) { | |
| const fechaInicioSheet = ss.getSheetByName('Fecha inicio'); | |
| let defaultStartTimestamp = null; | |
| if (fechaInicioSheet) { | |
| const fechaInicioValue = fechaInicioSheet.getRange('A2').getValue(); | |
| if (fechaInicioValue) { | |
| // Normalizamos la fecha a medianoche y convertimos a timestamp | |
| defaultStartTimestamp = new Date(fechaInicioValue).setHours(0, 0, 0, 0); | |
| } | |
| } | |
| return defaultStartTimestamp; | |
| } | |
| /** | |
| * Obtiene y gestiona los índices de las columnas necesarias para el procesamiento de tareas | |
| * | |
| * @param {Array} headers - Array con los nombres de las columnas de la hoja de cálculo | |
| * @param {Sheet} sheet - Hoja de cálculo actual donde se procesarán las tareas | |
| * @return {Object} Objeto que mapea nombres de columnas a sus índices correspondientes | |
| * | |
| * @description | |
| * Esta función realiza las siguientes operaciones: | |
| * 1. Busca los índices de las columnas requeridas en el encabezado | |
| * 2. Si las columnas 'errores' u 'Orden' no existen: | |
| * - Las crea al final de la hoja | |
| * - Actualiza los índices correspondientes | |
| * 3. Retorna un objeto con todos los índices necesarios para el procesamiento | |
| * | |
| * Columnas requeridas: | |
| * - Id: Identificador único de la tarea | |
| * - assignee: Persona asignada a la tarea | |
| * - Fecha y hora de inicio: Fecha programada de inicio | |
| * - Estimación original en horas: Duración estimada de la tarea | |
| * - duedate: Fecha límite de la tarea | |
| * - Dependencia: IDs de tareas de las que depende | |
| * - errores: Columna para mensajes de error (se crea si no existe) | |
| * - Orden: Columna para orden de procesamiento (se crea si no existe) | |
| * | |
| * @throws {Error} Si hay problemas al acceder o modificar la hoja de cálculo | |
| */ | |
| function obtenerIndicesColumnas(headers, sheet) { | |
| const columns = { | |
| id: headers.indexOf("Id"), | |
| assignee: headers.indexOf("assignee"), | |
| startDate: headers.indexOf("Fecha y hora de inicio"), | |
| estimate: headers.indexOf("Estimación original en horas (Viene directo de la guía de estimación)"), | |
| dueDate: headers.indexOf("duedate"), | |
| dependencies: headers.indexOf("Dependencia"), | |
| errors: headers.indexOf("errores"), | |
| order: headers.indexOf("Orden") | |
| }; | |
| // Crear columnas de errores y orden si no existen | |
| if (columns.errors === -1) { | |
| columns.errors = headers.length; | |
| sheet.getRange(1, columns.errors + 1).setValue("errores"); | |
| } | |
| if (columns.order === -1) { | |
| columns.order = headers.length + (columns.errors === headers.length ? 1 : 0); | |
| sheet.getRange(1, columns.order + 1).setValue("Orden"); | |
| } | |
| return columns; | |
| } | |
| /** | |
| * Valida la existencia de columnas obligatorias en la hoja de cálculo | |
| * | |
| * @param {Array<string>} headers - Array con los nombres de las columnas de la hoja | |
| * @throws {Error} Si no se encuentra alguna de las columnas requeridas | |
| * | |
| * @description | |
| * Esta función verifica que existan todas las columnas necesarias para el procesamiento | |
| * de las tareas. Las columnas requeridas son: | |
| * - Id: Identificador único de la tarea | |
| * - assignee: Persona asignada a la tarea | |
| * - Estimación original en horas: Duración estimada de la tarea | |
| * | |
| * Si falta alguna columna, se lanza un error con un mensaje descriptivo que indica | |
| * cuál es la columna faltante. | |
| */ | |
| function validarColumnasRequeridas(headers) { | |
| const requiredColumns = ["Id", "assignee", "Estimación original en horas (Viene directo de la guía de estimación)"]; | |
| for (const col of requiredColumns) { | |
| if (headers.indexOf(col) === -1) { | |
| throw new Error(`Error: Columna "${col}" no encontrada en la hoja de cálculo`); | |
| } | |
| } | |
| } | |
| /** | |
| * Limpia el contenido de la columna de errores en la hoja de cálculo | |
| * | |
| * @param {GoogleAppsScript.Spreadsheet.Sheet} sheet - Hoja de cálculo actual | |
| * @param {Array<Array<any>>} data - Datos de la hoja de cálculo, incluyendo encabezados | |
| * @param {Object} columns - Objeto con los índices de las columnas | |
| * @param {number} columns.errors - Índice de la columna de errores | |
| * | |
| * @description | |
| * Esta función elimina todo el contenido de la columna de errores, manteniendo | |
| * el encabezado intacto. Solo se ejecuta si hay más de una fila en la hoja | |
| * (excluyendo el encabezado). | |
| * | |
| * El proceso: | |
| * 1. Verifica si hay datos más allá del encabezado | |
| * 2. Si existe, limpia el contenido de la columna de errores desde la fila 2 | |
| * hasta la última fila con datos | |
| */ | |
| function limpiarColumnaErrores(sheet, data, columns) { | |
| if (data.length > 1) { | |
| sheet.getRange(2, columns.errors + 1, data.length - 1, 1).clearContent(); | |
| } | |
| } | |
| /** | |
| * Obtiene la configuración de horario laboral desde la hoja 'Horario Laboral' | |
| * | |
| * @returns {Object.<number, {start: number, end: number}|null>} Objeto que mapea días de la semana a horarios laborales | |
| * | |
| * @description | |
| * Esta función lee la configuración de horarios desde la hoja 'Horario Laboral' del spreadsheet. | |
| * Para cada día de la semana, determina si es laborable y sus horarios de trabajo: | |
| * - Si el día tiene entrada y salida definidos, se configura como laborable con esos horarios | |
| * - Si el día no tiene horarios definidos, se marca como no laborable (null) | |
| * | |
| * El objeto retornado tiene la siguiente estructura: | |
| * { | |
| * 0: null, // Domingo (no laborable si no tiene horario) | |
| * 1: {start: 8, end: 17}, // Lunes (horario según configuración) | |
| * 2: {start: 8, end: 17}, // Martes (horario según configuración) | |
| * 3: {start: 8, end: 17}, // Miércoles (horario según configuración) | |
| * 4: {start: 8, end: 17}, // Jueves (horario según configuración) | |
| * 5: {start: 8, end: 17}, // Viernes (horario según configuración) | |
| * 6: null // Sábado (no laborable si no tiene horario) | |
| * } | |
| * | |
| * @throws {Error} Si no se puede acceder a la hoja de horarios o hay problemas al procesar los datos | |
| */ | |
| function obtenerHorarioLaboral() { | |
| const ss = SpreadsheetApp.getActiveSpreadsheet(); | |
| const hojaHorario = ss.getSheetByName('Horario Laboral'); | |
| if (!hojaHorario) { | |
| Logger.log('No se encontró la hoja "Horario Laboral"'); | |
| return {}; | |
| } | |
| const datos = hojaHorario.getDataRange().getValues(); | |
| const headers = datos[0]; | |
| const diaIndex = headers.indexOf('Día'); | |
| const entradaIndex = headers.indexOf('Entrada'); | |
| const salidaIndex = headers.indexOf('Salida'); | |
| const horarios = {}; | |
| // Mapeo de días de la semana con acentos | |
| const diasSemana = { | |
| 'domingo': 0, | |
| 'lunes': 1, | |
| 'martes': 2, | |
| 'miercoles': 3, // Removido acento para mayor compatibilidad | |
| 'jueves': 4, | |
| 'viernes': 5, | |
| 'sabado': 6 // Removido acento para mayor compatibilidad | |
| }; | |
| // Procesar cada fila de datos | |
| for (let i = 1; i < datos.length; i++) { | |
| const dia = datos[i][diaIndex]; | |
| const entrada = datos[i][entradaIndex]; | |
| const salida = datos[i][salidaIndex]; | |
| // Normalizar el día eliminando acentos y convirtiendo a minúsculas | |
| const diaNormalizado = dia.toLowerCase() | |
| .normalize('NFD') | |
| .replace(/[\u0300-\u036f]/g, ''); | |
| const diaNumero = diasSemana[diaNormalizado]; | |
| // Validación de horarios | |
| if (!entrada || !salida) { | |
| horarios[diaNumero] = null; | |
| continue; | |
| } | |
| // Función auxiliar para convertir hora a formato 24h | |
| const convertirHora = (valor) => { | |
| if (valor instanceof Date) { | |
| return valor.getHours(); | |
| } | |
| if (typeof valor === 'string') { | |
| const hora = parseInt(valor.split(':')[0]); | |
| return isNaN(hora) ? 0 : hora; | |
| } | |
| const hora = parseInt(String(valor).split(':')[0]); | |
| return isNaN(hora) ? 0 : hora; | |
| }; | |
| const horaEntrada = convertirHora(entrada); | |
| const horaSalida = convertirHora(salida); | |
| // Validar que las horas estén en el rango correcto | |
| if (horaEntrada >= 0 && horaEntrada <= 23 && | |
| horaSalida >= 0 && horaSalida <= 23 && | |
| horaEntrada < horaSalida) { | |
| horarios[diaNumero] = { | |
| start: horaEntrada, | |
| end: horaSalida | |
| }; | |
| } else { | |
| Logger.log(`Horario inválido para el día ${dia}: entrada=${horaEntrada}, salida=${horaSalida}`); | |
| horarios[diaNumero] = null; | |
| } | |
| } | |
| return horarios; | |
| } | |
| /** | |
| * Obtiene la lista de días festivos desde la hoja de cálculo | |
| * | |
| * @returns {Array<number>} Array de timestamps que representan los días festivos | |
| * | |
| * @description | |
| * Lee y procesa los días festivos desde la hoja 'Días festivos': | |
| * 1. Obtiene los datos de la columna 'Fecha' | |
| * 2. Filtra valores vacíos | |
| * 3. Convierte cada fecha a timestamp (milisegundos desde epoch) | |
| * | |
| * @throws {Error} Si no se encuentra la hoja 'Días festivos' | |
| * @throws {Error} Si no se encuentra la columna 'Fecha' | |
| */ | |
| function obtenerFestivos() { | |
| const ss = SpreadsheetApp.getActiveSpreadsheet(); | |
| const hojaFestivos = ss.getSheetByName('Días festivos'); | |
| if (!hojaFestivos) { | |
| Logger.log('No se encontró la hoja "Días festivos"'); | |
| return []; | |
| } | |
| const datos = hojaFestivos.getDataRange().getValues(); | |
| const headers = datos[0]; | |
| const fechaIndex = headers.indexOf('Fecha'); | |
| if (fechaIndex === -1) { | |
| Logger.log('No se encontró la columna "Fecha" en la hoja "Días festivos"'); | |
| return []; | |
| } | |
| // Ignorar la fila de encabezados | |
| return datos.slice(1) | |
| .map(row => row[fechaIndex]) | |
| .filter(fecha => fecha) // Filtrar valores vacíos | |
| .map(fecha => { | |
| const d = new Date(fecha); | |
| return d.getTime(); | |
| }); | |
| } | |
| /** | |
| * Obtiene la configuración de vacaciones por desarrollador para el año 2025 | |
| * | |
| * @returns {Object.<string, Array<string>>} Mapa de desarrolladores a sus días de vacaciones | |
| * | |
| * @description | |
| * Esta función define los períodos de vacaciones para cada desarrollador del equipo: | |
| * 1. Cada desarrollador está identificado por su correo electrónico | |
| * 2. Las fechas se almacenan en formato DateString (YYYY-MM-DD) | |
| * 3. Los meses en JavaScript van de 0 a 11 (0 = enero, 11 = diciembre) | |
| * 4. Se incluyen todos los desarrolladores del equipo con sus respectivos días de vacaciones | |
| * | |
| * Nota: Las fechas están hardcodeadas para el año 2025 y deberían actualizarse | |
| * anualmente o preferiblemente cargarse desde una fuente de datos externa. | |
| */ | |
| function obtenerVacacionesDesarrolladores() { | |
| const ss = SpreadsheetApp.getActiveSpreadsheet(); | |
| const hojaVacaciones = ss.getSheetByName('Vacaciones'); | |
| if (!hojaVacaciones) { | |
| Logger.log('No se encontró la hoja "Vacaciones"'); | |
| return {}; | |
| } | |
| const datos = hojaVacaciones.getDataRange().getValues(); | |
| const headers = datos[0]; | |
| const personaIndex = headers.indexOf('Persona'); | |
| const fechaIndex = headers.indexOf('Fecha'); | |
| if (personaIndex === -1 || fechaIndex === -1) { | |
| Logger.log('No se encontraron las columnas requeridas en la hoja "Vacaciones"'); | |
| return {}; | |
| } | |
| // Ignorar la fila de encabezados y agrupar por persona | |
| const vacacionesPorPersona = {}; | |
| datos.slice(1).forEach(row => { | |
| const persona = row[personaIndex]; | |
| const fecha = row[fechaIndex]; | |
| if (persona && fecha) { | |
| if (!vacacionesPorPersona[persona]) { | |
| vacacionesPorPersona[persona] = []; | |
| } | |
| vacacionesPorPersona[persona].push(new Date(fecha).toDateString()); | |
| } | |
| }); | |
| return vacacionesPorPersona; | |
| } | |
| /** | |
| * Prepara y normaliza los datos de las tareas para su procesamiento | |
| * | |
| * @param {Array} data - Datos crudos de la hoja de cálculo | |
| * @param {Object} columns - Objeto que mapea nombres de columnas a sus índices | |
| * @returns {Array<Object>} Array de tareas normalizadas con las siguientes propiedades: | |
| * - rowIndex: Número de fila en la hoja (comenzando desde 2) | |
| * - id: Identificador único de la tarea | |
| * - assignee: Email del desarrollador asignado | |
| * - startDate: Timestamp de la fecha de inicio (null si no está definida) | |
| * - dueDate: Timestamp de la fecha de fin (null si no está definida) | |
| * - estimate: Duración estimada en horas (0 si no está definida) | |
| * - dependencies: Array de IDs de tareas de las que depende | |
| * - hasPredefinedDates: Boolean indicando si tiene fechas predefinidas | |
| * | |
| * @description | |
| * Esta función procesa los datos crudos de la hoja de cálculo para crear un array | |
| * de objetos normalizados que serán utilizados para el cálculo de fechas. Realiza | |
| * las siguientes transformaciones: | |
| * 1. Convierte fechas a timestamps para facilitar cálculos | |
| * 2. Parsea la estimación a número | |
| * 3. Procesa las dependencias separando por comas y eliminando espacios | |
| * 4. Determina si la tarea tiene fechas predefinidas | |
| * 5. Agrega el índice de fila para referencia posterior | |
| */ | |
| function prepararDatosTareas(data, columns) { | |
| return data.slice(1).map((row, index) => ({ | |
| rowIndex: index + 2, | |
| id: row[columns.id], | |
| assignee: row[columns.assignee], | |
| startDate: row[columns.startDate] ? new Date(row[columns.startDate]).getTime() : null, | |
| dueDate: row[columns.dueDate] ? new Date(row[columns.dueDate]).getTime() : null, | |
| estimate: parseFloat(row[columns.estimate]) || 0, | |
| dependencies: row[columns.dependencies] ? row[columns.dependencies].toString().split(',').map(dep => dep.trim()).filter(Boolean) : [], | |
| hasPredefinedDates: !!(row[columns.startDate] && row[columns.dueDate]) | |
| })); | |
| } | |
| /** | |
| * Valida que las tareas tengan los datos requeridos y registra errores si es necesario | |
| * | |
| * @param {Array<Object>} tasks - Array de objetos con los datos de las tareas | |
| * @param {Object} tasks[].id - Identificador único de la tarea | |
| * @param {Object} tasks[].assignee - Email del desarrollador asignado | |
| * @param {Object} tasks[].estimate - Duración estimada en horas | |
| * @param {Object} tasks[].rowIndex - Índice de la fila en la hoja de cálculo | |
| * @param {Sheet} sheet - Hoja de cálculo actual | |
| * @param {Object} columns - Objeto con los índices de las columnas | |
| * @param {number} columns.errors - Índice de la columna de errores | |
| * @throws {Error} Si hay tareas inválidas (sin ID, asignado o estimación) | |
| * | |
| * @description | |
| * Esta función valida que cada tarea tenga los datos mínimos requeridos: | |
| * 1. ID único | |
| * 2. Desarrollador asignado | |
| * 3. Estimación en horas | |
| * | |
| * Si encuentra tareas inválidas: | |
| * 1. Registra un mensaje de error en la columna de errores | |
| * 2. Lanza una excepción con el detalle de las tareas inválidas | |
| * | |
| * @example | |
| * // Ejemplo de uso: | |
| * const tasks = [ | |
| * { id: "T1", assignee: "dev@email.com", estimate: 8, rowIndex: 2 }, | |
| * { id: "T2", assignee: "", estimate: 4, rowIndex: 3 } | |
| * ]; | |
| * validarTareas(tasks, sheet, { errors: 7 }); | |
| * // Lanzará error: "Las siguientes tareas son inválidas: T2" | |
| */ | |
| function validarTareas(tasks, sheet, columns) { | |
| // Validar que las tareas tengan los datos requeridos | |
| const invalidTasks = tasks.filter(task => !task.id || !task.assignee || !task.estimate); | |
| if (invalidTasks.length > 0) { | |
| const errorMsg = `Las siguientes tareas son inválidas: ${invalidTasks.map(t => t.id || `Fila ${t.rowIndex} sin ID`).join(', ')}`; | |
| sheet.getRange(2, columns.errors + 1).setValue(errorMsg); | |
| throw new Error(errorMsg); | |
| } | |
| // Validar IDs únicos | |
| const ids = new Set(); | |
| const duplicateIds = tasks.filter(task => { | |
| if (ids.has(task.id)) { | |
| return true; | |
| } | |
| ids.add(task.id); | |
| return false; | |
| }); | |
| if (duplicateIds.length > 0) { | |
| const errorMsg = `Se encontraron IDs duplicados: ${duplicateIds.map(t => t.id).join(', ')}`; | |
| sheet.getRange(2, columns.errors + 1).setValue(errorMsg); | |
| throw new Error(errorMsg); | |
| } | |
| } | |
| /** | |
| * Actualiza las fechas y el orden de una tarea en la hoja de cálculo | |
| * | |
| * @param {Sheet} sheet - Hoja de cálculo actual | |
| * @param {Object} task - Objeto con los datos de la tarea | |
| * @param {number} startTS - Timestamp de la fecha de inicio | |
| * @param {number} endTS - Timestamp de la fecha de fin | |
| * @param {number} orderIndex - Índice de orden de la tarea | |
| * @param {Object} columns - Objeto con los índices de las columnas | |
| * | |
| * @description | |
| * Esta función actualiza en la hoja de cálculo: | |
| * 1. La fecha de inicio de la tarea (formato dd/MM/yyyy HH:mm) | |
| * 2. La fecha de fin de la tarea (formato dd/MM/yyyy HH:mm) | |
| * 3. El índice de orden de la tarea | |
| * | |
| * Las fechas se formatean usando la zona horaria de la sesión actual. | |
| * | |
| * @example | |
| * // Ejemplo de uso: | |
| * const task = { rowIndex: 2 }; | |
| * const columns = { startDate: 3, dueDate: 4, order: 5 }; | |
| * actualizarFechasYOrdenTarea(sheet, task, 1640995200000, 1641081600000, 1, columns); | |
| */ | |
| function actualizarFechasYOrdenTarea(sheet, task, startTS, endTS, orderIndex, columns) { | |
| sheet.getRange(task.rowIndex, columns.startDate + 1) | |
| .setValue(Utilities.formatDate(new Date(startTS), Session.getScriptTimeZone(), "dd/MM/yyyy HH:mm")); | |
| sheet.getRange(task.rowIndex, columns.dueDate + 1) | |
| .setValue(Utilities.formatDate(new Date(endTS), Session.getScriptTimeZone(), "dd/MM/yyyy HH:mm")); | |
| sheet.getRange(task.rowIndex, columns.order + 1).setValue(orderIndex); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment