-
- Cancel
-
-
- Update Status
-
+ {currentUser && (
+
+ Welcome, {currentUser.name || currentUser.username}
+ ({currentUser.role})
-
-
- );
-}
-
-// Componente principal
-function App() {
- // Estados de autenticación
- const [isAuthenticated, setIsAuthenticated] = useState(false);
- const [currentUser, setCurrentUser] = useState(null);
- const [loginError, setLoginError] = useState(null);
-
- // Estados de la aplicación
- const [isLoading, setLoading] = useState(false);
- const [isInserting, setInserting] = useState(false);
- const [items, setItems] = useState([]);
- const [error, setError] = useState();
-
- // Estado para la pestaña activa
- const [activeTab, setActiveTab] = useState('tasks');
-
- // Estado para filtros
- const [priorityFilter, setPriorityFilter] = useState('All');
-
- // Estados para sprints y desarrolladores
- const [sprints, setSprints] = useState([]);
- const [selectedSprint, setSelectedSprint] = useState(null);
- const [developers, setDevelopers] = useState([]);
- const [tasksWithoutSprint, setTasksWithoutSprint] = useState([]);
- const [loadingSprints, setLoadingSprints] = useState(false);
- const [loadingDevelopers, setLoadingDevelopers] = useState(false);
-
- // Estado para el modal
- const [modalOpen, setModalOpen] = useState(false);
- const [selectedTask, setSelectedTask] = useState(null);
-
- // Estado para el modal de cambio de estado
- const [statusModalOpen, setStatusModalOpen] = useState(false);
- const [taskForStatusChange, setTaskForStatusChange] = useState(null);
-
- // Estados para el modal de creación de sprint
- const [newSprintModalOpen, setNewSprintModalOpen] = useState(false);
- const [newSprintName, setNewSprintName] = useState('');
- const [newSprintDescription, setNewSprintDescription] = useState('');
- const [newSprintStartDate, setNewSprintStartDate] = useState('');
- const [newSprintDuration, setNewSprintDuration] = useState(2);
- const [isCreatingSprint, setIsCreatingSprint] = useState(false);
-
- // Comprobar si el usuario es desarrollador
- const isDeveloper = currentUser?.role === 'Developer';
-
- // Función para manejar el login
- const handleLogin = (user, error = null) => {
- if (user) {
- setCurrentUser(user);
- setIsAuthenticated(true);
- setLoginError(null);
- } else {
- setLoginError(error);
- }
- };
-
- // Función para manejar el logout
- const handleLogout = () => {
- localStorage.removeItem('currentUser');
- setCurrentUser(null);
- setIsAuthenticated(false);
- };
-
- // Verificar si hay un usuario en localStorage al cargar la página
- useEffect(() => {
- const storedUser = localStorage.getItem('currentUser');
- if (storedUser) {
- try {
- const parsedUser = JSON.parse(storedUser);
- setCurrentUser(parsedUser);
- setIsAuthenticated(true);
- } catch (error) {
- localStorage.removeItem('currentUser');
- }
- }
- }, []);
-
- // Función para abrir el modal con la tarea seleccionada
- const openTaskModal = (task) => {
- setSelectedTask(task);
- setModalOpen(true);
- };
+ )}
- // Función para cerrar el modal
- const closeTaskModal = () => {
- setModalOpen(false);
- };
-
- // Función para abrir el modal de cambio de estado
- const openStatusModal = (task) => {
- setTaskForStatusChange(task);
- setStatusModalOpen(true);
- };
-
- // Función para cerrar el modal de cambio de estado
- const closeStatusModal = () => {
- setStatusModalOpen(false);
- setTaskForStatusChange(null);
- };
-
- // Funciones para el modal de creación de sprint
- const openNewSprintModal = () => {
- // Establecer la fecha de inicio por defecto como la fecha actual
- const today = new Date();
- const formattedDate = today.toISOString().split('T')[0]; // Formato YYYY-MM-DD
-
- setNewSprintName('');
- setNewSprintDescription('');
- setNewSprintStartDate(formattedDate);
- setNewSprintDuration(2);
- setNewSprintModalOpen(true);
- };
-
- // Función para cerrar el modal de nuevo sprint
- const closeNewSprintModal = () => {
- setNewSprintModalOpen(false);
- };
-
- // Función para calcular la fecha de finalización basada en la fecha de inicio y la duración
- const calculateEndDate = (startDate, durationWeeks) => {
- if (!startDate) return '';
-
- const start = new Date(startDate);
- const end = new Date(start);
- end.setDate(start.getDate() + (durationWeeks * 7));
-
- return end.toISOString().split('T')[0]; // Formato YYYY-MM-DD
- };
-
- // Función para crear un nuevo sprint
- const createNewSprint = () => {
- if (!newSprintName.trim() || !newSprintStartDate) {
- setError(new Error('Sprint name and start date are required'));
- return;
- }
-
- setIsCreatingSprint(true);
-
- const endDate = calculateEndDate(newSprintStartDate, newSprintDuration);
-
- const sprintData = {
- name: newSprintName,
- description: newSprintDescription,
- startDate: newSprintStartDate,
- endDate: endDate,
- createdBy: currentUser?.id || null
- };
-
- fetch(API_SPRINTS, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify(sprintData)
- })
- .then(response => {
- if (response.ok) {
- return response.json();
- } else {
- throw new Error('Failed to create sprint');
- }
- })
- .then(
- (result) => {
- // Agregar el nuevo sprint a la lista de sprints
- setSprints([...sprints, result]);
- setIsCreatingSprint(false);
- closeNewSprintModal();
- },
- (error) => {
- setIsCreatingSprint(false);
- setError(error);
- }
- );
- };
-
- // Función para cargar los sprints
- const loadSprints = () => {
- setLoadingSprints(true);
- fetch(API_SPRINTS)
- .then(response => {
- if (response.ok) {
- return response.json();
- } else {
- throw new Error('Error loading sprints');
- }
- })
- .then(
- (result) => {
- setSprints(result);
- setLoadingSprints(false);
- },
- (error) => {
- setLoadingSprints(false);
- setError(error);
- });
- };
-
- // Función para cargar los desarrolladores
- const loadDevelopers = () => {
- setLoadingDevelopers(true);
- fetch(API_USERS)
- .then(response => {
- if (response.ok) {
- return response.json();
- } else {
- throw new Error('Error loading developers');
- }
- })
- .then(
- (result) => {
- // Filtrar solo los usuarios con rol "Developer"
- const devs = result.filter(user => user.role === 'Developer');
- setDevelopers(devs);
- setLoadingDevelopers(false);
- },
- (error) => {
- setLoadingDevelopers(false);
- setError(error);
- });
- };
-
- // Función para cargar tareas por sprint
- const loadTasksBySprint = (sprintId) => {
- setLoading(true);
- fetch(`${API_TASKS_BY_SPRINT}/${sprintId}`)
- .then(response => {
- if (response.ok) {
- return response.json();
- } else {
- throw new Error('Error loading tasks for sprint');
- }
- })
- .then(
- (result) => {
- setLoading(false);
- // Adaptando al nuevo formato de datos
- setItems(result.map(item => ({
- id: item.id,
- description: item.description,
- done: item.done,
- createdAt: item.creation_ts,
- status: item.status || 'Pending',
- priority: item.priority || 'Medium',
- steps: item.steps || '',
- assignedTo: item.assignedTo,
- createdBy: item.createdBy,
- isArchived: item.isArchived,
- sprintId: item.sprintId,
- estimatedHours: item.estimatedHours,
- actualHours: item.actualHours
- })));
-
- // Cargar tareas sin sprint
- loadTasksWithoutSprint();
- },
- (error) => {
- setLoading(false);
- setError(error);
- });
- };
-
- // Función para cargar tareas sin sprint asignado (usando filtrado del lado del cliente)
- const loadTasksWithoutSprint = () => {
- // Cargar todas las tareas
- fetch(API_LIST)
- .then(response => {
- if (response.ok) {
- return response.json();
- } else {
- throw new Error('Error loading tasks');
- }
- })
- .then(
- (result) => {
- // Filtrar tareas sin sprint (donde sprintId es null o undefined)
- const tasksWithoutSprint = result.filter(item => !item.sprintId);
-
- // Adaptando al nuevo formato de datos
- setTasksWithoutSprint(tasksWithoutSprint.map(item => ({
- id: item.id,
- description: item.description,
- done: item.done,
- createdAt: item.creation_ts,
- status: item.status || 'Pending',
- priority: item.priority || 'Medium',
- steps: item.steps || '',
- assignedTo: item.assignedTo,
- createdBy: item.createdBy,
- isArchived: item.isArchived,
- sprintId: item.sprintId,
- estimatedHours: item.estimatedHours,
- actualHours: item.actualHours
- })));
- },
- (error) => {
- setError(error);
- });
- };
-
- // Función para manejar el cambio de sprint seleccionado
- const handleSprintChange = (sprintId) => {
- if (sprintId === '') {
- // Si se selecciona "All Tasks", cargamos todas las tareas
- setSelectedSprint(null);
- setLoading(true);
- fetch(API_LIST)
- .then(response => {
- if (response.ok) {
- return response.json();
- } else {
- throw new Error('Error loading all tasks');
- }
- })
- .then(
- (result) => {
- setLoading(false);
- // Adaptando al nuevo formato de datos
- setItems(result.map(item => ({
- id: item.id,
- description: item.description,
- done: item.done,
- createdAt: item.creation_ts,
- status: item.status || 'Pending',
- priority: item.priority || 'Medium',
- steps: item.steps || '',
- assignedTo: item.assignedTo,
- createdBy: item.createdBy,
- isArchived: item.isArchived,
- sprintId: item.sprintId,
- estimatedHours: item.estimatedHours,
- actualHours: item.actualHours
- })));
-
- // Cargar tareas sin sprint
- loadTasksWithoutSprint();
- },
- (error) => {
- setLoading(false);
- setError(error);
- });
- } else {
- // Si se selecciona un sprint específico
- const sprint = sprints.find(s => s.id === parseInt(sprintId));
- setSelectedSprint(sprint);
- loadTasksBySprint(sprintId);
- }
- };
-
- // Función para asignar una tarea a un sprint
- const assignTaskToSprint = (taskId, sprintId) => {
- // Primero obtenemos la tarea actual para preservar sus datos
- fetch(`${API_LIST}/${taskId}`)
- .then(response => {
- if (response.ok) {
- return response.json();
- } else {
- throw new Error('Failed to get task data');
- }
- })
- .then(taskData => {
- // Actualizamos la tarea con el nuevo sprintId
- return fetch(`${API_LIST}/${taskId}`, {
- method: 'PUT', // Usamos PUT en lugar de PATCH
- headers: {
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify({
- ...taskData,
- sprintId: sprintId
- })
- });
- })
- .then(response => {
- if (response.ok) {
- return response.json();
- } else {
- throw new Error('Failed to assign task to sprint');
- }
- })
- .then(
- (result) => {
- // Actualizar las listas de tareas
- if (selectedSprint && selectedSprint.id === sprintId) {
- loadTasksBySprint(sprintId);
- } else {
- // Si la tarea se asignó a otro sprint, solo actualizamos la lista de tareas sin sprint
- loadTasksWithoutSprint();
- }
-
- // Si el modal está abierto con esta tarea, actualizamos la tarea seleccionada
- if (selectedTask && selectedTask.id === taskId) {
- setSelectedTask({
- ...selectedTask,
- sprintId: sprintId
- });
- }
- },
- (error) => {
- setError(error);
- }
- );
- };
-
- function deleteItem(deleteId) {
- fetch(`${API_LIST}/${deleteId}`, {
- method: 'DELETE',
- })
- .then(response => {
- if (response.ok) {
- return response;
- } else {
- throw new Error('Something went wrong ...');
- }
- })
- .then(
- (result) => {
- const remainingItems = items.filter(item => item.id !== deleteId);
- setItems(remainingItems);
- // Si el modal está abierto con esta tarea, cerrarlo
- if (selectedTask && selectedTask.id === deleteId) {
- closeTaskModal();
- }
- },
- (error) => {
- setError(error);
- }
- );
- }
-
- // Función para cambiar el estado de una tarea conservando su prioridad
- function handleStatusChange(task, newStatus, actualHours = null) {
- // Crear el objeto con los datos a actualizar
- const updateData = {
- status: newStatus
- };
-
- // Si hay horas reales y el estado es "Completed", actualizar las horas reales
- if (actualHours && newStatus === 'Completed') {
- updateData.actualHours = parseFloat(actualHours);
- }
-
- // Actualizar el estado de la tarea
- fetch(`${API_LIST}/${task.id}/status`, {
- method: 'PATCH',
- headers: {
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify(updateData)
- })
- .then(response => {
- if (response.ok) {
- return response.json();
- } else {
- throw new Error('Failed to update task status');
- }
- })
- .then(
- (result) => {
- // Si se actualizaron las horas reales, actualizar la tarea completa
- if (actualHours && newStatus === 'Completed') {
- return fetch(`${API_LIST}/${task.id}/actual-hours`, {
- method: 'PATCH',
- headers: {
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify({ hours: parseFloat(actualHours) })
- });
- }
- return Promise.resolve();
- }
- )
- .then(
- (result) => {
- // Recargar la tarea para ver los cambios
- reloadOneItem(task.id);
- },
- (error) => {
- setError(error);
- }
- );
- }
-
- function reloadOneItem(id){
- fetch(`${API_LIST}/${id}`)
- .then(response => {
- if (response.ok) {
- return response.json();
- } else {
- throw new Error('Something went wrong ...');
- }
- })
- .then(
- (result) => {
- const updatedItems = items.map(
- x => (x.id === id ? {
- ...x,
- 'description': result.description,
- 'done': result.done,
- 'status': result.status,
- 'priority': result.priority,
- 'steps': result.steps,
- 'assignedTo': result.assignedTo,
- 'createdBy': result.createdBy,
- 'isArchived': result.isArchived,
- 'creation_ts': result.creation_ts,
- 'estimatedHours': result.estimatedHours,
- 'actualHours': result.actualHours
- } : x));
- setItems(updatedItems);
-
- // Actualizar la tarea seleccionada si está abierta en el modal
- if (selectedTask && selectedTask.id === id) {
- setSelectedTask(updatedItems.find(item => item.id === id));
- }
- },
- (error) => {
- setError(error);
- });
- }
-
- // Deprecado: Este método conserva prioridad, pero ahora usamos handleStatusChange
- function modifyItem(id, description, done) {
- // Encuentra el item actual para preservar otros campos
- const currentItem = items.find(item => item.id === id);
-
- // Actualiza solo los campos necesarios
- var data = {
- "description": description,
- "done": done,
- "status": done ? "Completed" : (currentItem.status === "Completed" ? "In Progress" : currentItem.status),
- "priority": currentItem.priority // Preservamos la prioridad
- };
-
- return fetch(`${API_LIST}/${id}`, {
- method: 'PUT',
- headers: {
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify(data)
- })
- .then(response => {
- if (response.ok) {
- return response;
- } else {
- throw new Error('Something went wrong ...');
- }
- });
- }
-
- function updateTaskFromModal(id, updatedTask) {
- fetch(`${API_LIST}/${id}`, {
- method: 'PUT',
- headers: {
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify(updatedTask)
- })
- .then(response => {
- if (response.ok) {
- return response.json();
- } else {
- throw new Error('Failed to update task');
- }
- })
- .then(
- (result) => {
- reloadOneItem(id);
- closeTaskModal();
- },
- (error) => {
- setError(error);
- }
- );
- }
-
- useEffect(() => {
- // Solo cargar datos si el usuario está autenticado
- if (isAuthenticated) {
- // Cargar sprints y desarrolladores al iniciar
- loadSprints();
- loadDevelopers();
-
- // Cargar todas las tareas inicialmente
- setLoading(true);
- fetch(API_LIST)
- .then(response => {
- if (response.ok) {
- return response.json();
- } else {
- throw new Error('Something went wrong ...');
- }
- })
- .then(
- (result) => {
- setLoading(false);
- // Adaptando al nuevo formato de datos
- setItems(result.map(item => ({
- id: item.id,
- description: item.description,
- done: item.done,
- createdAt: item.creation_ts,
- status: item.status || 'Pending',
- priority: item.priority || 'Medium',
- steps: item.steps || '',
- assignedTo: item.assignedTo,
- createdBy: item.createdBy,
- isArchived: item.isArchived,
- sprintId: item.sprintId,
- estimatedHours: item.estimatedHours,
- actualHours: item.actualHours
- })));
-
- // Cargar tareas sin sprint
- loadTasksWithoutSprint();
- },
- (error) => {
- setLoading(false);
- setError(error);
- });
- }
- }, [isAuthenticated]);
-
- function addItem(text, steps, priority, sprintId, assignedTo, estimatedHours) {
- setInserting(true);
-
- // Obtener la fecha y hora actual
- const currentTimestamp = new Date().toISOString();
-
- // Si el usuario es developer, auto-asignar la tarea a sí mismo
- const taskAssignedTo = isDeveloper ? currentUser.id : assignedTo;
-
- var data = {
- description: text,
- done: false,
- status: "Pending", // Siempre comenzar en Pending
- priority: priority,
- steps: steps || '',
- creation_ts: currentTimestamp,
- sprintId: sprintId || null,
- assignedTo: taskAssignedTo,
- createdBy: currentUser?.id || null, // Asignar el creador como el usuario actual
- estimatedHours: estimatedHours || null
- };
-
- fetch(API_LIST, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify(data),
- }).then((response) => {
- if (response.ok) {
- return response;
- } else {
- throw new Error('Something went wrong ...');
- }
- }).then(
- (result) => {
- // Obtenemos la ubicación del nuevo recurso
- var id = result.headers.get('location');
-
- // Creamos un nuevo item con los datos disponibles
- var newItem = {
- id: id,
- description: text,
- done: false,
- status: "Pending",
- priority: priority,
- steps: steps || '',
- createdAt: currentTimestamp,
- assignedTo: taskAssignedTo,
- createdBy: currentUser?.id || null,
- isArchived: 0,
- sprintId: sprintId || null,
- estimatedHours: estimatedHours || null
- };
-
- // Si hay un sprint seleccionado y la tarea pertenece a ese sprint, la agregamos a la lista
- if (selectedSprint && sprintId === selectedSprint.id) {
- setItems([newItem, ...items]);
- }
- // Si no hay sprint seleccionado y la tarea no tiene sprint, la agregamos a la lista
- else if (!selectedSprint && !sprintId) {
- setItems([newItem, ...items]);
- }
-
- // Si la tarea no tiene sprint asignado, la agregamos a la lista de tareas sin sprint
- if (!sprintId) {
- setTasksWithoutSprint([newItem, ...tasksWithoutSprint]);
- }
-
- setInserting(false);
- },
- (error) => {
- setInserting(false);
- setError(error);
- }
- );
- }
-
- // Filtrado por prioridad
- const filteredItems = items.filter(item => {
- // Filtro por prioridad
- let priorityMatch = priorityFilter === 'All' || item.priority === priorityFilter;
-
- // Para desarrolladores, solo mostrar tareas asignadas a ellos
- let assigneeMatch = true;
- if (isDeveloper) {
- assigneeMatch = item.assignedTo === currentUser.id;
- }
-
- return priorityMatch && assigneeMatch;
- });
-
- // Agrupar por estado
- const pendingItems = filteredItems.filter(item => item.status === 'Pending');
- const inProgressItems = filteredItems.filter(item => item.status === 'In Progress');
- const inReviewItems = filteredItems.filter(item => item.status === 'In Review');
- const completedItems = filteredItems.filter(item => item.status === 'Completed');
-
- const priorityOptions = ['All', 'Low', 'Medium', 'High', 'Critical'];
-
- // Función para renderizar las filas de tareas
- const renderTaskRows = (tasks) => {
- if (tasks.length === 0) {
- return (
-
-
- No tasks in this section.
-
-
- );
- }
-
- return tasks.map(item => (
-
openTaskModal(item)} className="task-row">
-
- {item.description}
- {item.estimatedHours && (
-
-
-
Est: {item.estimatedHours}h
- {item.actualHours && (
-
/ Act: {item.actualHours}h
- )}
-
- )}
-
-
-
- {item.priority}
-
-
-
- {item.createdAt && {item.createdAt} }
-
- e.stopPropagation()}>
- {/* Para usuarios no desarrolladores o para developers que son asignados a esta tarea */}
- {(!isDeveloper || (isDeveloper && item.assignedTo === currentUser.id)) && (
- {
- event.stopPropagation();
- setTaskForStatusChange(item);
- setStatusModalOpen(true);
- }}
- size="small"
- >
- Status
-
- )}
-
-
- ));
- };
-
- // Filtrado de tareas sin sprint (para desarrolladores, mostrar solo sus tareas)
- const filteredTasksWithoutSprint = tasksWithoutSprint.filter(item => {
- if (isDeveloper) {
- return item.assignedTo === currentUser.id;
- }
- return true;
- });
-
- // Estilos para el modal
- const modalStyle = {
- position: 'absolute',
- top: '50%',
- left: '50%',
- transform: 'translate(-50%, -50%)',
- width: '80%',
- maxWidth: 600,
- maxHeight: '80vh',
- overflow: 'auto',
- bgcolor: '#312D2A', // Color de fondo Oracle Dark
- boxShadow: 24,
- p: 4,
- borderRadius: '8px',
- color: 'white',
- border: '1px solid rgba(255, 255, 255, 0.1)'
- };
-
- // Si el usuario no está autenticado, mostrar el componente de login
- if (!isAuthenticated) {
- return
;
- }
-
- return (
-
- {/* Botón de logout en la esquina superior derecha */}
-
-
- Logout
-
-
-
- {/* Oracle Logo (tamaño aumentado) */}
-
-
-
TODO LIST
-
- {/* Mostrar el nombre del usuario actual y su rol */}
- {currentUser && (
-
- Welcome, {currentUser.name || currentUser.username}
- ({currentUser.role})
-
- )}
-
- {/* Pestañas de navegación - Sólo mostrar las pestañas permitidas */}
-
-
setActiveTab('tasks')}
- >
- Tasks
-
-
- {/* Sólo mostrar GitHub y KPI Reports para no desarrolladores */}
- {!isDeveloper && (
- <>
-
setActiveTab('github')}
- >
- GitHub Integration
-
-
setActiveTab('reports')}
- >
- KPI Reports
-
- >
- )}
+
+
setActiveTab('tasks')}
+ >
+ Tasks
-
- {/* Contenido condicional según la pestaña activa */}
- {activeTab === 'tasks' ? (
+ {!currentUser?.role === 'Developer' && (
<>
- {/* Selector de Sprint */}
-
-
Select Sprint
-
-
- Sprint
- handleSprintChange(e.target.value)}
- label="Sprint"
- className="sprint-select"
- disabled={loadingSprints}
- >
- All Tasks
- {sprints.map(sprint => (
-
- {sprint.name}
-
- ))}
-
-
- {loadingSprints &&
}
-
-
- {/* Sólo mostrar botón para crear sprints a no desarrolladores */}
- {!isDeveloper && (
-
- Create New Sprint
-
- )}
+
setActiveTab('github')}
+ >
+ GitHub Integration
-
-
-
- { error &&
-
-
Error: {error.message}
-
- }
-
- {/* Filtros (solo por prioridad) */}
-
-
- Priority:
- setPriorityFilter(e.target.value)}
- >
- {priorityOptions.map(option => (
- {option}
- ))}
-
-
+
setActiveTab('reports')}
+ >
+ KPI Reports
-
- { isLoading ? (
-
- ) : (
-
- {selectedSprint && (
-
-
Sprint: {selectedSprint.name}
-
{selectedSprint.description}
-
- Start: {new Date(selectedSprint.startDate).toLocaleDateString()}
- End: {new Date(selectedSprint.endDate).toLocaleDateString()}
-
-
- )}
-
- {/* Pending Tasks Section */}
-
-
Pending {pendingItems.length}
-
-
-
-
- {renderTaskRows(pendingItems)}
-
-
-
- {/* In Progress Tasks Section */}
-
-
In Progress {inProgressItems.length}
-
-
-
-
- {renderTaskRows(inProgressItems)}
-
-
-
- {/* In Review Tasks Section */}
-
-
In Review {inReviewItems.length}
-
-
-
-
- {renderTaskRows(inReviewItems)}
-
-
-
- {/* Completed Tasks Section */}
-
-
Completed {completedItems.length}
-
-
-
-
- {renderTaskRows(completedItems)}
-
-
-
- {/* Tasks Without Sprint Section */}
-
-
-
Tasks Without Sprint {filteredTasksWithoutSprint.length}
-
-
-
-
- {filteredTasksWithoutSprint.length === 0 ? (
-
-
- No tasks without sprint.
-
-
- ) : (
- filteredTasksWithoutSprint.map(item => (
- openTaskModal(item)} className="task-row">
-
- {item.description}
- {item.estimatedHours && (
-
-
-
Est: {item.estimatedHours}h
- {item.actualHours && (
-
/ Act: {item.actualHours}h
- )}
-
- )}
-
-
-
- {item.priority}
-
-
-
- {item.createdAt && {item.createdAt} }
-
- e.stopPropagation()}>
- {selectedSprint && !isDeveloper && (
- {
- event.stopPropagation();
- assignTaskToSprint(item.id, selectedSprint.id);
- }}
- size="small"
- >
- Assign to Sprint
-
- )}
- {(!isDeveloper || (isDeveloper && item.assignedTo === currentUser.id)) && !selectedSprint && (
- {
- event.stopPropagation();
- setTaskForStatusChange(item);
- setStatusModalOpen(true);
- }}
- size="small"
- >
- Status
-
- )}
-
-
- ))
- )}
-
-
-
-
- )}
>
- ) : activeTab === 'github' ? (
-
- ) : (
-
)}
-
- {/* Modal para detalles de tarea */}
-
-
- {selectedTask && (
- <>
-
-
- Task Details
-
-
-
-
-
-
-
-
- Description
-
- {selectedTask.description}
-
-
-
- {selectedTask.steps && (
-
-
Steps
-
- {selectedTask.steps.split('\n').map((step, index) => (
-
- {step}
-
- ))}
-
-
- )}
-
-
-
- Status
-
- {selectedTask.status}
-
-
-
-
- Priority
-
- {selectedTask.priority}
-
-
-
- {/* Mostrar las horas estimadas */}
- {selectedTask.estimatedHours && (
-
- Estimated Hours
-
- {selectedTask.estimatedHours}h
-
-
- )}
-
- {/* Mostrar las horas reales */}
- {selectedTask.actualHours && (
-
- Actual Hours
-
- {selectedTask.actualHours}h
-
-
- )}
-
- {/* Mostrar el sprint asignado */}
-
- Sprint
-
- {selectedTask.sprintId ?
- (sprints.find(s => s.id === selectedTask.sprintId)?.name || `Sprint ID: ${selectedTask.sprintId}`) :
- 'Not assigned'}
-
-
-
-
- Created
-
- {selectedTask.createdAt && {selectedTask.createdAt} }
-
-
-
- {selectedTask.assignedTo && (
-
- Assigned To
-
- {developers.find(d => d.id === selectedTask.assignedTo)?.name || `User ID: ${selectedTask.assignedTo}`}
-
-
- )}
-
- {selectedTask.createdBy && (
-
- Created By
-
- User ID: {selectedTask.createdBy}
-
-
- )}
-
-
-
- {/* Botón para cambiar estado disponible solo para no desarrolladores o para developers asignados a esta tarea */}
- {(!isDeveloper || (isDeveloper && selectedTask.assignedTo === currentUser.id)) && (
- {
- setTaskForStatusChange(selectedTask);
- setStatusModalOpen(true);
- closeTaskModal();
- }}
- >
- Change Status
-
- )}
-
- {/* Botón para eliminar disponible solo para no desarrolladores */}
- {!isDeveloper && selectedTask.status === "Completed" && (
- {
- deleteItem(selectedTask.id);
- closeTaskModal();
- }}
- >
- Delete Task
-
- )}
-
-
- >
- )}
-
-
-
- {/* Modal para crear un nuevo sprint */}
-
-
-
-
- Create New Sprint
-
-
-
-
-
-
-
-
- Name
- setNewSprintName(e.target.value)}
- className="modal-input"
- />
-
-
-
- Description
-
-
-
- Start Date
- setNewSprintStartDate(e.target.value)}
- className="modal-input"
- />
-
-
-
- Duration (weeks)
- setNewSprintDuration(Number(e.target.value))}
- className="modal-input"
- min="1"
- max="12"
- />
-
-
-
-
- {isCreatingSprint ? 'Creating...' : 'Create Sprint'}
-
-
-
-
-
-
- {/* Modal para cambiar el estado de una tarea */}
-
{
- if (taskForStatusChange) {
- handleStatusChange(taskForStatusChange, newStatus, actualHours);
- }
- }}
- />
- );
+
+ {activeTab === 'tasks' &&
}
+ {activeTab === 'github' &&
}
+ {activeTab === 'reports' &&
}
+
+ );
}
export default App;
\ No newline at end of file
diff --git a/MtdrSpring/backend/src/main/frontend/src/API.js b/MtdrSpring/backend/src/main/frontend/src/components/API.js
similarity index 100%
rename from MtdrSpring/backend/src/main/frontend/src/API.js
rename to MtdrSpring/backend/src/main/frontend/src/components/API.js
diff --git a/MtdrSpring/backend/src/main/frontend/src/GitHubIntegration.js b/MtdrSpring/backend/src/main/frontend/src/components/GitHubIntegration.js
similarity index 100%
rename from MtdrSpring/backend/src/main/frontend/src/GitHubIntegration.js
rename to MtdrSpring/backend/src/main/frontend/src/components/GitHubIntegration.js
diff --git a/MtdrSpring/backend/src/main/frontend/src/components/Login.js b/MtdrSpring/backend/src/main/frontend/src/components/Login.js
new file mode 100644
index 0000000..8c09622
--- /dev/null
+++ b/MtdrSpring/backend/src/main/frontend/src/components/Login.js
@@ -0,0 +1,70 @@
+import React, { useState } from 'react';
+import { API_USERS } from './API';
+
+function Login({ onLogin, loginError }) {
+ const [username, setUsername] = useState('');
+ const [password, setPassword] = useState('');
+ const [isLoading, setIsLoading] = useState(false);
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+ if (!username || !password) return;
+
+ setIsLoading(true);
+
+ try {
+ const response = await fetch(`${API_USERS}/username/${username}`);
+ if (!response.ok) throw new Error('Invalid credentials');
+
+ const userData = await response.json();
+ if (userData.password === password) {
+ localStorage.setItem(
+ 'currentUser',
+ JSON.stringify({
+ id: userData.id,
+ username: userData.username,
+ name: userData.name,
+ role: userData.role,
+ })
+ );
+ onLogin(userData);
+ } else {
+ throw new Error('Invalid credentials');
+ }
+ } catch (error) {
+ onLogin(null, error.message);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ return (
+
+ );
+}
+
+export default Login;
\ No newline at end of file
diff --git a/MtdrSpring/backend/src/main/frontend/src/ReportsDashboard.css b/MtdrSpring/backend/src/main/frontend/src/components/ReportsDashboard.css
similarity index 100%
rename from MtdrSpring/backend/src/main/frontend/src/ReportsDashboard.css
rename to MtdrSpring/backend/src/main/frontend/src/components/ReportsDashboard.css
diff --git a/MtdrSpring/backend/src/main/frontend/src/ReportsDashboard.js b/MtdrSpring/backend/src/main/frontend/src/components/ReportsDashboard.js
similarity index 97%
rename from MtdrSpring/backend/src/main/frontend/src/ReportsDashboard.js
rename to MtdrSpring/backend/src/main/frontend/src/components/ReportsDashboard.js
index 58d9164..e04fe2f 100644
--- a/MtdrSpring/backend/src/main/frontend/src/ReportsDashboard.js
+++ b/MtdrSpring/backend/src/main/frontend/src/components/ReportsDashboard.js
@@ -151,7 +151,7 @@ function ReportsDashboard() {
if (selectedSprint) {
setLoading(true);
- // Cargar tareas completadas
+ // Load completed tasks
fetch(`${API_URL}/reports/sprint/${selectedSprint}/completed-tasks`)
.then(response => {
if (response.ok) {
@@ -187,7 +187,6 @@ function ReportsDashboard() {
});
// Fetch KPI data for all developers in the selected sprint
- // Vamos a cargar los KPI para cada desarrollador individualmente
if (users && users.length > 0) {
Promise.all(
users.map(user =>
@@ -201,7 +200,7 @@ function ReportsDashboard() {
})
.then(data => {
if (data) {
- // Añadir información del usuario al objeto KPI
+ // Add information of the user to KPI object
return {
...data,
userId: user.id,
@@ -216,7 +215,7 @@ function ReportsDashboard() {
})
)
).then(results => {
- // Filtrar resultados nulos
+ // Filter null results
const validResults = results.filter(result => result !== null);
setAllUsersKpi(validResults);
@@ -742,8 +741,8 @@ function ReportsDashboard() {
Comparativas
-
handleTabChange(e, 4)}
>
@@ -802,9 +801,11 @@ function ReportsDashboard() {
if (actualHours > 0) {
const efficiencyValue = (estimatedHours / actualHours) * 100;
- if (efficiencyValue < 80) {
+ const EFFICIENCY_LOW_THRESHOLD = 80;
+ const EFFICIENCY_HIGH_THRESHOLD = 120;
+ if (efficiencyValue < EFFICIENCY_LOW_THRESHOLD) {
efficiencyClass = 'efficiency-low';
- } else if (efficiencyValue > 120) {
+ } else if (efficiencyValue > EFFICIENCY_HIGH_THRESHOLD) {
efficiencyClass = 'efficiency-high';
} else {
efficiencyClass = 'efficiency-medium';
@@ -818,7 +819,7 @@ function ReportsDashboard() {
{formatHours(task.estimatedHours)}
{formatHours(task.actualHours)}
- {task.actualHours > 0
+ {task.actualHours > 0
? formatPercentage((task.estimatedHours / task.actualHours) * 100)
: 'N/A'}
@@ -1061,22 +1062,21 @@ function ReportsDashboard() {
{userKpi.tasks.map((task, index) => {
// Calcular clase de eficiencia
- let efficiencyClass = '';
- if (task.completed && task.actualHours > 0) {
- const efficiencyValue = (task.estimatedHours / task.actualHours) * 100;
- if (efficiencyValue < 80) {
- efficiencyClass = 'efficiency-low';
- } else if (efficiencyValue > 120) {
- efficiencyClass = 'efficiency-high';
- } else {
- efficiencyClass = 'efficiency-medium';
+ let efficiencyClass = calculateEfficiencyClass(task);
+ function calculateEfficiencyClass(task) {
+ if (task.completed && task.actualHours > 0) {
+ const efficiencyValue = (task.estimatedHours / task.actualHours) * 100;
+ if (efficiencyValue < 80) return 'efficiency-low';
+ if (efficiencyValue > 120) return 'efficiency-high';
+ return 'efficiency-medium';
}
+ return '';
}
// Calcular clase de estado
let statusClass = '';
- const status = task.status || 'Pending';
- switch(status) {
+ const taskStatus = task.status || 'Pending';
+ switch(taskStatus) {
case 'Pending':
statusClass = 'status-pending';
break;
@@ -1098,7 +1098,7 @@ function ReportsDashboard() {
{task.description}
- {status}
+ {taskStatus}
{formatHours(task.estimatedHours)}
diff --git a/MtdrSpring/backend/src/main/frontend/src/components/TaskManager.js b/MtdrSpring/backend/src/main/frontend/src/components/TaskManager.js
new file mode 100644
index 0000000..0348966
--- /dev/null
+++ b/MtdrSpring/backend/src/main/frontend/src/components/TaskManager.js
@@ -0,0 +1,47 @@
+import React, { useState, useEffect } from 'react';
+import TaskTable from './TaskTable';
+import { API_LIST, API_SPRINTS } from './API';
+
+function TaskManager({ currentUser }) {
+ const [tasks, setTasks] = useState([]);
+ const [sprints, setSprints] = useState([]);
+ const [selectedSprint, setSelectedSprint] = useState(null);
+ const [isLoading, setIsLoading] = useState(false);
+
+ useEffect(() => {
+ loadTasks();
+ loadSprints();
+ }, []);
+
+ const loadTasks = async () => {
+ setIsLoading(true);
+ try {
+ const response = await fetch(API_LIST);
+ const data = await response.json();
+ setTasks(data);
+ } catch (error) {
+ console.error('Error loading tasks:', error);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const loadSprints = async () => {
+ try {
+ const response = await fetch(API_SPRINTS);
+ const data = await response.json();
+ setSprints(data);
+ } catch (error) {
+ console.error('Error loading sprints:', error);
+ }
+ };
+
+ return (
+
+
Tasks
+
+
+ );
+}
+
+export default TaskManager;
\ No newline at end of file
diff --git a/MtdrSpring/backend/src/main/frontend/src/components/TaskTable.js b/MtdrSpring/backend/src/main/frontend/src/components/TaskTable.js
new file mode 100644
index 0000000..4663fc2
--- /dev/null
+++ b/MtdrSpring/backend/src/main/frontend/src/components/TaskTable.js
@@ -0,0 +1,26 @@
+import React from 'react';
+
+function TaskTable({ tasks, currentUser }) {
+ return (
+
+
+
+ Description
+ Status
+ Priority
+
+
+
+ {tasks.map((task) => (
+
+ {task.description}
+ {task.status}
+ {task.priority}
+
+ ))}
+
+
+ );
+}
+
+export default TaskTable;
\ No newline at end of file
diff --git a/MtdrSpring/backend/src/main/frontend/src/github-integration.css b/MtdrSpring/backend/src/main/frontend/src/components/github-integration.css
similarity index 100%
rename from MtdrSpring/backend/src/main/frontend/src/github-integration.css
rename to MtdrSpring/backend/src/main/frontend/src/components/github-integration.css