Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Mis 8 horas
Dashboard personal conectado a ClickUp para cumplir una meta de 8 horas diarias de tareas completadas. Muestra qué tareas ya suman horas, cuáles faltan cerrar y cuánto falta planificar, sugiere tareas para llenar el hueco del día y permite crear, mover, estimar y cerrar tareas sin salir de la página.
- Backend: NestJS. Habla con ClickUp, guarda la API key, calcula todas las métricas y expone una API propia.
- Frontend: Angular. Solo muestra los datos que calcula el backend y envía acciones.
Es una herramienta de un solo usuario: solo muestra las tareas asignadas al dueño de la API key.
Existe un prototipo funcional en prototipo-html/ (sección 13), que es también el respaldo si no se puede levantar NestJS o Angular. Úsalo como referencia visual y de lógica.
Contenido
- Objetivo
- Reglas de negocio
- Arquitectura
- API propia
- Backend
- Frontend
- Interfaz
- Textos
- Diseño visual
- Plan de desarrollo
- Criterios de aceptación
- Ideas para después
- Prototipo HTML
1. Objetivo
Que el usuario vea de un vistazo, para cada día:
- Cuántas horas ya completó (HPC).
- Cuántas horas tiene planificadas y pendientes de cerrar.
- Cuántas horas le faltan planificar para llegar a la meta.
- Qué tareas puede traer a ese día para completar las 8 horas.
- En qué Sprint está ese día y si el día es feriado o de vacaciones.
Y que pueda, desde la misma página: crear una tarea para el día, editar su estimado, moverla de día y marcarla como terminado.
2. Reglas de negocio
Replican el dashboard "Cumplimiento diario" de la empresa. Viven solo en el backend, en MetricsService.
| Concepto | Definición |
|---|---|
| Día de una tarea | Fecha de vencimiento (due_date) convertida a la zona horaria configurada (America/Lima). |
| Tarea completada | status.type es closed o done, o la tarea tiene date_closed. Ver "Pendiente de confirmar". |
| HPL (horas planificadas) | Suma de time_estimate de las tareas con fecha ese día. |
| HPC (planificadas completadas) | Suma de time_estimate de las tareas con fecha ese día y completadas. |
| Por cerrar | Tareas con fecha ese día, con estimación y no completadas. |
| No suman | Tareas con fecha ese día sin estimación (time_estimate nulo o 0). Se muestran con aviso. |
| Meta | Configurable, 8 h por defecto. En un día libre (feriado o vacaciones) es 0. |
| Falta planificar | max(0, meta − HPC − por cerrar) |
| Deuda del día | max(0, meta − HPC) |
| Día libre | Fecha de la lista de días libres. Meta 0, sin deuda ni "falta planificar", y no cuenta como día transcurrido. |
| Rendimiento semanal | HPC acumulado ÷ (meta × días hábiles transcurridos hasta hoy). No cuenta días futuros ni días libres. |
| Sprint de un día | El Sprint cuyo rango (desde - hasta, tomado del nombre de la lista) contiene el día. Si un día cae en dos Sprints (el hasta de uno es el desde del siguiente), gana el que empieza más tarde. |
| Sprint abierto | Sprint cuya fecha final no ha pasado. |
Colores del rendimiento: verde ≥ 90 %, amarillo 70–89 %, rojo < 70 %.
2.1 Decisiones tomadas
Decisión (2026-10-15): tareas de varios días. Una tarea que abarca varios días se cuenta completa en el día de su due_date. start_date no se usa (se evaluó un interruptor para elegir entre ambas y se descartó el 2026-10-19). Para repartir trabajo largo, dividir la tarea en subtareas, una por día. Repartir la estimación por rango queda como idea futura.
Decisión (2026-10-15): crear tarea.
- Se crea siempre con estado
asignado, así que no suma al HPC hasta que se marca comoterminado. Mientras tanto aparece en "Por cerrar" del día elegido. - Obligatorios: asunto, fecha, Sprint, tiempo estimado mayor que 0, Clientes, Jefatura y al menos un asignado.
- Si el día queda por encima de la meta, solo se avisa; la tarea se crea igual.
- Si el día es libre, se avisa que no debería tener tareas; la tarea se crea igual.
- La lista (Sprint) se elige en cada creación, entre los Sprints abiertos ordenados por número. Por defecto, el Sprint al que pertenece el día seleccionado.
- El tiempo estimado se propone con el hueco del día (
gapMs), o 1:00 si el día ya llegó a la meta.
Decisión (2026-10-15): días libres.
- Feriado y vacaciones tienen el mismo efecto. Solo cambia la etiqueta.
- ClickUp no expone sus feriados por API, y el portal de la empresa los guarda por usuario en el navegador. Por eso son una lista que mantiene el usuario a mano.
- En el prototipo se cargaron los 16 feriados nacionales de Chile 2026, según prensa. Confirmar contra el calendario oficial.
Decisión (2026-10-15): acciones sobre tareas existentes. Desde la vista del día se puede editar el estimado, mover la tarea a otra fecha y marcarla como terminado. No hay "deshacer": un error se corrige en ClickUp.
2.2 Pendiente de confirmar
Con quien mantiene el dashboard de la empresa:
- Si el día se define por fecha de vencimiento o de inicio (se usa
due_datede forma provisional). - Qué estados cuentan como completados. En las listas de Sprint los estados son:
to do(open),asignadoydevuelto a desarrollo(unstarted),en progresoyen pausa(custom),cancelado,revision consultoria,revision clienteylisto para deploy(done), yterminado(closed). La regla actual (closedodone) cuenta como completadas las cuatro de tipodone, incluidocancelado, que probablemente no debería sumar horas. - Si las subtareas suman por separado o se descuentan del padre.
- Horas cerradas en un día libre: si suman como extra al rendimiento semanal (comportamiento actual del prototipo) o se excluyen del todo. Recomendado: excluirlas, para que un día libre no altere el porcentaje.
- Diferencia con el portal: el 6 de octubre el portal mostraba +1:30 de horas adicionales mientras ClickUp sumaba exactamente 8:00 (2:00 + 6:00). Sin explicar; posible dato desactualizado en el portal o una regla distinta.
3. Arquitectura
┌──────────────┐ /api/* ┌──────────────────────┐ HTTPS + API key ┌─────────┐
│ Angular │ ────────► │ NestJS │ ────────────────► │ ClickUp │
│ (localhost: │ ◄──────── │ (localhost:3000) │ ◄──────────────── │ API v2 │
│ 4200) │ JSON │ métricas + caché │ └─────────┘
└──────────────┘ └──────────────────────┘
- La API key solo existe en el backend (
.env). El navegador nunca la ve. - El backend entrega los datos ya calculados y agrupados por día. Angular no recalcula HPC ni deuda.
- En desarrollo, Angular usa
proxy.conf.jsonpara mandar/apial backend, así no hay problemas de CORS.
Estructura del repositorio:
mis-8-horas/
prototipo-html/ # respaldo autónomo en un solo HTML (sección 13)
backend/ # nest new backend
frontend/ # ng new frontend --standalone --routing=false --style=scss
contracts/ # tipos TypeScript compartidos (sección 4.1)
README.md
CLAUDE.md
contracts/ se importa en ambos proyectos mediante paths en sus tsconfig.json ("@contracts/*": ["../contracts/*"]).
4. API propia
4.1 Tipos compartidos (contracts/index.ts)
export interface ClientTagDto {
name: string;
color: string | null; // color de la opción en ClickUp, si tiene
}
export interface TaskDto {
id: string;
name: string;
url: string;
closed: boolean;
status: string;
color: string;
estimateMs: number; // 0 si no tiene
dueDate: string | null; // "YYYY-MM-DD" en zona local, o null
path: string; // "Espacio / Carpeta / Lista"
clients: ClientTagDto[]; // valor del Custom Field "Clientes"; vacío si no tiene
daysLate?: number; // solo en backlog, si está vencida
fits?: boolean; // solo en sugerencias: cabe en el hueco del día pedido
}
export interface SprintDto {
id: string; // id de la lista en ClickUp
name: string; // "Sprint 19 (10/5 - 10/19)"
number: number; // 19
start: string; // "YYYY-MM-DD", tomado del nombre
end: string; // "YYYY-MM-DD", tomado del nombre
open: boolean; // end >= hoy
}
export interface DayOffDto {
date: string; // "YYYY-MM-DD"
label: string; // "Feriado", "Vacaciones"…
}
export interface DayDto {
date: string; // "YYYY-MM-DD"
weekday: number; // 0 = domingo … 6 = sábado
isToday: boolean;
isPast: boolean;
dayOff: string | null; // etiqueta del día libre, o null si es día normal
sprint: SprintDto | null;
metaMs: number; // 0 si es día libre
doneMs: number; // HPC
pendingMs: number; // por cerrar
gapMs: number; // falta planificar
debtMs: number; // deuda
done: TaskDto[];
pending: TaskDto[]; // ordenadas por estimación desc.
noEstimate: TaskDto[];
}
export interface WeekDto {
weekStart: string; // lunes, "YYYY-MM-DD"
days: DayDto[]; // 5 o 7 según ajustes
weekly: {
doneMs: number;
metaMs: number;
elapsedDays: number; // excluye días libres
performance: number; // 0–100, redondeado
level: 'green' | 'yellow' | 'red';
};
fetchedAt: string; // ISO, para mostrar "actualizado hace X min"
}
export interface SuggestionsDto {
date: string;
gapMs: number;
items: TaskDto[]; // ya ordenadas (sección 7.4)
counts: { all: number; overdue: number; noDate: number };
}
export interface UserRefDto {
id: string;
name: string;
}
export interface NewTaskMetaDto {
sprints: SprintDto[]; // solo abiertos, ordenados por número
clients: { id: string; name: string }[]; // opciones del campo "Clientes"
users: UserRefDto[]; // miembros del espacio de trabajo (Jefatura y Assignees)
me: UserRefDto;
}
export interface CreateTaskDto {
name: string;
date: string; // "YYYY-MM-DD"
listId: string; // id del Sprint elegido
estimateMs: number; // > 0
clientId: string; // id de la opción de "Clientes"
bossUserId: string; // usuario de "Jefatura"
assigneeIds: string[]; // al menos uno
}
export interface TaskActionResultDto {
week: WeekDto;
suggestions: SuggestionsDto;
}
export interface SettingsDto {
metaHours: number; // 1–16, pasos de 0.5
includeWeekends: boolean;
teamId: string | null;
autoRefreshMin: 0 | 5 | 10 | 15;
alertEnabled: boolean; // aviso de media tarde (opcional)
alertTime: string; // "16:00"
}
export interface MeDto {
username: string;
email: string;
teams: { id: string; name: string }[];
}
4.2 Endpoints
| Método | Ruta | Descripción |
|---|---|---|
| GET | /api/me |
Usuario de ClickUp y sus espacios de trabajo. |
| GET | /api/week?start=YYYY-MM-DD&refresh=true |
Semana calculada. start se normaliza al lunes. refresh=true ignora la caché. |
| GET | /api/suggestions?date=YYYY-MM-DD&filter=all|overdue|noDate |
Tareas atrasadas o sin fecha, ordenadas para llenar el hueco de ese día. |
| GET | /api/sprints |
Todos los Sprints de la carpeta, con open. |
| GET | /api/tasks/new-meta |
NewTaskMetaDto: Sprints abiertos, opciones de Clientes, usuarios y el usuario actual. |
| POST | /api/tasks |
Body CreateTaskDto. Crea la tarea con estado asignado, invalida la caché y devuelve TaskActionResultDto. |
| PATCH | /api/tasks/:id/due |
Body { "date": "YYYY-MM-DD" }. Cambia el vencimiento (también para mover). Devuelve TaskActionResultDto. |
| PATCH | /api/tasks/:id/estimate |
Body { "estimateMs": number }, mayor que 0. Cambia time_estimate. Devuelve TaskActionResultDto. |
| PATCH | /api/tasks/:id/status |
Body { "status": "terminado" }. Cambia el estado. Devuelve TaskActionResultDto. |
| GET | /api/days-off |
Lista de días libres (DayOffDto[]). |
| PUT | /api/days-off |
Reemplaza la lista de días libres. Valida el formato de las fechas. |
| GET | /api/settings |
Ajustes actuales. |
| PUT | /api/settings |
Guarda los ajustes. Valida con class-validator. |
| GET | /api/health |
Comprueba que la API key funciona (/user de ClickUp). |
Errores con el formato estándar de Nest ({ statusCode, message }):
- 401 si la key de ClickUp es inválida: "La API key no es válida o expiró. Revisa el archivo .env del backend."
- 400 si ClickUp rechaza un valor (por ejemplo
Invalid Valueen un Custom Field). Pasar el mensaje al usuario. - 502 si ClickUp no responde.
- 429 si se alcanza el límite de ClickUp.
5. Backend (NestJS)
5.1 Configuración
# backend/.env (en .gitignore)
CLICKUP_API_KEY=pk_xxxxxxxx
CLICKUP_TEAM_ID= # opcional; si está vacío se usa el de settings o el primero
CLICKUP_SPRINTS_FOLDER_ID=90117541378 # carpeta "Sprints 2026" con las listas "Sprint NN (M/D - M/D)"
CLICKUP_CLIENT_FIELD=Clientes # nombre del Custom Field de cliente (desplegable)
CLICKUP_BOSS_FIELD=Jefatura # nombre del Custom Field de jefatura (personas)
CLICKUP_NEW_STATUS=asignado # estado con el que se crean las tareas
CLICKUP_DONE_STATUS=terminado # estado con el que se cierran
TZ=America/Lima
PORT=3000
CACHE_TTL_SECONDS=60
- Usar
@nestjs/configcon validación del esquema (Joi oclass-validator). La app no debe arrancar sinCLICKUP_API_KEY. - Manejar las fechas con Luxon, usando la zona de
TZ. Nunca usarnew Date()sin zona para agrupar por día. - Los ajustes se guardan en
backend/data/settings.json.SettingsServicelee, valida y escribe ese archivo, con valores por defecto si no existe. - Los días libres se guardan en
backend/data/days-off.json, con la forma[{ "date": "YYYY-MM-DD", "label": "Feriado" }].DaysOffServicedevuelve lista vacía si no existe el archivo.
5.2 Módulos
backend/src/
app.module.ts
config/env.validation.ts
clickup/
clickup.module.ts
clickup.client.ts // HttpService (@nestjs/axios), header Authorization
clickup.mapper.ts // respuesta de ClickUp → Task interno (incluye clients)
clickup.types.ts // tipos crudos de la API de ClickUp
settings/ // GET/PUT /api/settings, lee/escribe data/settings.json
days-off/ // GET/PUT /api/days-off, lee/escribe data/days-off.json
sprints/
sprints.service.ts // lista Sprints, parsea el nombre, sprintOf(date), abiertos
metrics/
metrics.service.ts // FUNCIONES PURAS: buildDay, buildWeek, weeklyPerformance, sortSuggestions
metrics.service.spec.ts
planning/
planning.controller.ts // /api/me, /api/week, /api/suggestions, /api/tasks/*, /api/health
planning.service.ts // orquesta ClickupClient + MetricsService + caché
dto/create-task.dto.ts
notifications/ // opcional, fase 2
afternoon-check.job.ts // @Cron con @nestjs/schedule
common/
clickup-exception.filter.ts
dates.ts // weekStart, toLocalDateKey, range de semana en ms
5.3 Responsabilidades
ClickupClient
| Uso | Endpoint de ClickUp |
|---|---|
| Usuario actual | GET /user |
| Espacios de trabajo y miembros | GET /team (cada equipo trae members[].user) |
| Tareas de la semana | GET /team/{team_id}/task?assignees[]={user_id}&subtasks=true&include_closed=true&due_date_gt={inicio-1}&due_date_lt={fin}&page={n} |
| Tareas abiertas | GET /team/{team_id}/task?assignees[]={user_id}&subtasks=true&page={n} |
| Listas de Sprint | GET /folder/{folder_id}/list?archived=false |
| Campos personalizados | GET /list/{list_id}/field |
| Crear tarea | POST /list/{list_id}/task |
| Cambiar fecha, estimado o estado | PUT /task/{task_id} |
- Paginación: 100 tareas por página. Seguir hasta
last_page === trueo hasta recibir menos de 100. Límite de seguridad: 10 páginas. - Las fechas llegan como milisegundos en string: convertirlas a número.
- Al mover o crear una tarea, fijar las 12:00 hora local del día destino, con
due_date_time: false, para evitar saltos de día. ClickUp normaliza las fechas sin hora y puede devolverlas, por ejemplo, a las 04:00 de Lima; el día sigue siendo el correcto. - Guardar en memoria el
user.iddespués de la primera llamada a/user. - Lectura del Custom Field "Clientes" (
custom_fields[], buscando por nombre sin distinguir mayúsculas):drop_down:valuees elorderindexde la opción; buscarla entype_config.options.labels:valuees una lista de ids de opción.- Otros tipos: usar el valor como texto.
- Escritura al crear (formatos verificados contra la API real):
Clientes(drop_down):{ "id": "<field-id>", "value": "<option-uuid>" }.Jefatura(tipousers, no desplegable):{ "id": "<field-id>", "value": { "add": [<user-id>], "rem": [] } }. Una lista simple[<user-id>]devuelve400 Invalid Value.status: "asignado",assignees: [<user-id>…]ytime_estimateen milisegundos.
- Los dos campos tienen el mismo id en todas las listas de Sprint revisadas. Obtener sus ids y opciones desde la lista del primer Sprint abierto y guardarlos en caché (TTL largo).
- Los estados
asignadoyterminadoexisten en las listas de Sprint. Si una lista no tiene alguno, ClickUp rechaza el cambio y el error se devuelve al usuario.
SprintsService
- Lee las listas de la carpeta de Sprints y parsea el nombre con
/^Sprint\s+(\d+)\s*\((\d+)\/(\d+)\s*-\s*(\d+)\/(\d+)\)/. - No usar
start_datenidue_datede la lista: varios Sprints los tienen desactualizados (por ejemplo, de 2025). - El año no viene en el nombre: se elige el año (actual ± 1) que deje la fecha final más cercana a hoy.
sprintOf(date): de los Sprints cuyo rango contiene el día, devuelve el de inicio más tardío.open: fecha final ≥ hoy. Los abiertos se ordenan por número.- Caché con TTL largo (por ejemplo, 1 hora).
MetricsService
- Sin dependencias de HTTP ni de Nest más allá de
@Injectable(). Recibe tareas normalizadas, ajustes, días libres, Sprints y la fecha de "hoy", y devuelveDayDto,WeekDtoySuggestionsDto. - Recibir "hoy" como parámetro (no leer el reloj adentro) para que las pruebas sean deterministas.
- Un día libre produce
metaMs = 0,gapMs = 0ydebtMs = 0, y no cuenta enelapsedDays.
PlanningService
- Pide a ClickUp las tareas de la semana y las abiertas en paralelo.
- Caché con
@nestjs/cache-manager: clavesweek:{teamId}:{weekStart}yopen:{teamId}, TTLCACHE_TTL_SECONDS. - Invalida la caché al crear, mover, estimar o cerrar una tarea, o con
refresh=true. - Valida
CreateTaskDtoantes de llamar a ClickUp: estimado > 0, cliente y jefatura presentes, al menos un asignado, y Sprint abierto.
NotificationsModule (fase 2)
- Cron a la hora
alertTimeen días hábiles no libres: si HPC + por cerrar está por debajo del 50 % de la meta, envía un aviso. Canal por definir: correo, Slack o notificación del sistema.
5.4 Pruebas (Jest)
Obligatorias en metrics.service.spec.ts:
-
Una tarea cerrada sin estimación no suma a HPC y aparece en
noEstimate. -
Una tarea cerrada con fecha de otro día no suma al día evaluado.
-
El rendimiento semanal de un miércoles usa 3 días de meta, no 5.
-
gapMsydebtMsnunca son negativos. -
Una tarea con
due_datea las 23:30 de Lima cae en ese día y no en el siguiente. -
Las sugerencias quedan ordenadas: primero las que encajan, luego las que tienen estimación, luego atrasadas antes que sin fecha, y de mayor a menor estimación.
-
Con fines de semana desactivados, sábado y domingo no cuentan como días transcurridos.
-
Un día libre tiene
metaMs,gapMsydebtMsen 0 y no cuenta como día transcurrido: un miércoles con el lunes libre usa 2 días de meta. -
Un día libre sin tareas no produce división por cero ni
NaN. ParaSprintsService: -
Parsea
"Sprint 19 (10/5 - 10/19)"a número 19 y fechas de inicio y fin. -
El 5 de octubre pertenece al Sprint 19 y no al 18, aunque aparezca como fecha final del 18.
-
Un día de un Sprint cerrado no aparece entre los abiertos, pero
sprintOflo devuelve. -
Los Sprints abiertos salen ordenados por número.
-
Ignora listas cuyo nombre no sigue el patrón.
Para ClickupClient: paginación con un HttpService simulado (3 páginas, la última con last_page: true), mapeo de Clientes (drop_down por orderindex y labels por ids) y que Jefatura se envíe como { add: [id], rem: [] }.
6. Frontend (Angular)
6.1 Configuración
- Angular 18 o superior, con componentes standalone, signals y control de flujo nuevo (
@if,@for). provideHttpClient(withInterceptors([errorInterceptor])).- Configuración regional en español:
registerLocaleData(localeEsPE)y{ provide: LOCALE_ID, useValue: 'es-PE' }. - Proxy de desarrollo:
// frontend/proxy.conf.json
{ "/api": { "target": "http://localhost:3000", "secure": false } }
ng serve --proxy-config proxy.conf.json
6.2 Estructura
frontend/src/app/
app.component.ts
core/
api.service.ts // llamadas a /api/*, tipadas con @contracts
error.interceptor.ts // traduce errores a mensajes para el usuario
state/planner.store.ts // estado con signals
shared/
hours.pipe.ts // ms → "H:MM"
day-label.pipe.ts // "YYYY-MM-DD" → "Jueves 15 de octubre"
hours-input.ts // parsea "2:30", "2.5" o "2" a milisegundos
toast.service.ts
features/
top-bar/
week-strip/ // week-strip + day-card
day-view/ // day-view, hour-ruler, stat-tiles, task-group, task-row, client-chip
task-dialogs/ // new-task, estimate, move
suggestions/ // suggestion-panel, suggestion-row, weekly-score
settings/
styles/_tokens.scss // colores claro/oscuro (sección 9)
6.3 Estado (PlannerStore)
Servicio @Injectable({ providedIn: 'root' }) con:
- Signals:
weekStart,selectedDate,week,suggestions,filter,settings,loading,error,newTaskMeta. - Computed:
selectedDay,selectedSprintylastUpdatedLabel("actualizado hace 2 min"). - Acciones:
loadWeek(refresh?),selectDay(date)(también recarga sugerencias),prevWeek(),nextWeek(),goToday(),moveTask(id, date),setEstimate(id, estimateMs),markDone(id),createTask(dto),loadNewTaskMeta()(carga una vez) ysaveSettings(s). Las acciones sobre tareas reemplazanweekysuggestionscon la respuesta del PATCH/POST y muestran el aviso de confirmación (sección 8). - Actualización automática: con RxJS,
timersegúnautoRefreshMin, combinado confromEvent(document, 'visibilitychange'). Se pausa con la pestaña oculta y recarga al volver si pasó el intervalo. Se cancela contakeUntilDestroyed.
Los componentes no llaman a ApiService directamente: solo leen signals del store y llaman a sus acciones.
6.4 Formato de fechas
DatePipeenes-PEdevuelve el día en minúsculas ("jueves"), así queday-label.pipe.tsdebe capitalizar la primera letra.- Tarjeta semanal: número grande
d, debajo el día (EEEE, capitalizado) y el mes (MMMM). - Encabezado del día:
EEEE d 'de' MMMM 'de' y→ "Jueves 15 de octubre de 2026". - Botones: "Traer al 15".
- Horas siempre como
H:MM(5:30,0:45). Los campos de tiempo aceptan2:30,2.5o2.
7. Funcionalidades de la interfaz
7.1 Barra superior
- Título, usuario conectado, Sprint del día seleccionado (por ejemplo "Sprint 19 (10/5 - 10/19)") y "actualizado hace X min".
- El Sprint sigue al día seleccionado: cambia con las flechas de semana, con Hoy y al elegir otro día. Si el día no cae en ningún Sprint, no se muestra nada.
- Navegación de semanas: anterior, Hoy y siguiente.
- Botones Actualizar (
refresh=true) y Ajustes.
7.2 Franja semanal
- Una tarjeta por día, de lunes a viernes, o los 7 días si se incluyen fines de semana.
- Cada tarjeta muestra el número del día en grande, el nombre del día y el mes.
- El día actual lleva una etiqueta "hoy" y el día seleccionado un borde de acento.
- Mini barra con el verde (completado) y el amarillo (por cerrar) sobre la meta.
- Texto: "
5:30de8:00completadas ·2:00por cerrar". - Días pasados bajo la meta: horas en rojo. Días que cumplieron: en verde.
- Día libre: fondo
--gap-soft, número del día y etiqueta en rojo, y texto "Feriado" o "Vacaciones" en lugar de "X de 8:00". Si hubo trabajo, "2:00trabajadas". Conserva el borde de acento si está seleccionado.
7.3 Vista del día
- Encabezado: número grande y fecha completa. Botón Nueva tarea junto al veredicto.
- Veredicto a la derecha, según el caso:
- Día libre: "sin meta este día", o "
2:00trabajadas como extra". - "Meta cumplida".
- "Cierra X:XX y llegas a la meta" (o "Quedaron X:XX" si el día ya pasó).
- "Faltan X:XX por planificar".
- Día libre: "sin meta este día", o "
- Regla de horas (
HourRuler), el elemento protagonista:- Cada tarea es un bloque proporcional, con su nombre y un tooltip con las horas.
- Orden: completadas (verde sólido), por cerrar (amarillo con borde) y el hueco (rayado rojo con "faltan X:XX").
- Marcas de hora debajo. Si lo planificado supera la meta, una línea punteada marca dónde están las 8 h. En un día libre no se dibuja esa línea y la regla tiene un mínimo de 1 h de ancho para no dividir por cero.
- Tres indicadores: "Ya suman (HPC)", "Sumarán si las cierras" y "Falta planificar".
- Grupos de tareas: "Sumando", "Por cerrar" y "No suman" (solo si hay tareas en el grupo).
- Cada fila: chip del cliente antes del nombre, nombre con enlace a ClickUp, ruta, estado con su color y horas.
- Acciones por fila: Editar (o Poner estimado si no tiene), Mover, y Marcar terminado (solo si el estado no es
terminado).
7.4 Panel de sugerencias
- Rendimiento semanal arriba del panel, en porcentaje con su color y el detalle "X:XX completadas de Y:YY en N días transcurridos". Debajo, una línea separadora.
- Título: "Para completar el 15 de octubre", con el mensaje del hueco. En un día libre: "Este día es feriado. No hay meta, pero puedes adelantar trabajo."
- Filtros: Todas, Atrasadas y Sin fecha, con sus contadores.
- Orden (lo calcula el backend):
- Con estimación y que caben en el hueco (etiqueta "encaja").
- Con estimación, aunque no quepan.
- Sin estimación, con el aviso "no sumará sin estimación".
- Dentro de cada grupo, atrasadas antes que sin fecha, y de mayor a menor estimación.
- Botón "Traer al N": se deshabilita mientras se procesa y la vista se actualiza con la respuesta del PATCH.
- Si el día ya pasó, avisa que mover tareas ahí no recupera lo que no se cerró.
7.5 Ajustes
- Espacio de trabajo (lista desde
/api/me), meta diaria, fines de semana y actualización automática (apagada, 5, 10 o 15 min).- Cancelar cierra el diálogo sin guardar; los campos se rellenan de nuevo al reabrirlo. - Fase 2: aviso de media tarde y su hora.
- La API key no se edita desde la interfaz; se cambia en el
.envdel backend.
7.6 Estados vacíos y errores
- Backend caído: "No se puede conectar con el servidor local. ¿Está corriendo el backend en el puerto 3000?"
- 401 de ClickUp: mensaje del backend con la indicación de revisar el
.env. - Día sin tareas: la regla muestra "Sin tareas con horas este día".
- Sin backlog: "No tienes tareas atrasadas ni sin fecha. Crea tareas en ClickUp y pulsa Actualizar."
- Sin Sprints abiertos: "No hay Sprints abiertos en la carpeta de Sprints." al abrir Nueva tarea.
- Cargando: contenido atenuado, sin parpadeos al actualizar automáticamente (mantener los datos anteriores hasta que lleguen los nuevos).
7.7 Nueva tarea
| Campo | Regla |
|---|---|
| Asunto | Texto obligatorio. |
| Fecha | El día seleccionado, editable. Al cambiarla se actualizan el Sprint y la propuesta de tiempo. |
| Sprint | Sprints abiertos ordenados por número. Por defecto, el que contiene la fecha. Si ninguno la contiene: "Ningún Sprint abierto incluye esta fecha. Elige otro de la lista." |
| Tiempo estimado | H:MM, obligatorio y mayor que 0:00. Se propone el hueco del día, o 1:00 si ya llegó a la meta. Atajos: Hueco X:XX, 0:30, 1:00, 2:00, 4:00. |
| Clientes | Obligatorio. Campo con búsqueda (más de 100 opciones). |
| Jefatura | Obligatorio. Campo con búsqueda entre los miembros del espacio de trabajo. |
| Assignees | Obligatorio, al menos uno. Por defecto, el usuario actual; se pueden agregar y quitar. |
Avisos (no bloquean):
- Día por encima de la meta: "Con esto el día queda en X:XX y pasas de la meta de 8:00. Igual se creará."
- Día libre: "Ese día es feriado y no debería tener tareas. Puedes crearla igual; las horas contarán como extra."
- Bajo el tiempo: "Con esto llegas a la meta" o "Con esto llevas X:XX de 8:00; faltarían Y:YY".
7.8 Editar estimado, mover y marcar terminado
- Editar estimado: campo
H:MMobligatorio y mayor que 0:00. Muestra cómo queda el día con el nuevo valor (reemplaza el actual) y avisa si pasa de la meta. Escribetime_estimate. - Mover: campo de fecha, por defecto el día siguiente al actual de la tarea. Si el destino es un día libre, avisa pero permite mover. Si es de otra semana, la tarea sale de la vista actual.
- Marcar terminado: cambia el estado a
terminado; la tarea pasa a "Sumando" si tiene estimado. - Ninguna acción tiene "deshacer".
8. Textos
- Español, en minúsculas tipo oración, sin mayúsculas sostenidas en etiquetas.
- Botones con verbos claros: "Actualizar", "Traer al 15", "Guardar ajustes", "Nueva tarea", "Marcar terminado", "Mover", "Guardar estimado".
- Los avisos de confirmación usan el mismo verbo que el botón: "Movida al jueves 15 de octubre", "Creada el jueves 15 de octubre", "Estimado guardado: 2:00", "Marcada como terminado: {nombre}".
9. Diseño visual
| Token | Claro | Oscuro | Uso |
|---|---|---|---|
--bg |
#E9EDF2 |
#10151F |
Página |
--surface |
#FFFFFF |
#18202D |
Paneles y tarjetas |
--ink |
#18223A |
#E6EBF3 |
Texto principal |
--muted |
#5F6B80 |
#93A0B5 |
Texto secundario |
--done |
#0E8A68 |
#2FBE91 |
HPC, meta cumplida |
--pend |
#D99A16 |
#F0B43A |
Planificado pendiente |
--gap |
#C9472F |
#F0705A |
Falta planificar, atrasos, día libre |
--gap-soft |
#F7DDD7 |
#3D1F1A |
Fondo de la tarjeta de un día libre y avisos |
--accent |
#2F49B8 |
#8A9EFF |
Selección, botones principales, chip de cliente |
- Tipografía: Schibsted Grotesk (Google Fonts) con
font-variant-numeric: tabular-numsy fallback asystem-ui. - La regla de horas es lo protagonista. El resto, sobrio.
- El chip de cliente usa el color de la opción en ClickUp, con texto blanco, o
--accentsobre--accent-softsi la opción no tiene color. - Responsive: bajo 960 px, el panel de sugerencias baja debajo del día. Bajo 700 px, la franja semanal tiene desplazamiento horizontal.
- Respetar
prefers-color-schemeyprefers-reduced-motion, con foco visible con teclado.
10. Plan de desarrollo
contracts/con los tipos de la sección 4.1.- Backend:
MetricsServicecon todas sus pruebas, con datos falsos. Es la parte crítica. Incluir días libres. - Backend:
SprintsServiceyDaysOffServicecon sus pruebas. - Backend:
ClickupClient, el mapper (conclients) y su prueba de paginación. - Backend:
PlanningController, la caché ySettingsModule. Probar con curl contra ClickUp real, incluida una tarea de prueba que se borre después. - Frontend:
ApiService,PlannerStorey los pipes. - Frontend: franja semanal y vista del día, con Sprint, día libre y chip de cliente.
- Frontend: panel de sugerencias y ajustes.
- Frontend: diálogos de nueva tarea, estimado y mover; marcar terminado.
- Actualización automática y estados de error.
- Fase 2: notificaciones de media tarde.
11. Criterios de aceptación
- Al crear en ClickUp una tarea asignada a mí, con fecha y estimación, y pulsar Actualizar, aparece en el día correcto en "Por cerrar".
- Al cerrarla en ClickUp y actualizar, pasa a "Sumando" y la regla se pinta de verde.
- Una tarea sin estimación aparece en "No suman" con su aviso, y con Poner estimado pasa a contar.
- Una tarea vencida o sin fecha aparece en sugerencias, y "Traer al N" cambia su fecha en ClickUp.
- Las tarjetas muestran número del día, nombre del día y mes.
- El rendimiento semanal no penaliza días futuros ni días libres.
- La API key no aparece en el código de Angular ni en ninguna respuesta de
/api/*. - La actualización automática funciona y se pausa con la pestaña oculta.
MetricsServicetiene todas las pruebas de la sección 5.4 en verde.- Cada tarea muestra el chip del cliente antes del nombre.
- La barra superior muestra el Sprint del día seleccionado y cambia al navegar. El 5 de octubre muestra el Sprint 19, no el 18.
- Un día de
days-offse ve con fondo rojo, meta 0 y la etiqueta correspondiente, y no baja el rendimiento semanal. - Nueva tarea exige asunto, Sprint, estimado > 0, Clientes, Jefatura y asignado; crea la tarea con estado
asignado, que aparece en "Por cerrar" y pasa a "Sumando" al marcarla como terminada; avisa si el día es libre o se pasa de la meta, y la crea igual. - Nueva tarea propone el Sprint del día y el hueco del día como estimado.
- Editar estimado, Mover y Marcar terminado cambian la tarea en ClickUp y actualizan la vista sin recargarla.
- El rendimiento semanal aparece en la parte superior del panel de sugerencias.
12. Ideas para después
- Vista de mes con un mapa de calor de horas completadas.
- Arrastrar tareas desde sugerencias a un día de la franja semanal (Angular CDK Drag and Drop).
- Comparar el tiempo registrado (time tracking) contra el estimado.
- Historial semanal guardado en SQLite para ver la tendencia del rendimiento.
- Horas por cliente de la semana, con una barra simple.
- Aviso de tareas cerradas sin estimación en la semana, con acceso directo a "Poner estimado".
- Rendimiento por Sprint: horas completadas ÷ meta de los días hábiles transcurridos del Sprint, descontando días libres.
- Acciones de estimar, mover y marcar terminado también en el panel de sugerencias.
- Abrir en la última semana vista y no siempre en la actual.
- Duplicar una tarea frecuente, como las reuniones, al crear una nueva.
- Calendarios de feriados por país, para equipos mixtos.
13. Prototipo HTML (respaldo)
Carpeta prototipo-html/. Funciona sin backend ni Angular: se abre mis-8-horas.html con doble clic. Es el respaldo si no se puede levantar NestJS o Angular, y la referencia de comportamiento para el proyecto nuevo.
| Archivo | Contenido |
|---|---|
mis-8-horas.html |
Toda la aplicación: estilos, lógica, llamadas a ClickUp y vistas. |
config.js |
window.M8_CONFIG = { apiKey: "pk_…" }. Opcionales: clientField (por defecto "Clientes") y sprintsFolderId. Contiene la API key: no compartir ni subir a git. |
config.example.js |
Plantilla de config.js sin la key. |
days-off.js |
window.M8_DAYS_OFF = [{ "date": "YYYY-MM-DD", "label": "Feriado" }]. Lista de días libres que el usuario edita a mano. Hoy trae los 16 feriados nacionales de Chile 2026. |
Por qué los archivos son .js y no .json: una página abierta con doble clic no puede leer un .json con fetch (el navegador lo bloquea), pero sí un .js con <script>.
Lo que incluye el prototipo, y que el proyecto nuevo debe igualar:
- Todas las reglas de la sección 2, con la zona horaria del navegador (el proyecto nuevo debe usar
America/Limacon Luxon). - Chip del campo "Clientes" antes del nombre de cada tarea.
- Sprint del día seleccionado en la barra superior, calculado desde los nombres de las listas de la carpeta de Sprints.
- Días libres desde
days-off.js, con tarjeta roja y meta 0. - Nueva tarea, Editar estimado, Mover y Marcar terminado.
- Rendimiento semanal arriba del panel de sugerencias.
- Ajustes con Cancelar y una casilla para usar la key de
config.jso una escrita a mano.
Limitaciones conocidas:
- La API key queda en texto plano en
config.js, y las llamadas a ClickUp salen desde el navegador. - El rendimiento semanal suma las horas cerradas en días libres como extra (ver "Pendiente de confirmar").
- No hay actualización automática, ni caché, ni pruebas automáticas.
- Si el navegador bloquea la conexión al abrir con doble clic, servir la carpeta con
python3 -m http.server.