Highlight task planner items near reset
All checks were successful
Deploy Sokko G / deploy (push) Successful in 14s

This commit is contained in:
Shinuwa 2026-08-20 16:42:41 +02:00
parent 49ba0c4cb2
commit e4cbe3e087
5 changed files with 80 additions and 2 deletions

View file

@ -3,7 +3,7 @@ import { readFile } from "node:fs/promises";
import { test } from "node:test"; import { test } from "node:test";
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import { SOCKET_SHAPES, formatValue, getSocketItemName, setItemCategory } from "../website/src/features/toolboxes/modules/equipmentPlannerOptions.js"; 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 { 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 { 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"; 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) lastResetAt: Date.UTC(2026, 0, 1, 5)
}, Date.UTC(2026, 0, 2, 8)); }, Date.UTC(2026, 0, 2, 8));
assert.equal(resetData.tasks.find((task) => task.id === "a").checked, false); 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", () => { test("equipment planner options expose stable formatting and socket helpers", () => {

View file

@ -15,6 +15,7 @@ import {
getTaskTitle, getTaskTitle,
getTaskTypeLabel, getTaskTypeLabel,
getTasksForParent, getTasksForParent,
isTaskResetSoon,
isTaskTreeComplete isTaskTreeComplete
} from "./taskPlannerUtils.js"; } from "./taskPlannerUtils.js";
@ -23,6 +24,7 @@ export function TaskPlannerBranch({
parentMap, parentMap,
tasks, tasks,
data, data,
nowMs,
reorder, reorder,
textContent, textContent,
openDescriptions, openDescriptions,
@ -60,6 +62,7 @@ export function TaskPlannerBranch({
parentMap={parentMap} parentMap={parentMap}
tasks={tasks} tasks={tasks}
data={data} data={data}
nowMs={nowMs}
reorder={reorder} reorder={reorder}
textContent={textContent} textContent={textContent}
openDescriptions={openDescriptions} openDescriptions={openDescriptions}
@ -106,6 +109,7 @@ export function TaskPlannerBranch({
task={task} task={task}
tasks={tasks} tasks={tasks}
data={data} data={data}
nowMs={nowMs}
parentMap={parentMap} parentMap={parentMap}
parentId={parentId} parentId={parentId}
reorder={reorder} reorder={reorder}
@ -208,6 +212,7 @@ function TaskPlannerItem({
task, task,
tasks, tasks,
data, data,
nowMs,
parentMap, parentMap,
parentId, parentId,
reorder, reorder,
@ -237,6 +242,7 @@ function TaskPlannerItem({
const className = [ const className = [
"task-planner-item", "task-planner-item",
task.checked ? "is-complete" : "", task.checked ? "is-complete" : "",
isTaskResetSoon(task, data, nowMs) ? "is-reset-soon" : "",
missingPrerequisites.length ? "has-missing-prerequisite" : "", missingPrerequisites.length ? "has-missing-prerequisite" : "",
reorder.isItemDragging(task.id) ? "is-dragging" : "", reorder.isItemDragging(task.id) ? "is-dragging" : "",
reorder.isItemDropTarget(task.id) ? "is-drop-target" : "", reorder.isItemDropTarget(task.id) ? "is-drop-target" : "",
@ -519,4 +525,3 @@ function LinksEditor({ label, relations, task, tasks, parentMap, relationTargets
</div> </div>
); );
} }

View file

@ -354,6 +354,7 @@ export function TaskPlannerModule({ toolboxId, moduleId, context, editing }) {
parentMap={parentMap} parentMap={parentMap}
tasks={data.tasks} tasks={data.tasks}
data={data} data={data}
nowMs={nowMs}
reorder={reorder} reorder={reorder}
textContent={textContent} textContent={textContent}
openDescriptions={openDescriptions} openDescriptions={openDescriptions}

View file

@ -1,5 +1,8 @@
// Rôle : regroupe les constantes et helpers purs de l'outil task planner. // Rôle : regroupe les constantes et helpers purs de l'outil task planner.
export const TASK_TYPES = ["daily", "weekly", "unique"]; 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 = [ export const WEEK_DAYS = [
{ value: 1, label: "Lundi" }, { value: 1, label: "Lundi" },
{ value: 2, label: "Mardi" }, { value: 2, label: "Mardi" },
@ -42,6 +45,39 @@ export function getLatestWeeklyReset(now, resetTime, weekDay) {
return reset.getTime(); 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()) { export function applyDueResets(data, now = Date.now()) {
let latestResetAt = data.lastResetAt; let latestResetAt = data.lastResetAt;
let changed = false; let changed = false;

View file

@ -360,6 +360,37 @@
rgba(5, 7, 17, 0.36); 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 { .task-planner-line {
display: grid; display: grid;
grid-template-columns: 30px 24px minmax(0, 1fr) 134px auto auto; grid-template-columns: 30px 24px minmax(0, 1fr) 134px auto auto;