Author SHA1 Message Date
Antho fc3cde3d65 merge: intègre test/couverture-front dans dev
Build & Deploy / build (push) Successful in 19s
2026-08-15 15:02:01 +02:00
Antho af464cfcb7 test(front): étend la couverture de 8 à 41 tests
Les fonctionnalités ajoutées récemment n'étaient couvertes par aucun test,
et deux utilitaires historiques ne l'avaient jamais été.

- statutMonitoring : bornes du seuil testées de part et d'autre, là où une
  confusion entre < et <= se verrait.
- dates : protège la correction du décalage de fuseau (toISOString basculait
  en UTC, le 1er janvier local devenait le 31 décembre). Les helpers sont
  sortis de Historique.jsx vers utils/dates.js, un fichier de composant ne
  pouvant pas exporter autre chose sans casser le rafraîchissement à chaud.
- exportToCSV : échappement des virgules, guillemets et sauts de ligne, et
  conservation du zéro, qui est falsy alors que « 0 erreur » est justement
  l'information utile. Cet utilitaire sert quatre écrans et n'avait aucun test.
- Dashboard : les menus ne proposent que les valeurs ayant des résultats, les
  catégories se restreignent au service choisi, et une catégorie devenue sans
  résultat est réinitialisée. Vérifié par mutation : casser la cascade fait
  bien échouer deux tests.
- Admin : création, modification pré-remplie, confirmation avant suppression,
  remontée du refus 409 de l'API, désactivation et non suppression d'un
  monitoring, affichage des libellés à la place des identifiants.

Corrige un défaut révélé par ces tests : referentielsConfig capturait les
fonctions de l'API à l'import, figeant l'implémentation. Elles sont désormais
résolues à l'appel, ce qui découple la configuration du module d'API.
2026-08-15 15:02:00 +02:00
Antho b5e4741178 merge: intègre fix/tracabilite-build dans dev
Build & Deploy / build (push) Successful in 23s
2026-08-15 14:47:00 +02:00
8 changed files with 482 additions and 37 deletions
+174
View File
@@ -0,0 +1,174 @@
/* Écran d'administration des référentiels.
Avant correction, les boutons Modifier / Désactiver n'appelaient rien
(console.log) et « Enregistrer » se contentait de fermer la modale. Ces
tests vérifient que chaque action atteint réellement l'API et que les
erreurs métier remontent à l'utilisateur. */
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { render, screen, waitFor, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import Admin from '../pages/Admin/Admin';
import * as api from '../services/api';
const SERVICES = [
{ id_service: 1, nom_service: 'Contrat' },
{ id_service: 2, nom_service: 'Fournisseur' },
];
const CATEGORIES = [{ id_categorie: 1, intitule_categorie: 'Tiers-payeurs' }];
const MONITORINGS = [{
id_monito: 1, monito_intitule: 'Erreurs tiers payeurs', id_service: 1,
id_categorie: 1, table_source: 'MONITO_TIERS_PAYEURS', bdd_source: 'Sage',
}];
function stubLectures() {
vi.spyOn(api, 'getServices').mockResolvedValue(SERVICES);
vi.spyOn(api, 'getCategories').mockResolvedValue(CATEGORIES);
vi.spyOn(api, 'getContacts').mockResolvedValue([]);
vi.spyOn(api, 'getMonitorings').mockResolvedValue(MONITORINGS);
}
async function ouvrirOnglet(utilisateur, nom) {
await utilisateur.click(screen.getByRole('button', { name: nom }));
}
describe('Admin — CRUD des référentiels', () => {
beforeEach(() => {
vi.restoreAllMocks();
stubLectures();
vi.spyOn(window, 'confirm').mockReturnValue(true);
});
it('crée un service via lAPI et recharge la liste', async () => {
const creation = vi.spyOn(api, 'createService').mockResolvedValue({ id_service: 3 });
const utilisateur = userEvent.setup();
render(<Admin />);
await screen.findByText('Administration');
await ouvrirOnglet(utilisateur, 'Services');
await utilisateur.click(screen.getByRole('button', { name: /Ajouter/ }));
await utilisateur.type(screen.getByLabelText('Nom du service'), 'Comptabilité');
await utilisateur.click(screen.getByRole('button', { name: 'Enregistrer' }));
await waitFor(() => {
expect(creation).toHaveBeenCalledWith({ nom_service: 'Comptabilité' });
});
/* Rechargement après écriture : la liste doit être redemandée. */
expect(api.getServices).toHaveBeenCalledTimes(2);
});
it('pré-remplit le formulaire de modification avec la ligne choisie', async () => {
vi.spyOn(api, 'updateService').mockResolvedValue({});
const utilisateur = userEvent.setup();
render(<Admin />);
await screen.findByText('Administration');
await ouvrirOnglet(utilisateur, 'Services');
const ligne = screen.getByText('Fournisseur').closest('tr');
await utilisateur.click(within(ligne).getByRole('button', { name: /Modifier/ }));
expect(screen.getByLabelText('Nom du service')).toHaveValue('Fournisseur');
});
it('envoie lidentifiant de la ligne lors dune modification', async () => {
const modification = vi.spyOn(api, 'updateService').mockResolvedValue({});
const utilisateur = userEvent.setup();
render(<Admin />);
await screen.findByText('Administration');
await ouvrirOnglet(utilisateur, 'Services');
const ligne = screen.getByText('Fournisseur').closest('tr');
await utilisateur.click(within(ligne).getByRole('button', { name: /Modifier/ }));
await utilisateur.click(screen.getByRole('button', { name: 'Enregistrer' }));
await waitFor(() => {
expect(modification).toHaveBeenCalledWith(2, { nom_service: 'Fournisseur' });
});
});
it('demande confirmation avant de supprimer, et nappelle rien si on refuse', async () => {
window.confirm.mockReturnValue(false);
const suppression = vi.spyOn(api, 'deleteService').mockResolvedValue({});
const utilisateur = userEvent.setup();
render(<Admin />);
await screen.findByText('Administration');
await ouvrirOnglet(utilisateur, 'Services');
const ligne = screen.getByText('Contrat').closest('tr');
await utilisateur.click(within(ligne).getByRole('button', { name: /Supprimer/ }));
expect(window.confirm).toHaveBeenCalled();
expect(suppression).not.toHaveBeenCalled();
});
it('affiche le refus de lAPI quand des enregistrements sont rattachés', async () => {
/* Cas réel : l'API renvoie 409 avec le décompte des rattachements. */
vi.spyOn(api, 'deleteService').mockRejectedValue(
new Error('Erreur 409: Suppression impossible : 5 monitoring(s) rattachés.'),
);
const utilisateur = userEvent.setup();
render(<Admin />);
await screen.findByText('Administration');
await ouvrirOnglet(utilisateur, 'Services');
const ligne = screen.getByText('Contrat').closest('tr');
await utilisateur.click(within(ligne).getByRole('button', { name: /Supprimer/ }));
expect(await screen.findByText(/5 monitoring\(s\) rattachés/)).toBeInTheDocument();
});
it('parle de désactivation, pas de suppression, pour un monitoring', async () => {
/* TABLE_FINAL référence le monitoring : il n'est jamais supprimé. */
const desactivation = vi.spyOn(api, 'desactiverMonitoring').mockResolvedValue({});
const utilisateur = userEvent.setup();
render(<Admin />);
await screen.findByText('Administration');
const ligne = screen.getByText('Erreurs tiers payeurs').closest('tr');
expect(within(ligne).getByRole('button', { name: /Désactiver/ })).toBeInTheDocument();
await utilisateur.click(within(ligne).getByRole('button', { name: /Désactiver/ }));
await waitFor(() => expect(desactivation).toHaveBeenCalledWith(1));
});
it('affiche le libellé du service plutôt que son identifiant brut', async () => {
render(<Admin />);
await screen.findByText('Administration');
const ligne = screen.getByText('Erreurs tiers payeurs').closest('tr');
/* id_service = 1 doit s'afficher « Contrat ». */
expect(within(ligne).getByText('Contrat')).toBeInTheDocument();
});
it('convertit en entier les identifiants issus des listes déroulantes', async () => {
/* Un <select> renvoie une chaîne ; l'API attend un entier. */
const creation = vi.spyOn(api, 'createMonitoring').mockResolvedValue({ id_monito: 9 });
const utilisateur = userEvent.setup();
render(<Admin />);
await screen.findByText('Administration');
await utilisateur.click(screen.getByRole('button', { name: /Ajouter/ }));
await utilisateur.type(screen.getByLabelText('Nom'), 'Nouveau monitoring');
await utilisateur.selectOptions(screen.getByLabelText('Service'), '2');
await utilisateur.selectOptions(screen.getByLabelText('Catégorie'), '1');
await utilisateur.type(screen.getByLabelText('Table source'), 'MONITO_TEST');
await utilisateur.type(screen.getByLabelText('Base source'), 'Sage');
await utilisateur.click(screen.getByRole('button', { name: 'Enregistrer' }));
await waitFor(() => {
expect(creation).toHaveBeenCalledWith({
monito_intitule: 'Nouveau monitoring',
id_service: 2,
id_categorie: 1,
table_source: 'MONITO_TEST',
bdd_source: 'Sage',
});
});
});
});
@@ -0,0 +1,108 @@
/* Filtres du dashboard.
Le comportement testé ici est la correction apportée aux menus déroulants :
ils étaient alimentés par les référentiels SERVICE / CATEGORIE complets,
dont certaines entrées (« Business Intelligence ») n'ont aucun monitoring
rattaché et menaient sur un écran vide. */
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { MemoryRouter } from 'react-router-dom';
import Dashboard from '../pages/Dashboard/Dashboard';
import * as api from '../services/api';
/* Ce que renvoie GET /dashboard/filtres : uniquement les valeurs présentes
dans VUE_CONSO. Contrat porte 2 catégories, Fournisseur une seule. */
const FILTRES = {
services: ['Contrat', 'Fournisseur'],
categories: ['Comptes fournisseurs', 'DOM-TOM', 'Tiers-payeurs'],
combinaisons: [
{ service: 'Contrat', categorie: 'DOM-TOM' },
{ service: 'Contrat', categorie: 'Tiers-payeurs' },
{ service: 'Fournisseur', categorie: 'Comptes fournisseurs' },
],
};
function renduDashboard() {
return render(<MemoryRouter><Dashboard /></MemoryRouter>);
}
describe('Dashboard — menus de filtre', () => {
beforeEach(() => {
vi.restoreAllMocks();
vi.spyOn(api, 'getDashboardFiltres').mockResolvedValue(FILTRES);
vi.spyOn(api, 'getDashboardSummary').mockResolvedValue({
total: 3, ok: 0, critical: 3, total_erreurs: 12,
});
vi.spyOn(api, 'getDashboard').mockResolvedValue([]);
});
it('ne propose que les services ayant au moins un monitoring', async () => {
renduDashboard();
const selectService = await screen.findByDisplayValue('Tous les services');
const options = [...selectService.options].map(o => o.value);
expect(options).toContain('Contrat');
expect(options).toContain('Fournisseur');
/* Le cœur du correctif : ces entrées du référentiel ne doivent pas
apparaître puisqu'elles ramèneraient un dashboard vide. */
expect(options).not.toContain('Business Intelligence');
expect(options).not.toContain('Comptabilité');
});
it('restreint les catégories au service sélectionné', async () => {
const utilisateur = userEvent.setup();
renduDashboard();
const selectService = await screen.findByDisplayValue('Tous les services');
const selectCategorie = screen.getByDisplayValue('Toutes les catégories');
/* Sans filtre de service : toutes les catégories utilisées */
expect([...selectCategorie.options].map(o => o.value)).toEqual(
expect.arrayContaining(['DOM-TOM', 'Tiers-payeurs', 'Comptes fournisseurs']),
);
await utilisateur.selectOptions(selectService, 'Fournisseur');
await waitFor(() => {
const restantes = [...selectCategorie.options].map(o => o.value).filter(Boolean);
expect(restantes).toEqual(['Comptes fournisseurs']);
});
});
it('réinitialise la catégorie devenue sans résultat après changement de service', async () => {
const utilisateur = userEvent.setup();
renduDashboard();
const selectService = await screen.findByDisplayValue('Tous les services');
const selectCategorie = screen.getByDisplayValue('Toutes les catégories');
await utilisateur.selectOptions(selectCategorie, 'DOM-TOM');
expect(selectCategorie.value).toBe('DOM-TOM');
/* DOM-TOM n'existe pas pour Fournisseur : garder la sélection
produirait une combinaison vide. */
await utilisateur.selectOptions(selectService, 'Fournisseur');
await waitFor(() => expect(selectCategorie.value).toBe(''));
});
it('interroge lAPI avec les filtres sélectionnés', async () => {
const utilisateur = userEvent.setup();
renduDashboard();
const selectService = await screen.findByDisplayValue('Tous les services');
await utilisateur.selectOptions(selectService, 'Contrat');
await waitFor(() => {
expect(api.getDashboard).toHaveBeenLastCalledWith('Contrat', '');
});
});
it('affiche les KPI renvoyés par lAPI', async () => {
renduDashboard();
expect(await screen.findByText('12')).toBeInTheDocument();
expect(screen.getByText('Total erreurs')).toBeInTheDocument();
});
});
+50
View File
@@ -0,0 +1,50 @@
/* Formatage des bornes de période de la page Historique.
Ces tests protègent une régression précise : la version d'origine utilisait
toISOString(), qui convertit en UTC. Depuis un fuseau en avance sur UTC
(Paris = UTC+1 en hiver), le 1er janvier local devient le 31 décembre de
l'année précédente, et la période affichée est décalée d'un jour. */
import { describe, it, expect } from 'vitest';
import { formatDateISO, periodeParDefaut } from '../utils/dates';
describe('formatDateISO', () => {
it('formate une date en YYYY-MM-DD', () => {
expect(formatDateISO(new Date(2026, 7, 15))).toBe('2026-08-15');
});
it('complète le mois et le jour à deux chiffres', () => {
expect(formatDateISO(new Date(2026, 0, 5))).toBe('2026-01-05');
});
it('ne décale pas la date à minuit en heure locale', () => {
/* Le cas qui cassait : minuit local est la veille en UTC dès UTC+1. */
const premierJanvierMinuit = new Date(2026, 0, 1, 0, 0, 0);
expect(formatDateISO(premierJanvierMinuit)).toBe('2026-01-01');
});
it('ne décale pas non plus en fin de journée', () => {
/* Symétrique : 23h local bascule au lendemain pour les fuseaux négatifs. */
const finDeJournee = new Date(2026, 11, 31, 23, 30, 0);
expect(formatDateISO(finDeJournee)).toBe('2026-12-31');
});
});
describe('periodeParDefaut', () => {
it('démarre au 1er janvier de lannée de la date fournie', () => {
expect(periodeParDefaut(new Date(2026, 7, 15)).debut).toBe('2026-01-01');
});
it('se termine à la date fournie', () => {
expect(periodeParDefaut(new Date(2026, 7, 15)).fin).toBe('2026-08-15');
});
it('reste cohérente le 1er janvier (début = fin)', () => {
const periode = periodeParDefaut(new Date(2026, 0, 1));
expect(periode.debut).toBe('2026-01-01');
expect(periode.fin).toBe('2026-01-01');
});
it('suit le changement dannée', () => {
expect(periodeParDefaut(new Date(2027, 2, 9)).debut).toBe('2027-01-01');
});
});
@@ -0,0 +1,83 @@
/* Export CSV, utilisé par les pages Vue consolidée, Contrat, Fournisseur,
Historique et Détail monitoring. Les données exportées viennent de Sage et
du CRM : elles contiennent des intitulés avec virgules et apostrophes, d'où
l'importance de l'échappement. */
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { exportToCSV } from '../services/api';
/* Capture le contenu du Blob passé à URL.createObjectURL, sans télécharger. */
function capturerCSV(data, filename) {
let contenu = '';
let nomFichier = '';
vi.stubGlobal('Blob', class {
constructor(parties) { contenu = parties.join(''); }
});
vi.spyOn(URL, 'createObjectURL').mockReturnValue('blob:fake');
vi.spyOn(URL, 'revokeObjectURL').mockImplementation(() => {});
vi.spyOn(document, 'createElement').mockReturnValue({
set download(valeur) { nomFichier = valeur; },
get download() { return nomFichier; },
href: '',
click: () => {},
});
exportToCSV(data, filename);
return { contenu, nomFichier };
}
describe('exportToCSV', () => {
beforeEach(() => {
vi.restoreAllMocks();
});
it('écrit une ligne den-têtes puis une ligne par enregistrement', () => {
const { contenu } = capturerCSV(
[{ nom: 'Tiers payeurs', erreurs: 5 }, { nom: 'DOM-TOM', erreurs: 3 }],
'export.csv',
);
const lignes = contenu.split('\n');
expect(lignes[0]).toBe('nom,erreurs');
expect(lignes[1]).toBe('Tiers payeurs,5');
expect(lignes[2]).toBe('DOM-TOM,3');
});
it('entoure de guillemets une valeur contenant une virgule', () => {
/* Sans échappement, la virgule créerait une colonne fantôme. */
const { contenu } = capturerCSV([{ intitule: 'XEFI Lyon, agence Est' }], 'x.csv');
expect(contenu.split('\n')[1]).toBe('"XEFI Lyon, agence Est"');
});
it('double les guillemets internes', () => {
const { contenu } = capturerCSV([{ intitule: 'Client "Test"' }], 'x.csv');
expect(contenu.split('\n')[1]).toBe('"Client ""Test"""');
});
it('protège une valeur contenant un saut de ligne', () => {
const { contenu } = capturerCSV([{ adresse: 'Ligne 1\nLigne 2' }], 'x.csv');
expect(contenu).toContain('"Ligne 1\nLigne 2"');
});
it('rend les valeurs nulles ou absentes par une cellule vide', () => {
const { contenu } = capturerCSV([{ a: null, b: undefined, c: 'ok' }], 'x.csv');
expect(contenu.split('\n')[1]).toBe(',,ok');
});
it('conserve le zéro au lieu de le traiter comme une valeur vide', () => {
/* 0 est falsy : un test de vérité naïf leffacerait alors que
« 0 erreur » est justement linformation utile. */
const { contenu } = capturerCSV([{ nb_erreurs: 0 }], 'x.csv');
expect(contenu.split('\n')[1]).toBe('0');
});
it('utilise le nom de fichier demandé', () => {
const { nomFichier } = capturerCSV([{ a: 1 }], 'historique_2026.csv');
expect(nomFichier).toBe('historique_2026.csv');
});
it('nexporte rien si la liste est vide', () => {
const creerElement = vi.spyOn(document, 'createElement');
exportToCSV([], 'vide.csv');
expect(creerElement).not.toHaveBeenCalled();
});
});
@@ -0,0 +1,33 @@
/* Règle métier de classement d'un monitoring selon son nombre d'erreurs.
Elle était dupliquée dans trois composants avant d'être centralisée : ces
tests fixent le contrat pour que les trois écrans restent cohérents. */
import { describe, it, expect } from 'vitest';
import { getStatutMonitoring, SEUIL_ATTENTION } from '../utils/statutMonitoring';
describe('getStatutMonitoring', () => {
it('classe OK un monitoring sans erreur', () => {
expect(getStatutMonitoring(0).label).toBe('OK');
expect(getStatutMonitoring(0).variant).toBe('ok');
});
it('classe Attention en dessous du seuil', () => {
expect(getStatutMonitoring(1).label).toBe('Attention');
expect(getStatutMonitoring(SEUIL_ATTENTION - 1).label).toBe('Attention');
});
it('bascule en Critique à partir du seuil, pas avant', () => {
/* La borne est le point où une erreur de type < / <= se verrait. */
expect(getStatutMonitoring(SEUIL_ATTENTION - 1).label).toBe('Attention');
expect(getStatutMonitoring(SEUIL_ATTENTION).label).toBe('Critique');
expect(getStatutMonitoring(SEUIL_ATTENTION + 1).label).toBe('Critique');
});
it('fournit toujours un libellé, une variante et une couleur', () => {
for (const nb of [0, 5, 50]) {
const statut = getStatutMonitoring(nb);
expect(statut.label).toBeTruthy();
expect(statut.variant).toBeTruthy();
expect(statut.color).toMatch(/^var\(--/);
}
});
});
@@ -1,9 +1,7 @@
import { /* Les appels API sont référencés via l'espace de noms et résolus au moment de
createService, updateService, deleteService, l'appel, et non capturés à l'import : la configuration ne fige donc pas une
createCategorie, updateCategorie, deleteCategorie, implémentation, ce qui la découple du module d'API. */
createContact, updateContact, deleteContact, import * as api from '../../services/api';
createMonitoring, updateMonitoring, desactiverMonitoring,
} from '../../services/api';
/* Description déclarative des 4 référentiels administrables. /* Description déclarative des 4 référentiels administrables.
Les colonnes du tableau, les champs du formulaire et les appels API sont Les colonnes du tableau, les champs du formulaire et les appels API sont
@@ -31,9 +29,9 @@ export const REFERENTIELS = [
{ name: 'table_source', label: 'Table source', type: 'text', required: true }, { name: 'table_source', label: 'Table source', type: 'text', required: true },
{ name: 'bdd_source', label: 'Base source', type: 'text', required: true }, { name: 'bdd_source', label: 'Base source', type: 'text', required: true },
], ],
creer: createMonitoring, creer: body => api.createMonitoring(body),
modifier: updateMonitoring, modifier: (id, body) => api.updateMonitoring(id, body),
supprimer: desactiverMonitoring, supprimer: id => api.desactiverMonitoring(id),
}, },
{ {
key: 'services', key: 'services',
@@ -42,9 +40,9 @@ export const REFERENTIELS = [
idField: 'id_service', idField: 'id_service',
colonnes: [{ key: 'nom_service', label: 'Nom' }], colonnes: [{ key: 'nom_service', label: 'Nom' }],
champs: [{ name: 'nom_service', label: 'Nom du service', type: 'text', required: true }], champs: [{ name: 'nom_service', label: 'Nom du service', type: 'text', required: true }],
creer: createService, creer: body => api.createService(body),
modifier: updateService, modifier: (id, body) => api.updateService(id, body),
supprimer: deleteService, supprimer: id => api.deleteService(id),
}, },
{ {
key: 'categories', key: 'categories',
@@ -53,9 +51,9 @@ export const REFERENTIELS = [
idField: 'id_categorie', idField: 'id_categorie',
colonnes: [{ key: 'intitule_categorie', label: 'Nom' }], colonnes: [{ key: 'intitule_categorie', label: 'Nom' }],
champs: [{ name: 'intitule_categorie', label: 'Intitulé', type: 'text', required: true }], champs: [{ name: 'intitule_categorie', label: 'Intitulé', type: 'text', required: true }],
creer: createCategorie, creer: body => api.createCategorie(body),
modifier: updateCategorie, modifier: (id, body) => api.updateCategorie(id, body),
supprimer: deleteCategorie, supprimer: id => api.deleteCategorie(id),
}, },
{ {
key: 'contacts', key: 'contacts',
@@ -76,9 +74,9 @@ export const REFERENTIELS = [
{ name: 'mail', label: 'Email', type: 'email', required: true }, { name: 'mail', label: 'Email', type: 'email', required: true },
{ name: 'id_service', label: 'Service', type: 'select', options: 'services', required: true }, { name: 'id_service', label: 'Service', type: 'select', options: 'services', required: true },
], ],
creer: createContact, creer: body => api.createContact(body),
modifier: updateContact, modifier: (id, body) => api.updateContact(id, body),
supprimer: deleteContact, supprimer: id => api.deleteContact(id),
}, },
]; ];
@@ -8,27 +8,9 @@ import DataTable from '../../components/common/DataTable';
import LoadingSpinner from '../../components/common/LoadingSpinner'; import LoadingSpinner from '../../components/common/LoadingSpinner';
import ErrorMessage from '../../components/common/ErrorMessage'; import ErrorMessage from '../../components/common/ErrorMessage';
import Button from '../../components/common/Button'; import Button from '../../components/common/Button';
import { periodeParDefaut } from '../../utils/dates';
import styles from './Historique.module.css'; import styles from './Historique.module.css';
/* Formate une date au format attendu par <input type="date"> (YYYY-MM-DD).
On lit les composantes en heure locale plutôt que d'utiliser toISOString(),
qui bascule en UTC : un 1er janvier saisi depuis Paris (UTC+1) y devient le
31 décembre de l'année précédente. */
function formatDateISO(date) {
const mois = String(date.getMonth() + 1).padStart(2, '0');
const jour = String(date.getDate()).padStart(2, '0');
return `${date.getFullYear()}-${mois}-${jour}`;
}
/* Période affichée par défaut : du 1er janvier de l'année courante à aujourd'hui. */
function periodeParDefaut() {
const aujourdhui = new Date();
return {
debut: formatDateISO(new Date(aujourdhui.getFullYear(), 0, 1)),
fin: formatDateISO(aujourdhui),
};
}
/* Récupère le tableau d'historique et la série du graphique pour les filtres /* Récupère le tableau d'historique et la série du graphique pour les filtres
donnés. Sans monitoring sélectionné, on prend la courbe globale. donnés. Sans monitoring sélectionné, on prend la courbe globale.
Fonction pure : elle ne touche à aucun état, ce qui permet de l'appeler Fonction pure : elle ne touche à aucun état, ce qui permet de l'appeler
+17
View File
@@ -0,0 +1,17 @@
/* Formate une date au format attendu par <input type="date"> (YYYY-MM-DD).
On lit les composantes en heure locale plutôt que d'utiliser toISOString(),
qui bascule en UTC : un 1er janvier saisi depuis Paris (UTC+1) y devient le
31 décembre de l'année précédente. */
export function formatDateISO(date) {
const mois = String(date.getMonth() + 1).padStart(2, '0');
const jour = String(date.getDate()).padStart(2, '0');
return `${date.getFullYear()}-${mois}-${jour}`;
}
/* Période affichée par défaut : du 1er janvier de l'année courante à aujourd'hui. */
export function periodeParDefaut(aujourdhui = new Date()) {
return {
debut: formatDateISO(new Date(aujourdhui.getFullYear(), 0, 1)),
fin: formatDateISO(aujourdhui),
};
}