diff --git a/docs/STORAGE_SCHEMA.md b/docs/STORAGE_SCHEMA.md index b03f5f0..6d21d9e 100644 --- a/docs/STORAGE_SCHEMA.md +++ b/docs/STORAGE_SCHEMA.md @@ -326,6 +326,7 @@ Type : `taskPlanner` "description": "Préparer la route.", "category": "Raid", "type": "daily", + "dailyResetTime": "06:30", "checked": true, "checkedAt": 1760000000000 }, @@ -358,6 +359,7 @@ Notes : - `weeklyResetDay` suit les jours JavaScript : `0` dimanche, `1` lundi, ..., `6` samedi. - `resetTime` est global à l'outil et vaut `HH:MM`. - `hideCompleted` masque les tâches effectuées à l'affichage, ainsi que les catégories dont toutes les tâches sont effectuées. +- `dailyResetTime` sur une tâche quotidienne surcharge l'heure globale. - `weeklyResetDay` sur une tâche hebdo surcharge le jour global. - `category` sur une tâche racine est facultatif. Les tâches enfants héritent de la catégorie effective de leur parent et ne stockent pas de catégorie propre. - Les tâches catégorisées sont regroupées par catégorie au niveau racine. @@ -380,6 +382,7 @@ Stockage compact : - `categoryOrder` est omis si aucune catégorie n'est utilisée. - `collapsedCategories` est omis si aucune catégorie n'est repliée. - `checked` et `checkedAt` sont omis si la tâche n'est pas effectuée. +- `dailyResetTime` unitaire est omis si non défini ou identique à l'heure globale. - `weeklyResetDay` unitaire est omis si non défini. - `relations` est omis si vide. - `relations[].prerequisite` est omis si `false`. diff --git a/tests/helpers/data-validation.mjs b/tests/helpers/data-validation.mjs index 03053ad..76d7e30 100644 --- a/tests/helpers/data-validation.mjs +++ b/tests/helpers/data-validation.mjs @@ -318,6 +318,10 @@ export function validateSiteContent(site) { "toolboxes.modules.taskPlanner.uncategorizedDropTitle", "toolboxes.modules.taskPlanner.showCategoryTitle", "toolboxes.modules.taskPlanner.hideCategoryTitle", + "toolboxes.modules.taskPlanner.taskDailyResetTimeLabel", + "toolboxes.modules.taskPlanner.inheritResetTime", + "toolboxes.modules.taskPlanner.taskResetLabel", + "toolboxes.modules.taskPlanner.noTaskResetLabel", "toolboxes.modules.taskPlanner.taskWeeklyResetDayLabel", "toolboxes.modules.taskPlanner.inheritWeeklyResetDay", "toolboxes.modules.taskPlanner.linksLabel", diff --git a/tests/toolbox-modules.test.mjs b/tests/toolbox-modules.test.mjs index ece4441..ecad531 100644 --- a/tests/toolbox-modules.test.mjs +++ b/tests/toolbox-modules.test.mjs @@ -406,9 +406,9 @@ test("task planner data normalizes invalid settings and relations", () => { categoryOrder: ["Raid", "Missing", "Farm", "Raid"], collapsedCategories: ["Raid", "", "Raid"], tasks: [ - { id: "task1", title: "Daily", type: "daily", checked: true, checkedAt: 1000, category: "Farm" }, + { id: "task1", title: "Daily", type: "daily", checked: true, checkedAt: 1000, category: "Farm", dailyResetTime: "06:30" }, { id: "task2", title: "Weekly", type: "weekly", weeklyResetDay: 4, category: "Raid" }, - { id: "task3", title: "Invalid type", type: "later", category: "Farm" } + { id: "task3", title: "Invalid type", type: "later", category: "Farm", dailyResetTime: "25:00" } ], relations: [ { id: "link1", fromTaskId: "task1", toTaskId: "task2" }, @@ -424,9 +424,11 @@ test("task planner data normalizes invalid settings and relations", () => { assert.deepEqual(normalized.categoryOrder, ["Raid"]); assert.deepEqual(normalized.collapsedCategories, ["Raid"]); assert.equal(normalized.tasks[0].category, undefined); + assert.equal(normalized.tasks[0].dailyResetTime, "06:30"); assert.equal(normalized.tasks[1].weeklyResetDay, 4); assert.equal(normalized.tasks[1].category, "Raid"); assert.equal(normalized.tasks[2].type, "unique"); + assert.equal(normalized.tasks[2].dailyResetTime, undefined); assert.deepEqual(normalized.relations.map((relation) => relation.id), ["link1", "dep1"]); assert.equal(normalized.relations[0].prerequisite, false); assert.equal(normalized.relations[1].prerequisite, true); @@ -439,7 +441,7 @@ test("task planner storage compacts defaults and export remaps task relations", hideCompleted: true, collapsedCategories: ["Raid"], tasks: [ - { id: "task1", title: "Daily", description: "", type: "daily", checked: false }, + { id: "task1", title: "Daily", description: "", type: "daily", checked: false, dailyResetTime: "06:30" }, { id: "task2", title: "Weekly", description: "Run", type: "weekly", checked: true, checkedAt: 2000, weeklyResetDay: 5, category: "Raid" } ], categoryOrder: ["Raid"], @@ -453,7 +455,7 @@ test("task planner storage compacts defaults and export remaps task relations", assert.equal(compact.hideCompleted, true); assert.deepEqual(compact.collapsedCategories, ["Raid"]); assert.deepEqual(compact.categoryOrder, ["Raid"]); - assert.deepEqual(compact.tasks[0], { id: "task1", title: "Daily", type: "daily" }); + assert.deepEqual(compact.tasks[0], { id: "task1", title: "Daily", type: "daily", dailyResetTime: "06:30" }); assert.equal(compact.tasks[1].category, "Raid"); assert.equal(compact.tasks[1].weeklyResetDay, 5); diff --git a/website/public/data/site.json b/website/public/data/site.json index 4752927..5cefe53 100644 --- a/website/public/data/site.json +++ b/website/public/data/site.json @@ -485,6 +485,10 @@ "uncategorizedDropTitle": "Sans catégorie", "showCategoryTitle": "Afficher cette catégorie", "hideCategoryTitle": "Réduire cette catégorie", + "taskDailyResetTimeLabel": "Heure quotidienne", + "inheritResetTime": "Utiliser l'heure globale", + "taskResetLabel": "Réinitialisation", + "noTaskResetLabel": "Aucun reset", "taskWeeklyResetDayLabel": "Jour hebdo", "inheritWeeklyResetDay": "Global", "linksLabel": "Parent", diff --git a/website/src/features/toolboxes/modules/TaskPlannerModule.jsx b/website/src/features/toolboxes/modules/TaskPlannerModule.jsx index 67381a2..d6a7707 100644 --- a/website/src/features/toolboxes/modules/TaskPlannerModule.jsx +++ b/website/src/features/toolboxes/modules/TaskPlannerModule.jsx @@ -55,7 +55,7 @@ function applyDueResets(data, now = Date.now()) { const tasks = data.tasks.map((task) => { if (task.type === "unique") return task; const resetAt = task.type === "daily" - ? getLatestDailyReset(now, data.resetTime) + ? getLatestDailyReset(now, task.dailyResetTime || data.resetTime) : getLatestWeeklyReset(now, data.resetTime, task.weeklyResetDay ?? data.weeklyResetDay); latestResetAt = Math.max(latestResetAt, resetAt); if (!task.checked || resetAt <= (task.checkedAt || data.lastResetAt)) return task; @@ -845,6 +845,7 @@ function TaskPlannerItem({ const nextType = event.target.value; const nextTask = { ...current, type: nextType }; if (nextType !== "weekly") delete nextTask.weeklyResetDay; + if (nextType !== "daily") delete nextTask.dailyResetTime; return nextTask; })} aria-label={textContent.typeLabel || "Type"} @@ -909,41 +910,22 @@ function TaskPlannerItem({ )} {settingsOpen && (
- {task.type === "weekly" && ( - - )} - + +
)} {children && } @@ -951,6 +933,73 @@ function TaskPlannerItem({ ); } +function TaskResetSettings({ task, data, textContent, onUpdateTask }) { + if (task.type === "daily") { + const hasOverride = Boolean(task.dailyResetTime); + return ( +
+ {textContent.taskDailyResetTimeLabel || "Heure quotidienne"} +
+ onUpdateTask(task.id, (current) => { + const nextTask = { ...current }; + if (event.target.value && event.target.value !== data.resetTime) nextTask.dailyResetTime = event.target.value; + else delete nextTask.dailyResetTime; + return nextTask; + })} + aria-label={`${textContent.taskDailyResetTimeLabel || "Heure quotidienne"} ${task.title}`} + /> + +
+
+ ); + } + + if (task.type === "weekly") { + return ( + + ); + } + + return ( +
+ {textContent.taskResetLabel || "Réinitialisation"} +

{textContent.noTaskResetLabel || "Aucun reset"}

+
+ ); +} + function LinksEditor({ label, relations, task, tasks, parentMap, relationTargets, emptyLabel, addLabel, removeLabel, prerequisiteLabel, onAddLinkTarget, onRemoveLinkTarget, onSetPrerequisite }) { const descendantIds = new Set(); function collectDescendants(taskId) { @@ -965,6 +1014,17 @@ function LinksEditor({ label, relations, task, tasks, parentMap, relationTargets return (
{label} +
{relations.length ? relations.map((relation) => (
@@ -984,17 +1044,6 @@ function LinksEditor({ label, relations, task, tasks, parentMap, relationTargets
)) : {emptyLabel}}
-
); } diff --git a/website/src/features/toolboxes/storage/toolboxStorage.js b/website/src/features/toolboxes/storage/toolboxStorage.js index a96a400..ae73787 100644 --- a/website/src/features/toolboxes/storage/toolboxStorage.js +++ b/website/src/features/toolboxes/storage/toolboxStorage.js @@ -610,6 +610,7 @@ function normalizeTaskPlannerTask(task) { checkedAt: normalizeTimestamp(task?.checkedAt) }; if (category) normalized.category = category; + if (type === "daily" && isClockTimeString(task?.dailyResetTime)) normalized.dailyResetTime = task.dailyResetTime; if (type === "weekly" && task?.weeklyResetDay !== undefined) normalized.weeklyResetDay = normalizeWeekDay(task.weeklyResetDay); if (!normalized.checked) normalized.checkedAt = 0; return normalized; @@ -869,6 +870,7 @@ export function compactModuleDataForStorage(type, value) { if (task.category) compactTask.category = task.category; if (task.checked) compactTask.checked = true; if (task.checked && task.checkedAt) compactTask.checkedAt = task.checkedAt; + if (task.type === "daily" && task.dailyResetTime && task.dailyResetTime !== normalized.resetTime) compactTask.dailyResetTime = task.dailyResetTime; if (task.type === "weekly" && task.weeklyResetDay !== undefined) compactTask.weeklyResetDay = task.weeklyResetDay; return compactTask; }); diff --git a/website/src/styles/_toolboxes.scss b/website/src/styles/_toolboxes.scss index 97ef861..d576b60 100644 --- a/website/src/styles/_toolboxes.scss +++ b/website/src/styles/_toolboxes.scss @@ -1948,14 +1948,14 @@ textarea:focus { } .task-planner-settings label, -.task-planner-weekly-override { +.task-planner-reset-override { display: grid; gap: 6px; min-width: 0; } .task-planner-settings span, -.task-planner-weekly-override span, +.task-planner-reset-override > span, .task-planner-relations > span { color: var(--color-text-secondary); font-size: var(--font-size-xs); @@ -1967,9 +1967,11 @@ textarea:focus { .task-planner-settings input, .task-planner-add-form select, .task-planner-type-select, -.task-planner-weekly-override select, +.task-planner-reset-override select, +.task-planner-reset-override input, .task-planner-relations select { width: 100%; + min-height: 36px; border: 1px solid var(--color-border); border-radius: var(--radius-md); background-color: rgba(15, 23, 42, 0.64); @@ -1979,7 +1981,7 @@ textarea:focus { .task-planner-settings select option, .task-planner-add-form select option, .task-planner-type-select option, -.task-planner-weekly-override select option, +.task-planner-reset-override select option, .task-planner-relations select option { background: #11182f; color: var(--color-text-primary); @@ -2349,8 +2351,8 @@ textarea:focus { .task-planner-task-settings { display: grid; - grid-template-columns: minmax(130px, 0.32fr) minmax(0, 1fr); - gap: 10px; + grid-template-columns: minmax(0, 1fr) minmax(0, 2fr); + gap: 12px; align-items: start; padding: 10px; border: 1px solid rgba(165, 180, 252, 0.1); @@ -2358,6 +2360,50 @@ textarea:focus { background: rgba(7, 10, 24, 0.24); } +.task-planner-reset-control { + display: grid; + grid-template-columns: minmax(0, 1fr) 34px; + gap: 8px; + align-items: center; +} + +.task-planner-reset-control button { + display: inline-grid; + width: 34px; + min-width: 34px; + min-height: 36px; + place-items: center; + padding: 0; + border: 1px solid rgba(165, 180, 252, 0.12); + border-radius: var(--radius-md); + background: rgba(21, 26, 48, 0.58); + color: var(--color-text-secondary); +} + +.task-planner-reset-control button:disabled { + opacity: 0.42; + cursor: not-allowed; +} + +.task-planner-reset-control button .ui-icon { + width: 14px; + height: 14px; +} + +.task-planner-reset-override.is-empty p { + display: grid; + min-height: 36px; + align-items: center; + margin: 0; + padding: 0 12px; + border: 1px solid rgba(165, 180, 252, 0.08); + border-radius: var(--radius-md); + background: rgba(15, 23, 42, 0.34); + color: var(--color-text-muted); + font-size: var(--font-size-sm); + font-weight: 800; +} + .task-planner-relations { display: grid; grid-column: span 1; @@ -2367,8 +2413,8 @@ textarea:focus { .task-planner-relations > div { display: grid; - gap: 5px; - min-height: 28px; + gap: 6px; + min-height: 36px; } .task-planner-relations em { @@ -2434,10 +2480,16 @@ textarea:focus { } .task-planner-relations select { - min-height: 34px; + min-height: 36px; padding-inline: 10px; } +@media (max-width: 760px) { + .task-planner-task-settings { + grid-template-columns: 1fr; + } +} + .combos-module { display: grid; gap: 12px; @@ -3497,7 +3549,7 @@ button.combo-input-token.combo-value-grab .ui-icon { .task-planner-settings select, .task-planner-add-form select, .task-planner-type-select, -.task-planner-weekly-override select, +.task-planner-reset-override select, .task-planner-relations select { appearance: none; -webkit-appearance: none; @@ -3527,8 +3579,8 @@ button.combo-input-token.combo-value-grab .ui-icon { .task-planner-add-form select:focus, .task-planner-type-select:hover, .task-planner-type-select:focus, -.task-planner-weekly-override select:hover, -.task-planner-weekly-override select:focus, +.task-planner-reset-override select:hover, +.task-planner-reset-override select:focus, .task-planner-relations select:hover, .task-planner-relations select:focus { border-color: rgba(196, 181, 253, 0.22); @@ -4296,6 +4348,7 @@ button.combo-input-token.combo-value-grab .ui-icon { .task-planner-settings-toggle:hover, .task-planner-dropdown-button:hover, .task-planner-settings-button:hover, +.task-planner-reset-control button:hover:not(:disabled), .task-planner-relation-chip button:hover, .module-scroll-button:hover, .tool-split-scroll-toggle:hover,