Add calendar toolbox with weekly and monthly scheduling
All checks were successful
Deploy Sokko G / deploy (push) Successful in 14s
All checks were successful
Deploy Sokko G / deploy (push) Successful in 14s
This commit is contained in:
parent
4f73e0068b
commit
992665f4ea
39 changed files with 4331 additions and 36 deletions
123
website/src/features/toolboxes/storage/modules/calendar.js
Normal file
123
website/src/features/toolboxes/storage/modules/calendar.js
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
// Rôle : normalise et compacte les événements du calendrier hebdomadaire et mensuel.
|
||||
import { CALENDAR_ALERT_MODES, CALENDAR_ALERT_OFFSETS, CALENDAR_COLORS, CALENDAR_MONTHLY_REPEAT_BY, CALENDAR_MONTH_REPEAT_MODES, CALENDAR_VIEW_MODES, formatLocalDateKey, getLocalDayIndex, parseLocalDateKey, WEEK_DAY_COUNT } from "../../modules/calendarUtils.js";
|
||||
import { uid } from "./shared.js";
|
||||
|
||||
const VIEW_MODES = new Set(CALENDAR_VIEW_MODES);
|
||||
const ALERT_MODES = new Set(CALENDAR_ALERT_MODES);
|
||||
const ALERT_OFFSETS = new Set(CALENDAR_ALERT_OFFSETS);
|
||||
const COLORS = new Set(CALENDAR_COLORS);
|
||||
const MONTHLY_REPEAT_BY = new Set(CALENDAR_MONTHLY_REPEAT_BY);
|
||||
const MONTH_REPEAT_MODES = new Set(CALENDAR_MONTH_REPEAT_MODES);
|
||||
|
||||
function normalizeDayIndex(value) {
|
||||
const parsed = Number(value);
|
||||
return Number.isInteger(parsed) && parsed >= 0 && parsed < WEEK_DAY_COUNT ? parsed : -1;
|
||||
}
|
||||
|
||||
function normalizeMinuteOfDay(value) {
|
||||
const parsed = Number(value);
|
||||
return Number.isInteger(parsed) && parsed >= 0 && parsed < 24 * 60 ? parsed : -1;
|
||||
}
|
||||
|
||||
function normalizeDateKey(value) {
|
||||
const date = parseLocalDateKey(value);
|
||||
return date ? formatLocalDateKey(date) : "";
|
||||
}
|
||||
|
||||
function normalizeExcludedOccurrences(value) {
|
||||
if (!Array.isArray(value)) return [];
|
||||
return [...new Set(value.map(normalizeDateKey).filter(Boolean))].sort();
|
||||
}
|
||||
|
||||
function normalizeAlertOffsets(value) {
|
||||
const rawOffsets = Array.isArray(value) ? value : [value];
|
||||
const offsets = [...new Set(rawOffsets.map(Number).filter((offset) => ALERT_OFFSETS.has(offset)))];
|
||||
return offsets.length ? offsets.sort((a, b) => b - a) : [10];
|
||||
}
|
||||
|
||||
export function normalizeCalendarData(data) {
|
||||
const viewMode = VIEW_MODES.has(data?.viewMode) ? data.viewMode : "week";
|
||||
const events = (Array.isArray(data?.events) ? data.events : [])
|
||||
.map((event) => {
|
||||
const title = String(event?.title || "").trim();
|
||||
const dayIndex = normalizeDayIndex(event?.dayIndex);
|
||||
const startDate = normalizeDateKey(event?.startDate);
|
||||
const endDate = normalizeDateKey(event?.endDate || event?.startDate);
|
||||
const parsedStartMinutes = normalizeMinuteOfDay(event?.startMinutes);
|
||||
const startMinutes = viewMode === "month" && parsedStartMinutes < 0 ? 0 : parsedStartMinutes;
|
||||
const monthlyRepeatBy = MONTHLY_REPEAT_BY.has(event?.monthlyRepeatBy) ? event.monthlyRepeatBy : "monthDay";
|
||||
const monthRepeat = MONTH_REPEAT_MODES.has(event?.repeat) ? event.repeat : event?.repeatable === false ? "none" : "monthly";
|
||||
const hasValidDateTarget = viewMode === "month"
|
||||
? startDate && endDate && endDate >= startDate
|
||||
: dayIndex >= 0;
|
||||
if (!title || !hasValidDateTarget || startMinutes < 0) return null;
|
||||
|
||||
const endMinutes = normalizeMinuteOfDay(event?.endMinutes);
|
||||
const alertMode = ALERT_MODES.has(event?.alertMode) ? event.alertMode : "off";
|
||||
const alertOffsetMinutes = normalizeAlertOffsets(event?.alertOffsetMinutes);
|
||||
const color = COLORS.has(event?.color) ? event.color : "violet";
|
||||
const repeatable = viewMode === "month" ? monthRepeat !== "none" : event?.repeatable !== false;
|
||||
const normalized = {
|
||||
id: event?.id || uid("calendar"),
|
||||
title,
|
||||
startMinutes,
|
||||
repeat: viewMode === "month" ? monthlyRepeatBy === "monthDay" && monthRepeat === "weekly" ? "none" : monthRepeat : repeatable ? "weekly" : "none",
|
||||
repeatable,
|
||||
alertMode,
|
||||
alertOffsetMinutes,
|
||||
color
|
||||
};
|
||||
if (viewMode === "month") {
|
||||
normalized.monthlyRepeatBy = monthlyRepeatBy;
|
||||
normalized.startDate = startDate;
|
||||
normalized.endDate = endDate;
|
||||
normalized.dayIndex = dayIndex >= 0 ? dayIndex : getLocalDayIndex(parseLocalDateKey(startDate));
|
||||
normalized.excludedOccurrences = normalizeExcludedOccurrences(event?.excludedOccurrences);
|
||||
} else {
|
||||
normalized.dayIndex = dayIndex;
|
||||
}
|
||||
if (endMinutes > startMinutes) normalized.endMinutes = endMinutes;
|
||||
return normalized;
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
return {
|
||||
initialized: data?.initialized === true || events.length > 0,
|
||||
viewMode,
|
||||
weekStartsOn: Number(data?.weekStartsOn) === 0 ? 0 : 1,
|
||||
events
|
||||
};
|
||||
}
|
||||
|
||||
export function compactCalendarDataForStorage(value) {
|
||||
const normalized = normalizeCalendarData(value);
|
||||
const compact = {};
|
||||
if (normalized.initialized) compact.initialized = true;
|
||||
if (normalized.viewMode !== "week") compact.viewMode = normalized.viewMode;
|
||||
if (normalized.weekStartsOn !== 1) compact.weekStartsOn = normalized.weekStartsOn;
|
||||
if (normalized.events.length) {
|
||||
compact.events = normalized.events.map((event) => {
|
||||
const compactEvent = {
|
||||
id: event.id,
|
||||
title: event.title
|
||||
};
|
||||
if (normalized.viewMode !== "month" || event.startMinutes !== 0) compactEvent.startMinutes = event.startMinutes;
|
||||
if (normalized.viewMode === "month") {
|
||||
compactEvent.startDate = event.startDate;
|
||||
if (event.endDate !== event.startDate) compactEvent.endDate = event.endDate;
|
||||
if (event.repeat !== "monthly") compactEvent.repeat = event.repeat;
|
||||
if (event.monthlyRepeatBy !== "monthDay") compactEvent.monthlyRepeatBy = event.monthlyRepeatBy;
|
||||
if (event.excludedOccurrences.length) compactEvent.excludedOccurrences = event.excludedOccurrences;
|
||||
} else {
|
||||
compactEvent.dayIndex = event.dayIndex;
|
||||
}
|
||||
if (normalized.viewMode !== "month" && event.endMinutes) compactEvent.endMinutes = event.endMinutes;
|
||||
if (normalized.viewMode !== "month" && event.repeatable === false) compactEvent.repeatable = false;
|
||||
if (event.alertMode !== "off") compactEvent.alertMode = event.alertMode;
|
||||
if (event.alertMode !== "off" && (event.alertOffsetMinutes.length !== 1 || event.alertOffsetMinutes[0] !== 10)) compactEvent.alertOffsetMinutes = event.alertOffsetMinutes;
|
||||
if (event.color !== "violet") compactEvent.color = event.color;
|
||||
return compactEvent;
|
||||
});
|
||||
}
|
||||
return Object.keys(compact).length ? compact : null;
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
// Rôle : expose le registre strict des compacteurs par type d'outil toolbox.
|
||||
import { compactCalculatorDataForStorage } from "./calculator.js";
|
||||
import { compactCalendarDataForStorage } from "./calendar.js";
|
||||
import { compactChecklistDataForStorage } from "./checklist.js";
|
||||
import { compactCombosDataForStorage } from "./combos.js";
|
||||
import { compactCountersDataForStorage } from "./counters.js";
|
||||
|
|
@ -20,6 +21,7 @@ export const MODULE_DATA_COMPACTORS = {
|
|||
counters: compactCountersDataForStorage,
|
||||
combos: compactCombosDataForStorage,
|
||||
calculator: compactCalculatorDataForStorage,
|
||||
calendar: compactCalendarDataForStorage,
|
||||
table: compactTableDataForStorage,
|
||||
timer: compactTimerDataForStorage,
|
||||
taskPlanner: compactTaskPlannerDataForStorage,
|
||||
|
|
|
|||
|
|
@ -15,13 +15,23 @@ const NOTEPAD_RGB_COLOR_MAP = {
|
|||
};
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value || "")
|
||||
return decodeBasicEntitiesDeep(value)
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """);
|
||||
}
|
||||
|
||||
function decodeBasicEntitiesDeep(value) {
|
||||
let current = String(value || "");
|
||||
for (let index = 0; index < 5; index += 1) {
|
||||
const next = decodeBasicEntities(current);
|
||||
if (next === current) return next;
|
||||
current = next;
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
function decodeBasicEntities(value) {
|
||||
return String(value || "")
|
||||
.replace(/ /gi, " ")
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ export const ID_PREFIXES = {
|
|||
counter: "c",
|
||||
combo: "o",
|
||||
calc: "r",
|
||||
calendar: "h",
|
||||
marker: "k",
|
||||
timer: "z",
|
||||
task: "a",
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { MODULE_DATA_COMPACTORS } from "./modules/index.js";
|
|||
import { ID_PREFIXES, uid } from "./modules/shared.js";
|
||||
|
||||
export { normalizeCalculatorData } from "./modules/calculator.js";
|
||||
export { normalizeCalendarData } from "./modules/calendar.js";
|
||||
export { clampQty, normalizeChecklistData } from "./modules/checklist.js";
|
||||
export { normalizeCombosData } from "./modules/combos.js";
|
||||
export { normalizeCountersData } from "./modules/counters.js";
|
||||
|
|
@ -250,6 +251,12 @@ function remapModuleDataForExport(type, data, nextId) {
|
|||
return remapped;
|
||||
}
|
||||
|
||||
if (type === "calendar") {
|
||||
const remapped = { ...compact };
|
||||
if (compact.events) remapped.events = compact.events.map((event) => ({ ...event, id: nextId("calendar") }));
|
||||
return remapped;
|
||||
}
|
||||
|
||||
if (type === "taskPlanner") {
|
||||
const taskIdMap = new Map();
|
||||
compact.tasks?.forEach((task) => taskIdMap.set(task.id, nextId("task")));
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue