v2026.5.16 — Passage au schéma de versionning ANNÉE.MAJEURE.PATCH + faux input date custom (Mardi 24.04.2026)

This commit is contained in:
Quentin Rouiller
2026-04-23 13:03:58 +02:00
parent 763e63d9c6
commit ea5a42c5e1
5 changed files with 291 additions and 26 deletions
+86 -8
View File
@@ -246,6 +246,7 @@ async function init() {
// Initialiser la date = aujourd'hui
state.currentDate = todayISO();
document.getElementById("date-picker").value = state.currentDate;
updateDatePickerDayLabel(state.currentDate); // v2026.5.16 : label "Mardi"
// v5.0.11 : détecter le contexte réseau en arrière-plan (non bloquant)
detectNetworkContextAsync();
@@ -799,11 +800,37 @@ function initAppFooter() {
function initAppClock() {
const el = document.getElementById("app-clock");
if (!el) return;
const dateEl = document.getElementById("app-clock-date");
const timeEl = document.getElementById("app-clock-time");
// v2026.5.16 : format "Mardi 21 avril 2026"
const JOURS = ["Dimanche", "Lundi", "Mardi", "Mercredi", "Jeudi", "Vendredi", "Samedi"];
const MOIS = [
"janvier", "février", "mars", "avril", "mai", "juin",
"juillet", "août", "septembre", "octobre", "novembre", "décembre"
];
let lastDateStr = "";
const tick = () => {
const d = new Date();
const h = String(d.getHours()).padStart(2, "0");
const m = String(d.getMinutes()).padStart(2, "0");
el.textContent = `${h}:${m}`;
const timeStr = `${h}:${m}`;
if (timeEl) timeEl.textContent = timeStr;
else el.textContent = timeStr; // fallback si ancien markup
// Date complète : actualisée seulement si elle a changé (évite reflow inutile)
if (dateEl) {
const jour = JOURS[d.getDay()];
const num = d.getDate();
const mois = MOIS[d.getMonth()];
const annee = d.getFullYear();
const dateStr = `${jour} ${num} ${mois} ${annee}`;
if (dateStr !== lastDateStr) {
dateEl.textContent = dateStr;
lastDateStr = dateStr;
}
}
// v5.0.0 : profite du tick pour mettre à jour la ligne rouge "now"
updateNowLine();
};
@@ -812,6 +839,21 @@ function initAppClock() {
setInterval(tick, 30 * 1000);
}
// v2026.5.16 : met à jour le label court du jour affiché à gauche du
// date-picker (ex: "Mardi", "Lundi"). Appelé à chaque changement de date.
const DAY_NAMES_FULL = ["Dimanche", "Lundi", "Mardi", "Mercredi", "Jeudi", "Vendredi", "Samedi"];
function updateDatePickerDayLabel(isoDate) {
const el = document.getElementById("date-picker-day");
if (!el) return;
if (!isoDate) { el.textContent = ""; return; }
try {
const d = isoToDate(isoDate);
el.textContent = DAY_NAMES_FULL[d.getDay()];
} catch (e) {
el.textContent = "";
}
}
// v5.0.0 : ligne verticale rouge "heure actuelle" sur la timeline, visible
// UNIQUEMENT quand on affiche aujourd'hui. Appelée à chaque tick de l'horloge
// + après chaque render (cf renderFromData).
@@ -2155,6 +2197,7 @@ async function loadForDate(isoDate, opts = {}) {
state.currentDate = isoDate;
document.getElementById("date-picker").value = isoDate;
updateDatePickerDayLabel(isoDate); // v2026.5.16 : label "Mardi" à côté
if (!state.session) {
// v4.2.5 : afficher le cache + bannière, plutôt que l'écran "session"
@@ -5366,7 +5409,10 @@ function splitOneContact(raw) {
// Prénom 41XXXXXXXXX → extraire 41XXXXXXXXX puis reformater en
// +41 XX XXX XX XX). On exige exactement 11 chiffres collés pour
// éviter de matcher des codes postaux ou autres nombres.
const rxLong = /(\+41\s?\d(?:[\d\s.\-]*\d)?|\+33\s?\d(?:[\d\s.\-]*\d)?|0\d(?:[\d\s.\-]*\d)?|(?<!\d)41\d{9}(?!\d)|(?<!\d)33\d{9}(?!\d))/g;
// v2026.5.16 : ne PAS matcher si le numéro est précédé d'une lettre ou
// d'un underscore (identifiants style XXXX_NNNNNNNN, ABC123456,
// SERIAL_0123456789). On ajoute un lookbehind négatif (?<![A-Za-z_]).
const rxLong = /(?<![A-Za-z_])(\+41\s?\d(?:[\d\s.\-]*\d)?|\+33\s?\d(?:[\d\s.\-]*\d)?|0\d(?:[\d\s.\-]*\d)?|(?<!\d)41\d{9}(?!\d)|(?<!\d)33\d{9}(?!\d))/g;
// SHORT : numéro interne court (5 chiffres).
// - v4.1.20 : accepte "12345Texte" (pas de séparateur après)
// - v4.2.3 : accepte aussi les formats AVEC ESPACES au sein du numéro,
@@ -5415,6 +5461,26 @@ function splitOneContact(raw) {
}
name = cleanContactName(name);
// v2026.5.16 : dernier garde-fou — rejeter les "noms" qui ressemblent
// à des fragments de description technique plutôt qu'à des vrais contacts.
// Exemples rejetés :
// - "1x" (quantité isolée)
// - "1x pc" (quantité + type matériel)
// - "pc XNNNNNN" (type + numéro de série)
// - "XXXX_NNNNNNNN" (identifiant matériel)
// Critères d'un vrai nom : contient au moins un mot qui commence par une
// majuscule ET n'est pas juste un identifiant technique.
if (name) {
const looksLikeIdentifier = /^[A-Z]{2,}[_\-]\d+$/.test(name); // XXXX_NNNNNNNN
const startsWithQuantity = /^\d+x(\s|$)/i.test(name); // "1x" ou "1x pc"
const noCapitalWord = !/\b[A-ZÉÈÀÂÎÔÛÇ][a-zéèàâîôûç]+/.test(name); // aucun mot "Xxxxx"
const hasOnlyTechTokens = /^(\d+x|pc|mac|t[ée]l[ée]phone|ecran|docking|rollout)(\s+(\d+x|pc|mac|t[ée]l[ée]phone|ecran|docking|rollout|[A-Z]\d+))*\s*$/i.test(name);
if (looksLikeIdentifier || startsWithQuantity || hasOnlyTechTokens || (noCapitalWord && !phone)) {
name = null;
}
}
return { name, phone };
}
@@ -5513,22 +5579,34 @@ function splitLieu(raw) {
// Retirer un / final (avec ou sans espaces)
s = s.replace(/\s*\/\s*$/, "").trim();
if (!s) return { ville: null, adresse: null };
const idx = s.indexOf("/");
// v2026.5.16 : le format EasyVista peut avoir jusqu'à 3 parties séparées
// par "/" : VILLE / ADRESSE / PRÉCISIONS (étage, bureau, indications).
// Exemple : "LAUSANNE / Av. de Beaulieu 19 / 4eme en face de l'ascenseur"
// On ne garde que VILLE + ADRESSE. Les précisions (3e partie et suivantes)
// sont strippées — elles alourdissent la carte et sont disponibles dans
// le tooltip détaillé.
const parts = s.split("/").map(p => p.trim()).filter(Boolean);
let ville, adresse;
if (idx < 0) {
if (parts.length === 0) {
return { ville: null, adresse: null };
} else if (parts.length === 1) {
// Pas de slash : tout est l'adresse
ville = null;
adresse = s;
adresse = parts[0];
} else {
ville = s.substring(0, idx).trim();
adresse = s.substring(idx + 1).trim();
// 2+ parties : ville = 1ère, adresse = 2e, on ignore le reste
ville = parts[0];
adresse = parts[1];
}
// Capitaliser la 1ère lettre des mots de voie (Rue, Chemin, Route, Avenue,
// Boulevard, Place, Quai, Impasse + abréviations Av., Ch., Rte, Bd)
if (adresse) {
adresse = adresse.replace(
/\b(rue|chemin|route|avenue|boulevard|place|quai|impasse|ruelle|allée|allee|passage|sentier|av\.?|ch\.?|rte\.?|bd\.?)\b/gi,
(match) => {
// Conserver la casse existante si déjà majuscule, sinon capitaliser
if (/^[A-ZÉÈÀÂÎÔÛÇ]/.test(match)) return match;
return match.charAt(0).toUpperCase() + match.slice(1).toLowerCase();
}