From e4cbe3e0873c0f62eb9285ef5ed44cef48e34fc2 Mon Sep 17 00:00:00 2001 From: Shinuwa Date: Thu, 20 Aug 2026 16:42:41 +0200 Subject: [PATCH] Highlight task planner items near reset --- tests/static-toolboxes.test.mjs | 7 +++- .../toolboxes/modules/TaskPlannerItems.jsx | 7 +++- .../toolboxes/modules/TaskPlannerModule.jsx | 1 + .../toolboxes/modules/taskPlannerUtils.js | 36 +++++++++++++++++++ .../src/styles/toolboxes/_task-planner.scss | 31 ++++++++++++++++ 5 files changed, 80 insertions(+), 2 deletions(-) diff --git a/tests/static-toolboxes.test.mjs b/tests/static-toolboxes.test.mjs index d9f384e..76f5d9d 100644 --- a/tests/static-toolboxes.test.mjs +++ b/tests/static-toolboxes.test.mjs @@ -3,7 +3,7 @@ import { readFile } from "node:fs/promises"; import { test } from "node:test"; import assert from "node:assert/strict"; import { SOCKET_SHAPES, formatValue, getSocketItemName, setItemCategory } from "../website/src/features/toolboxes/modules/equipmentPlannerOptions.js"; -import { applyDueResets, getTaskParentMap, getTaskTreeIds, moveTask } from "../website/src/features/toolboxes/modules/taskPlannerUtils.js"; +import { applyDueResets, getTaskParentMap, getTaskTreeIds, isTaskResetSoon, moveTask } from "../website/src/features/toolboxes/modules/taskPlannerUtils.js"; import { createInput, getInputLabel, normalizeCustomKey, parseHoldSeconds } from "../website/src/features/toolboxes/modules/combosUtils.js"; import { getBoundsFromStartAndSize, getSelectionBounds, moveSelectionCells, parseTsvBlock, pasteCellBlock } from "../website/src/features/toolboxes/modules/tableUtils.js"; import { canToggleAutoRefresh, getCountdownValidationError, isAlertGuarded } from "../website/src/features/toolboxes/modules/timerOptions.js"; @@ -471,6 +471,11 @@ test("task planner helpers keep hierarchy, reorder and reset behavior stable", ( lastResetAt: Date.UTC(2026, 0, 1, 5) }, Date.UTC(2026, 0, 2, 8)); assert.equal(resetData.tasks.find((task) => task.id === "a").checked, false); + assert.equal(isTaskResetSoon({ id: "d", title: "D", type: "daily", checked: false }, resetData, new Date(2026, 0, 2, 0, 1).getTime()), true); + assert.equal(isTaskResetSoon({ id: "e", title: "E", type: "daily", checked: false }, resetData, new Date(2026, 0, 1, 20, 0).getTime()), false); + assert.equal(isTaskResetSoon({ id: "f", title: "F", type: "weekly", checked: false, weeklyResetDay: 4 }, resetData, new Date(2026, 0, 6, 7, 0).getTime()), true); + assert.equal(isTaskResetSoon({ id: "g", title: "G", type: "weekly", checked: true, weeklyResetDay: 4 }, resetData, new Date(2026, 0, 6, 7, 0).getTime()), false); + assert.equal(isTaskResetSoon({ id: "h", title: "H", type: "unique", checked: false }, resetData, new Date(2026, 0, 6, 7, 0).getTime()), false); }); test("equipment planner options expose stable formatting and socket helpers", () => { diff --git a/website/src/features/toolboxes/modules/TaskPlannerItems.jsx b/website/src/features/toolboxes/modules/TaskPlannerItems.jsx index fff3b41..912c5c9 100644 --- a/website/src/features/toolboxes/modules/TaskPlannerItems.jsx +++ b/website/src/features/toolboxes/modules/TaskPlannerItems.jsx @@ -15,6 +15,7 @@ import { getTaskTitle, getTaskTypeLabel, getTasksForParent, + isTaskResetSoon, isTaskTreeComplete } from "./taskPlannerUtils.js"; @@ -23,6 +24,7 @@ export function TaskPlannerBranch({ parentMap, tasks, data, + nowMs, reorder, textContent, openDescriptions, @@ -60,6 +62,7 @@ export function TaskPlannerBranch({ parentMap={parentMap} tasks={tasks} data={data} + nowMs={nowMs} reorder={reorder} textContent={textContent} openDescriptions={openDescriptions} @@ -106,6 +109,7 @@ export function TaskPlannerBranch({ task={task} tasks={tasks} data={data} + nowMs={nowMs} parentMap={parentMap} parentId={parentId} reorder={reorder} @@ -208,6 +212,7 @@ function TaskPlannerItem({ task, tasks, data, + nowMs, parentMap, parentId, reorder, @@ -237,6 +242,7 @@ function TaskPlannerItem({ const className = [ "task-planner-item", task.checked ? "is-complete" : "", + isTaskResetSoon(task, data, nowMs) ? "is-reset-soon" : "", missingPrerequisites.length ? "has-missing-prerequisite" : "", reorder.isItemDragging(task.id) ? "is-dragging" : "", reorder.isItemDropTarget(task.id) ? "is-drop-target" : "", @@ -519,4 +525,3 @@ function LinksEditor({ label, relations, task, tasks, parentMap, relationTargets ); } - diff --git a/website/src/features/toolboxes/modules/TaskPlannerModule.jsx b/website/src/features/toolboxes/modules/TaskPlannerModule.jsx index c9e9510..bb79521 100644 --- a/website/src/features/toolboxes/modules/TaskPlannerModule.jsx +++ b/website/src/features/toolboxes/modules/TaskPlannerModule.jsx @@ -354,6 +354,7 @@ export function TaskPlannerModule({ toolboxId, moduleId, context, editing }) { parentMap={parentMap} tasks={data.tasks} data={data} + nowMs={nowMs} reorder={reorder} textContent={textContent} openDescriptions={openDescriptions} diff --git a/website/src/features/toolboxes/modules/taskPlannerUtils.js b/website/src/features/toolboxes/modules/taskPlannerUtils.js index 4998287..821a907 100644 --- a/website/src/features/toolboxes/modules/taskPlannerUtils.js +++ b/website/src/features/toolboxes/modules/taskPlannerUtils.js @@ -1,5 +1,8 @@ // RĂ´le : regroupe les constantes et helpers purs de l'outil task planner. export const TASK_TYPES = ["daily", "weekly", "unique"]; +const DAILY_RESET_SOON_MS = 6 * 60 * 60 * 1000; +const WEEKLY_RESET_SOON_MS = 48 * 60 * 60 * 1000; + export const WEEK_DAYS = [ { value: 1, label: "Lundi" }, { value: 2, label: "Mardi" }, @@ -42,6 +45,39 @@ export function getLatestWeeklyReset(now, resetTime, weekDay) { return reset.getTime(); } +export function getNextDailyReset(now, resetTime) { + const { hours, minutes } = parseResetTime(resetTime); + const reset = new Date(now); + reset.setHours(hours, minutes, 0, 0); + if (reset.getTime() <= now) reset.setDate(reset.getDate() + 1); + return reset.getTime(); +} + +export function getNextWeeklyReset(now, resetTime, weekDay) { + const { hours, minutes } = parseResetTime(resetTime); + const reset = new Date(now); + reset.setHours(hours, minutes, 0, 0); + const dayDiff = (weekDay - reset.getDay() + 7) % 7; + reset.setDate(reset.getDate() + dayDiff); + if (reset.getTime() <= now) reset.setDate(reset.getDate() + 7); + return reset.getTime(); +} + +export function getTaskResetTimeRemainingMs(task, data, now = Date.now()) { + if (task.checked || task.type === "unique") return null; + const nextResetAt = task.type === "daily" + ? getNextDailyReset(now, task.dailyResetTime || data.resetTime) + : getNextWeeklyReset(now, data.resetTime, task.weeklyResetDay ?? data.weeklyResetDay); + return Math.max(0, nextResetAt - now); +} + +export function isTaskResetSoon(task, data, now = Date.now()) { + const remainingMs = getTaskResetTimeRemainingMs(task, data, now); + if (remainingMs == null) return false; + const threshold = task.type === "daily" ? DAILY_RESET_SOON_MS : WEEKLY_RESET_SOON_MS; + return remainingMs <= threshold; +} + export function applyDueResets(data, now = Date.now()) { let latestResetAt = data.lastResetAt; let changed = false; diff --git a/website/src/styles/toolboxes/_task-planner.scss b/website/src/styles/toolboxes/_task-planner.scss index 9e65a9f..5daa99c 100644 --- a/website/src/styles/toolboxes/_task-planner.scss +++ b/website/src/styles/toolboxes/_task-planner.scss @@ -360,6 +360,37 @@ rgba(5, 7, 17, 0.36); } +.task-planner-item.is-reset-soon:not(.is-complete) { + border-color: rgba(34, 211, 238, 0.34); + box-shadow: + inset 0 0 0 1px rgba(34, 211, 238, 0.07), + 0 0 0 rgba(34, 211, 238, 0); + animation: task-planner-reset-soon-pulse 2600ms var(--ease-standard) infinite; +} + +@keyframes task-planner-reset-soon-pulse { + 0%, + 100% { + border-color: rgba(34, 211, 238, 0.26); + box-shadow: + inset 0 0 0 1px rgba(34, 211, 238, 0.05), + 0 0 0 rgba(34, 211, 238, 0); + } + + 50% { + border-color: rgba(34, 211, 238, 0.44); + box-shadow: + inset 0 0 0 1px rgba(34, 211, 238, 0.1), + 0 0 12px rgba(34, 211, 238, 0.07); + } +} + +@media (prefers-reduced-motion: reduce) { + .task-planner-item.is-reset-soon:not(.is-complete) { + animation: none; + } +} + .task-planner-line { display: grid; grid-template-columns: 30px 24px minmax(0, 1fr) 134px auto auto;