Page cover image

Herramienta de Gestión de Proyectos

Bienvenido a este capítulo donde estaremos desarrollando una herramienta de "Gestión de Proyectos" aplicando los conceptos de "lógica básica" y "comunicación básica" que hemos visto en este repositorio. Todas las herramientras usadas en este caso ya han sido explicadas en los capítulos correspondientes. De todas maneras, estaré dejando los enlaces según vea conveniente. Sin más dilación...


Empecemos...

...definiendo ¿qué es una herramienta de gestión de proyectos?. Y básicamente es una aplicación que te ayuda a organizar, planificar, ejecutar, etc., diferentes tareas y recursos que forman parte de un proyecto.

Y ¿para qué sirve una herramienta de este tipo? Pues, dependiendo de la complejidad de la misma, puede ayudarnos más o menos a: aumentar la productividad, mejorar la organización, facilitar la colaboración (si se crea para equipos), etc. Ahora hablemos...


...sobre el proyecto.

En mi caso, quería crear una herramienta de "gestión de proyectos" que me sirviera para trabajar en mi PC de manera personal, que no se conectase a internet y con un aspecto bastante simple y sobrio. Esto, porque he probado muchas herramientas y no me terminan de convencer. Así que pensé que se podía crear una.


Lógica

Para definir la lógica, empezaremos definiendo las características principales que queremos que tenga la herramienta. Preguntas como: ¿qué quiero que haga? ¿cuando haga clic aquí qué va a pasar? ¿cómo va a interactuar tal o cual cosa? ¿cómo se va a ver? ¿qué atributos quiero ver yo en esta herramienta? entre otras más, nos ayudarán a definir la lógica.

Recordemos que la lógica la he dividido en dos partes: por un lado está la parte de Fondo (estructura), que nos ayuda definiendo las funciones, y por otra parte está la Forma (estética), que nos ayuda definiendo la parte visual.

Con esto como punto de partida, definí que habían tres actividades clave que quería que la herramienta me permitiera hacer: agregar un proyecto, agregar tareas a cada proyecto y guardar mi progreso por si quedara algún pendiente para el día siguiente. Te dejo un esquema de mi lógica en base al "Fondo (estructura)":

A partir de definir las tres actividades principales, empezamos a completar las funcionalidades de cada una como mostró la imagen de arriba. Aquí te dejo un desglose de funcionalidades y componenetes que tuve en cuenta:

Funcionalidades y Componentes

  • Funcionalidades básicas:

    • Crear proyectos

    • Añadir tareas

    • Visualizar el progreso del proyecto

    • Guardado del progreso en local

  • Desglosando la herramienta en componentes:

    • Lista de proyectos

    • Formulario para crear proyectos

    • Vista de proyecto

    • Formulario para añadir tareas

    • Lista de tareas


Con la estructura clara pasaremos a definir la lógica visual. En mi caso, como quería que tuviera un aspecto simple y sobrio, me decanté por un estilo minimalista. Un aspecto importante que te permite definir la lógica de forma (estética) es el formato.

El formato tiene mucho que ver porque es el que va a dar forma a tu herramienta. Usualmente manejo dos tipos: contenedores y tablas. En este caso, como es una herramienta que divide varios "proyectos", me conviene más verlo en forma de contenedores y no de tablas.

Por lo demás, es definir de manera básica el aspecto en atributos como los colores, forma de botones, opciones de menú y uno muy importante es la "adaptabilidad". Básicamente la "adaptabilidad" hace referencia a que nuestra herramienta se pueda ver bien en PC si acortamos o agrandamos la página, y en móviles.

Nuestra lógica de "Forma" quedaría de la siguiente manera:

Simple, ¿verdad?. Yo sé que puede parecer complejo al inicio tener que hacer todo esto, pero te aseguro que es la manera en cómo te puedes ahorrar bastantes pasos y dolores de cabeza después. Ahora, ¿qué toca?.


Definir la comunicación...

...que no es otra cosa que hacer el prompt. Pero como ya hemos visto, de esta depende que la IA te entienda, te entienda a medias o no te entienda. Yo he propuesto, para este ejercicio, definir una jerarquía lineal haciendo una descripción de la herramienta y su comportamiento viéndolo de arriba hacia abajo.

Entonces, el promtp, en mi caso, quedaría de la siguiente manera:

Prompt 1: Basado en la lógica propuesta y con una jerarquía en la comunicación

Quiero crear una herramienta de gestión de proyecto personal que funcione en local. El lenguaje a usar será HTML, este puede contener CSS o JS, pero siempre dentro del HTML. (Comenzamos indicando el lenguaje, ya que es la base sobre la que se sentará la herramienta y resaltamos que considere un solo archivo porque lo usaremos para compartir.)

Lo primero que debe tener es un contenedor para agregar un nuevo proyecto y este debe tener los atributos "Nombre del proyecto" y "Descripción del proyecto", ambos son para ser rellenados. Luego, deben contar con dos campos, uno para fecha de inicio y otro para fecha de fin, ambos deben poder desplegar un pequeño calendario para elegir el día, mes y año resultando en formato 00/00/0000. Al final de estos puntos debe contener un botón que diga "Agregar Proyecto" para que este se agregue a la cola de proyectos. (Previamente, según el esquema de lógica, vimos que cada parte podía pertenecer a un contenedor, por lo que decide tratarse cada punto de forma independiente. Vamos detallando los atributos de cada elemento que queremos. Siempre detalles de funcionamiento, no de estilo.)

Lo segundo es crear un nuevo contenedor debajo del primero que permita "Agregar Tarea". Este debe tener los atributos "Nombre de la tarea" y "Descripción de la tarea", ambos son para ser rellenados. Debajo debe haber un menú desplegable que diga "Seleccionar un proyecto" y debe poder permitir elegir entre los proyectos que se han agregado en cola. (Continuamos detallando las acciones que queremos que se realice en cada parte de nuestra herramienta. Tendremos las mismas consideraciones para el resto del prompt.)

Tercero, crearás un tercer contenedor donde estarán los proyectos creados con el atributo de "Descripción" y las "Fecha de inicio" y "Fecha de fin", y cada uno contará con una barra de progreso y un botón que permita "Eliminar Proyecto".

Asimismo, las tareas que se agreguen deberán estar dentro de cada proyecto elegido y deberán verse a través de un menú desplegable. Dentro de ese menú, deberá haber un botón que diga "Finalizar Tarea" y cuando se marque, la barra de progreso del proyecto avanzará.

Por último, al final de todo habrán dos botones, uno que permita "limpiar todos los proyectos" que están en el HTML y otro que permita "Guardar el progreso". Recuerda que debe poderse guardar el progreso en el mismo archivo.

Desarrolla un estilo minimalista, con escala de grises, las cajas con bordes redondeados y las flechas de los menús desplegables que sean modernos. Además, los botones podrían tener color "1a1a1a" y texto blanco. (En este punto se agrega brevemente consideraciones de estilo. El estilo minimalista es un buen punto de partida para que no se vea tosco. Después podrás hacer mejoras.)

Ahora usaré un "Codepen" donde he pegado el código "HTML" que me generó Google AI Studio. Recuerda que para verlo y probarlo puedes dar clic donde dice "Run Pen". Además, en la herramienta "Codepen" encontrarás un botón que dice "HTML" donde podrás clicar para ver el código que nos ha generado:

Herramienta generada con Google AI Studio

Como podrás observar, la herramienta está completamente funcional y era lo que buscábamos, que en pocos pasos, con menos pruebas y errores, lleguemos al resultado que queríamos. Aun así, nos faltaría agregar la lógica de "Forma (estética)" que habíamos definido anteriormente.

Te dejo abajo el promtp con las indicaciones que le hice a la IA:

Prompt 2: resaltando mejoras estéticas

Hagamos unos cambios de estilo. Primero, me gustaría que los títulos "Proyecto", "Agregar Proyecto" y "Agregar Tarea" estén centrados. (Nuevamente, empezamos realizando los cambios desde arriba y en orden, como definimos previamente al hacer nuestra jerarquía de comunicación).

Seguidamente, los proyectos que se agregan en el contenedor de abajo deben estar enmarcados por un borde grisáseo y deben tener una sombra bastante tenue, con un espacio considerable entre cada proyecto, para que así no se mezclen o se interrumpan.

Después, haz que las tareas aparezcan arriba del botón "Eliminar Proyecto" y debajo de la barra de progreso y enmarcalas en un contenedor con líneas grises sutiles, manteniendo un espaciado para que no se vea pegado al botón de "Eliminar Proyecto" y tampoco a la barra de progreso.

En cuanto a la barra de progreso esta debe contener dos colores, azul "#0070ff" cuando no ha llegado al 100% y verde "#24d83d" cuando está en el 100%. Y en cuanto al resto de colores trabájalos en escala de grises y aplica un color "1a1a1a" para todos los botones. (Los códigos de colores mostrados aquí son valores hexadecimales. Si quieres saber el código de un color en particula, te recomiendo que entres aquí.)

Asimismo, el botón de "Finalizar Tarea" hazlo un poco más pequeño para que guarde relación con el texto de la tarea.

Por último, agrega una descripción al final de todo que diga "Diseñado por Tomas Dulanto" en un color gris claro y letra pequeña. Además, hazlo responsive. Ahora escribe todo el código en su totalidad.

Ahora podrás ver la herramienta terminada en el siguiente "Codepen":

Herramienta generada con Google AI Studio

Con esto ya podríamos dar por terminado el proyecto. Como verás, lo más complicado ha sido definir la lógica y la comunicación. Pero como has podido observar, esto nos ayudó a reducir el número de intentos y errores notablemente.

De todas formas, como ya te había comentado, te recomiendo trabajar el código por separado. Esto te dará más control y flexibilidad al momento de querer hacer cambios o escalar tu idea inicial.

Al final de tejo el código para que lo copies y pegues en la herramienta que uses para prototipar. Antes de terminar quiero que tengas en cuenta una cosa...

¡IA nos vemos pronto!

<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">  <!-- Para responsive -->
<title>Gestor de Proyectos Personal</title>
<style>
body {
    font-family: sans-serif;
    background-color: #f4f4f4;
    color: #333;
  }
  .container {
    background-color: #fff;
    border-radius: 8px;
    padding: 20px;
    margin-bottom: 20px;
    box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
  }

  h2 {
     text-align: center;
  }

  input, select, textarea {
    width: 100%;
    padding: 10px;
    margin-bottom: 10px;
    border: 1px solid #ddd;
    border-radius: 4px;
    box-sizing: border-box;
  }
  button {
    background-color: #1a1a1a;
    color: #fff;
    padding: 8px 12px;
    border: none;
    border-radius: 4px;
    cursor: pointer;
    font-size: 0.9em;
  }

  .project {
    border: 1px solid #ddd;
    box-shadow: 0 2px 4px rgba(0,0,0,0.1);
    margin-bottom: 20px;
    padding: 15px;
    border-radius: 8px;
  }
  .progress-bar {
    background-color: #ddd;
    border-radius: 4px;
    height: 20px;
    margin-bottom: 10px;
  }
  .progress {
    height: 100%;
    border-radius: 4px;
    width: 0%;
  }
  .task-list {
     border: 1px solid #ddd;
     margin-bottom: 10px;
     padding: 10px;
     list-style: none;
 }
  .task-list li {
     margin-bottom: 5px;
  }

  .select-wrapper {
    position: relative;
    display: inline-block;
    width: 100%;
  }
  .select-wrapper select {
    -webkit-appearance: none;
    -moz-appearance: none;
    appearance: none;
    background: transparent url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='6' viewBox='0 0 12 6'%3E%3Cpath fill='%23666' d='M0.379,0.909l5.243,5.241L11.621,0.909L12,1.288L6,7.288L-5.34359375e-16,1.288L0.379,0.909z'/%3E%3C/svg%3E") no-repeat right 10px center;
    padding-right: 30px;
  }

  details summary {
      cursor: pointer;
      margin-bottom: 10px;
  }

  .footer { /* Estilos para el pie de página */
    text-align: center;
    font-size: 0.8em;
    color: #bbb;
    margin-top: 20px;
  }

  /* Media query para responsive  (ajusta a tus necesidades) */
  @media (max-width: 768px) {
    .container {
        padding: 15px; /* reduce padding en pantallas pequeñas */
    }
  }
</style>
</head>
<body>
 <div class="container">
    <h2>Agregar Proyecto</h2>
   <input type="text" id="projectName" placeholder="Nombre del proyecto" required>
   <textarea id="projectDescription" placeholder="Descripción del proyecto" required></textarea>
   <input type="date" id="startDate" required>
    <input type="date" id="endDate" required>
   <button onclick="addProject()">Agregar Proyecto</button>
 </div>

 <div class="container">
    <h2>Agregar Tarea</h2>
   <input type="text" id="taskName" placeholder="Nombre de la tarea" required>
   <textarea id="taskDescription" placeholder="Descripción de la tarea" required></textarea>
 <div class="select-wrapper"> <select id="projectSelection" disabled></select></div>
 <button onclick="addTask()">Agregar Tarea</button>
 </div>

<div id="projectsContainer" class="container">
  <h2>Proyectos</h2>
</div>

<div class="container">
  <button onclick="clearProjects()">Limpiar Proyectos</button>
  <button onclick="saveProjects()">Guardar Progreso</button>
</div>

 <div class="footer">
     Diseñado por Tomas Dulanto
</div>



<script>
  let projects = [];

function addProject() {
  const projectName = document.getElementById('projectName').value.trim();
  const projectDescription = document.getElementById('projectDescription').value.trim();
  const startDate = document.getElementById('startDate').value;
  const endDate = document.getElementById('endDate').value;

  if (projectName === "" || projectDescription === "" || startDate === "" || endDate === "") {
      alert("Por favor, complete todos los campos del proyecto.");
      return;
  }

  const newProject = {
    name: projectName,
    description: projectDescription,
    startDate: startDate,
    endDate: endDate,
    tasks: [],
    completedTasks: 0
  };

  projects.push(newProject);
  renderProjects();
  updateProjectSelection();
  document.getElementById('projectName').value = '';
  document.getElementById('projectDescription').value = '';
  document.getElementById('startDate').value = '';
  document.getElementById('endDate').value = '';
}

function addTask() {
   const taskName = document.getElementById('taskName').value.trim();
   const taskDescription = document.getElementById('taskDescription').value.trim();
   const selectedProjectIndex = document.getElementById('projectSelection').value;

   if (taskName === "" || taskDescription === ""|| selectedProjectIndex === null ) {
       alert("Por favor, complete todos los campos de la tarea y seleccione un proyecto.");
        return;
   }

    const newTask = {
      name: taskName,
      description: taskDescription,
      status: "Sin empezar"
    };

    projects[selectedProjectIndex].tasks.push(newTask);
    renderProjects();

    document.getElementById("taskName").value= "";
    document.getElementById("taskDescription").value = "";

    updateTaskCounter(selectedProjectIndex);
}

function updateTaskCounter(projectIndex) {
    let completedCount = 0;
    for (const task of projects[projectIndex].tasks){
        if(task.status === "Finalizado"){
            completedCount++;
        }
    }

    projects[projectIndex].completedTasks = completedCount;
    updateProgressBar(projectIndex);
}

function calculateProgress(projectIndex) {
    if (!projects[projectIndex] || !projects[projectIndex].tasks ) return 0;

    const totalTasks = projects[projectIndex].tasks.length;
    if(totalTasks === 0 ) return 0;

    return Math.round((projects[projectIndex].completedTasks / totalTasks) * 100);
}

function updateProgressBar(projectIndex) {
    const progressBar = document.querySelector(`.project:nth-child(${projectIndex + 1}) .progress`);

    if (progressBar) {
        progressBar.style.width = `${calculateProgress(projectIndex)}%`;
    }
}

function renderProjects() {
    const projectsContainer = document.getElementById('projectsContainer');
    projectsContainer.innerHTML = '<h2>Proyectos</h2>';

    projects.forEach((project, index) => {
        const projectDiv = document.createElement('div');
        projectDiv.className = 'project';

        let progress = calculateProgress(index);
        let progressBarColor = progress === 0 ? "#ff1f00" : progress === 100 ? "#24d83d" : "#0070ff";

        projectDiv.innerHTML = `<h3>${project.name}</h3>
                                <p>${project.description}</p>
                                <p>Inicio: ${project.startDate}, Fin: ${project.endDate}</p>
                                <div class="progress-bar">
                                    <div class="progress" style="width: ${progress}%; background-color: ${progressBarColor};"></div>
                                </div>
                                <details>
                                    <summary>Tareas</summary>
                                    <ul class="task-list">${project.tasks.map(task => `
                                        <li>${task.name}  —  ${task.status !== "Finalizado" ? `<button onclick="completeTask(${index}, '${task.name}')">Finalizar Tarea</button>` : ''}
                                        </li>`).join('')}
                                    </ul>
                                </details>
                                <button onclick="deleteProject(${index})">Eliminar Proyecto</button>`;

        projectsContainer.appendChild(projectDiv);
    });
}

function updateProjectSelection() {
    const projectSelection = document.getElementById("projectSelection");
    projectSelection.innerHTML = "";
    projectSelection.disabled = projects.length === 0;

    projects.forEach((project, index) => {
        const option = document.createElement("option");
        option.value = index;
        option.text = project.name;
        projectSelection.appendChild(option);
    });
}

function clearProjects() {
    if(confirm("Esta seguro que quiere eliminar todos los proyectos?")){
        projects = [];
        renderProjects();
        updateProjectSelection();
    }
}

function deleteProject(index) {
    if (confirm("¿Estás seguro de que deseas eliminar este proyecto?")) {
        projects.splice(index, 1);
        renderProjects();
        updateProjectSelection();
    }
}

function completeTask(projectIndex, taskName) {
    const project = projects[projectIndex];
    const taskIndex = project.tasks.findIndex((task) => task.name === taskName);

    if (taskIndex !== -1 && project.tasks[taskIndex].status !== "Finalizado") {
        project.tasks[taskIndex].status = "Finalizado";
        project.completedTasks++;
        updateProgressBar(projectIndex);
        renderProjects();
    }
}

function saveProjects() {
    localStorage.setItem('projects', JSON.stringify(projects));
}

const savedProjects = localStorage.getItem("projects");
if (savedProjects) {
    try {
        projects = JSON.parse(savedProjects);
    } catch (error) {
        console.error("Error al analizar los datos guardados:", error);
        projects = [];
    }

    renderProjects();
    updateProjectSelection();
    projects.forEach((project, index) => updateProgressBar(index));
}







</script>

</body>
</html>

Last updated