Skip to content

Instantly share code, notes, and snippets.

@DeveloperArmando
Created April 5, 2025 00:46
Show Gist options
  • Select an option

  • Save DeveloperArmando/5bee01bd3d133748c63c0e2b9c2f11ae to your computer and use it in GitHub Desktop.

Select an option

Save DeveloperArmando/5bee01bd3d133748c63c0e2b9c2f11ae to your computer and use it in GitHub Desktop.
/**
* 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