Site Giraud-Finance V1.0

This commit is contained in:
2026-08-21 07:27:24 +00:00
parent df8264a8ec
commit 0c02267c0e
+174 -754
View File
@@ -1,794 +1,214 @@
"use strict";
// Tableaux globaux
let configIndices = []; // Données issues de la table "indice"
let coursActionsData = []; // Données issues de la table "cours_actions"
/*
* =========================================================
* CONFIGURATION
* =========================================================
*/
const API_URL = "../php/api_list_full_table_indice.php";
/*
* API de création.
*
* A créer côté PHP.
*/
const API_CREATE_URL = "../php/api_create_indice.php";
const tableauIndices = document.getElementById("tableauIndices");
const compteur = document.getElementById("compteur");
const message = document.getElementById("message");
const btnActualiser = document.getElementById("btnActualiser");
const btnNouveau = document.getElementById("btnNouveau");
const filtreIndice = document.getElementById("filtreIndice");
/*
* =========================================================
* MODAL
* =========================================================
*/
const modalNouvelEnregistrement = document.getElementById(
"modalNouvelEnregistrement",
function chargerDonnees() {
// Chargement en parallèle des deux API
Promise.all([
fetch("./php/api_lecture_tableindice.php").then((res) => res.json()),
fetch("./php/api_indices.php").then((res) => res.json()),
])
.then(([configResult, coursResult]) => {
if (configResult.status === "success") {
configIndices = configResult.data;
console.log(
"Configuration indices chargée :",
configIndices.length,
"éléments",
);
const formNouvelEnregistrement = document.getElementById(
"formNouvelEnregistrement",
);
const btnFermerModal = document.getElementById("btnFermerModal");
const btnAnnuler = document.getElementById("btnAnnuler");
const btnSauvegarder = document.getElementById("btnSauvegarder");
const messageNouveau = document.getElementById("messageNouveau");
/*
* =========================================================
* CHAMPS FORMULAIRE
* =========================================================
*/
const nouvelIndice = document.getElementById("nouvelIndice");
const nouveauNom = document.getElementById("nouveauNom");
const nouvelIsin = document.getElementById("nouvelIsin");
const nouveauSymbole = document.getElementById("nouveauSymbole");
const nouvelActif = document.getElementById("nouvelActif");
const nouvelAfficher = document.getElementById("nouvelAfficher");
/*
* =========================================================
* DONNEES
* =========================================================
*/
let tousLesEnregistrements = [];
/*
* =========================================================
* AFFICHER MESSAGE
* =========================================================
*/
function afficherMessage(texte, type = "info") {
message.textContent = texte;
message.classList.remove(
"hidden",
"bg-emerald-900/50",
"border-emerald-700",
"text-emerald-300",
"bg-rose-900/50",
"border-rose-700",
"text-rose-300",
"bg-slate-900",
"border-slate-700",
"text-slate-300",
);
message.classList.add("border");
if (type === "success") {
message.classList.add(
"bg-emerald-900/50",
"border-emerald-700",
"text-emerald-300",
);
} else if (type === "error") {
message.classList.add("bg-rose-900/50", "border-rose-700", "text-rose-300");
} else {
message.classList.add("bg-slate-900", "border-slate-700", "text-slate-300");
}
console.error("Erreur API config indices :", configResult.message);
}
/*
* =========================================================
* CACHER MESSAGE
* =========================================================
*/
function cacherMessage() {
message.classList.add("hidden");
if (coursResult.status === "success") {
coursActionsData = coursResult.data;
console.log(
"Cours actions chargés :",
coursActionsData.length,
"éléments",
);
} else {
console.error("Erreur API cours_actions :", coursResult.message);
}
/*
* =========================================================
* ECHAPPER HTML
* =========================================================
*/
// Mise à jour des vues
afficherSyntheseCartes();
alimenterBandeauxTicker();
function echapperHTML(valeur) {
if (valeur === null || valeur === undefined) {
return "";
// Statut de rafraîchissement
const statut = document.getElementById("statut");
if (statut) {
statut.innerText = `Dernière mise à jour : ${new Date().toLocaleTimeString()}`;
}
})
.catch((error) =>
console.error("Erreur réseau lors du chargement des API :", error),
);
}
return String(valeur)
.replace(/&/g, "&")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
function afficherSyntheseCartes() {
const container = document.getElementById("cards-container");
if (!container) return;
/*
* =========================================================
* CHECKBOX
* =========================================================
*/
container.innerHTML = "";
function creerCheckbox(valeur) {
const checked = Number(valeur) === 1 || valeur === true || valeur === "1";
// CARTES : On se base STRICTEMENT sur le champ afficher == 1
const actionsAffichees = configIndices.filter(
(item) => String(item.afficher) === "1" || item.afficher == 1,
);
return `
<div class="flex justify-center items-center">
if (actionsAffichees.length > 0) {
actionsAffichees.forEach((conf) => {
// Croisement avec les cours en direct
const d =
coursActionsData.find(
(item) => item.symbole === conf.symbole || item.isin === conf.isin,
) || {};
<input
type="checkbox"
${checked ? "checked" : ""}
disabled
class="w-5 h-5 accent-emerald-500 cursor-not-allowed opacity-90"
/>
const ecartValeur = parseFloat(d.variation || 0);
const isPositive = ecartValeur >= 0;
const badgeColor = isPositive ? "text-emerald-400" : "text-rose-400";
const sign = isPositive ? "+" : "";
const volumeFormate = d.volume
? parseInt(d.volume).toLocaleString("fr-FR")
: "--";
container.innerHTML += `
<div class="bg-slate-800 p-4 rounded-2xl shadow-xl border border-slate-700 flex flex-col justify-between hover:border-slate-500 transition-all">
<!-- En-tête : Nom / Ticker / ISIN / Indice à gauche ET Cours / Variation / Date à droite -->
<div class="flex justify-between items-start mb-2">
<div class="max-w-[150px]">
<h3 class="text-xs font-bold text-white leading-tight truncate" title="${conf.nom || conf.indice}">
${conf.nom || conf.indice} <span class="text-[11px] font-normal text-slate-400">(${conf.symbole || "N/A"})</span>
</h3>
<span class="text-[10px] text-slate-500 font-mono italic block mt-0.5">${conf.isin || "Aucun ISIN"}</span>
<span class="text-[10px] font-semibold text-slate-400 uppercase tracking-wide block mt-0.5">${conf.indice || ""}</span>
</div>
<div class="text-right">
<div class="text-base font-extrabold text-white leading-tight">
${d.cours ? parseFloat(d.cours).toLocaleString("fr-FR", { minimumFractionDigits: 2 }) : "--"}
</div>
<div class="text-xs font-bold ${badgeColor} leading-tight mt-0.5">
${sign}${ecartValeur.toLocaleString("fr-FR", { minimumFractionDigits: 2, maximumFractionDigits: 4 })}
<span class="font-semibold">(${sign}${parseFloat(d.variation_pct || 0).toFixed(2)}%)</span>
</div>
<div class="text-[8px] text-slate-500 font-mono mt-0.5">
${d.date_cours || "--/--"}
</div>
</div>
</div>
<!-- Bloc Infos : Haut / Bas / Volume -->
<div class="grid grid-cols-3 gap-1.5 bg-slate-900/50 p-1.5 rounded-xl border border-slate-700/50 text-center">
<div>
<span class="text-[8px] text-slate-400 block uppercase">Haut</span>
<span class="text-[10px] font-semibold text-slate-200">${d.plus_haut ? parseFloat(d.plus_haut).toLocaleString("fr-FR", { minimumFractionDigits: 2 }) : "--"}</span>
</div>
<div>
<span class="text-[8px] text-slate-400 block uppercase">Bas</span>
<span class="text-[10px] font-semibold text-slate-200">${d.plus_bas ? parseFloat(d.plus_bas).toLocaleString("fr-FR", { minimumFractionDigits: 2 }) : "--"}</span>
</div>
<div>
<span class="text-[8px] text-slate-400 block uppercase">Volume</span>
<span class="text-[10px] font-semibold text-slate-200">${volumeFormate}</span>
</div>
</div>
</div>
`;
});
} else {
container.innerHTML = `
<div class="col-span-3 text-center py-10 bg-slate-800/30 rounded-2xl border border-slate-800">
<span class="text-slate-400 text-sm">Aucune action configurée à 'afficher = 1'.</span>
</div>
`;
}
/*
* =========================================================
* CREER UNE LIGNE
* =========================================================
*/
function creerLigne(enregistrement) {
return `
<tr
class="hover:bg-slate-700/30 transition-colors"
>
<td
class="px-4 py-3 text-sm text-slate-500 whitespace-nowrap"
>
${echapperHTML(enregistrement.id)}
</td>
<td
class="px-4 py-3 whitespace-nowrap"
>
<span
class="inline-flex items-center
px-2.5 py-1
rounded-lg
bg-slate-700
border border-slate-600
text-xs font-semibold
text-slate-200"
>
${echapperHTML(enregistrement.indice)}
</span>
</td>
<td
class="px-4 py-3
text-sm font-medium
text-white
whitespace-nowrap"
>
${echapperHTML(enregistrement.nom)}
</td>
<td
class="px-4 py-3
text-sm text-slate-300
font-mono
whitespace-nowrap"
>
${echapperHTML(enregistrement.isin)}
</td>
<td
class="px-4 py-3
text-sm text-slate-300
font-semibold
whitespace-nowrap"
>
${echapperHTML(enregistrement.symbole)}
</td>
<td class="px-4 py-3 text-center">
${creerCheckbox(enregistrement.actif)}
</td>
<td class="px-4 py-3 text-center">
${creerCheckbox(enregistrement.afficher)}
</td>
<td
class="px-4 py-3
text-xs text-slate-400
whitespace-nowrap"
>
${echapperHTML(enregistrement.updated_at)}
</td>
<td
class="px-4 py-3
text-xs text-slate-500
whitespace-nowrap"
>
${echapperHTML(enregistrement.created_at)}
</td>
</tr>
`;
}
/*
* =========================================================
* CONSTRUIRE FILTRES
* =========================================================
*/
function alimenterBandeauxTicker() {
const preparerDonneesTicker = (nomIndiceRecherche) => {
const cleanSearch = nomIndiceRecherche
.replace(/[^a-z0-9]/gi, "")
.toLowerCase();
function construireFiltres() {
const indices = [
...new Set(
tousLesEnregistrements
.map(function (enregistrement) {
return enregistrement.indice;
})
.filter(function (indice) {
return (
indice !== null &&
indice !== undefined &&
String(indice).trim() !== ""
);
}),
),
];
indices.sort(function (a, b) {
return String(a).localeCompare(String(b), "fr", {
sensitivity: "base",
});
// BANDEAUX : On prend l'intégralité des actions disponibles pour l'indice (via coursActionsData ou configIndices)
let liste = coursActionsData.filter((item) => {
if (!item.indice) return false;
const cleanItemIndice = item.indice
.replace(/[^a-z0-9]/gi, "")
.toLowerCase();
return cleanItemIndice === cleanSearch;
});
const filtreActuel = filtreIndice.value;
if (liste.length === 0) {
liste = configIndices.filter((item) => {
if (!item.indice) return false;
const cleanItemIndice = item.indice
.replace(/[^a-z0-9]/gi, "")
.toLowerCase();
return cleanItemIndice === cleanSearch;
});
}
filtreIndice.innerHTML = "";
/*
* Tout
*/
const optionTout = document.createElement("option");
optionTout.value = "tout";
optionTout.textContent = "Tout";
filtreIndice.appendChild(optionTout);
/*
* Afficher
*/
const optionAfficher = document.createElement("option");
optionAfficher.value = "afficher";
optionAfficher.textContent = "Afficher";
filtreIndice.appendChild(optionAfficher);
/*
* Inactif
*/
const optionInactif = document.createElement("option");
optionInactif.value = "inactif";
optionInactif.textContent = "Inactif";
filtreIndice.appendChild(optionInactif);
/*
* Indices
*/
indices.forEach(function (indice) {
const option = document.createElement("option");
option.value = "indice:" + indice;
option.textContent = indice;
filtreIndice.appendChild(option);
// Fusion et formatage
let listeFinale = liste.map((conf) => {
let coursMatch =
coursActionsData.find(
(item) => item.symbole === conf.symbole || item.isin === conf.isin,
) || conf;
return {
nom: conf.nom || coursMatch.nom || conf.symbole,
cours: coursMatch.cours || 0,
variation_pct: coursMatch.variation_pct || 0,
};
});
/*
* Restaurer filtre
*/
// Tri alphabétique
listeFinale.sort((a, b) => a.nom.localeCompare(b.nom));
const optionExiste = Array.from(filtreIndice.options).some(function (option) {
return option.value === filtreActuel;
});
if (optionExiste) {
filtreIndice.value = filtreActuel;
} else {
filtreIndice.value = "tout";
}
}
/*
* =========================================================
* APPLIQUER FILTRE
* =========================================================
*/
function appliquerFiltre() {
const filtre = filtreIndice.value;
let enregistrementsFiltres;
/*
* Tout
*/
if (filtre === "tout") {
enregistrementsFiltres = tousLesEnregistrements;
} else if (filtre === "afficher") {
/*
* Afficher
*/
enregistrementsFiltres = tousLesEnregistrements.filter(
function (enregistrement) {
return (
Number(enregistrement.afficher) === 1 ||
enregistrement.afficher === true ||
enregistrement.afficher === "1"
);
},
);
} else if (filtre === "inactif") {
/*
* Inactif
*/
enregistrementsFiltres = tousLesEnregistrements.filter(
function (enregistrement) {
return !(
Number(enregistrement.actif) === 1 ||
enregistrement.actif === true ||
enregistrement.actif === "1"
);
},
);
} else if (filtre.startsWith("indice:")) {
/*
* Indice
*/
const indiceRecherche = filtre.substring("indice:".length);
enregistrementsFiltres = tousLesEnregistrements.filter(
function (enregistrement) {
return (
String(enregistrement.indice).toLowerCase() ===
String(indiceRecherche).toLowerCase()
);
},
);
} else {
enregistrementsFiltres = tousLesEnregistrements;
}
afficherDonneesFiltrees(enregistrementsFiltres);
}
/*
* =========================================================
* AFFICHER DONNEES FILTREES
* =========================================================
*/
function afficherDonneesFiltrees(enregistrements) {
tableauIndices.innerHTML = "";
if (!Array.isArray(enregistrements) || enregistrements.length === 0) {
compteur.textContent = "0 enregistrement";
tableauIndices.innerHTML = `
<tr>
<td
colspan="9"
class="px-6 py-10 text-center
text-sm text-slate-500"
>
Aucun enregistrement correspondant
au filtre sélectionné.
</td>
</tr>
`;
return;
}
/*
* =======================================================
* TRI PAR ID CROISSANT
* =======================================================
*/
const donneesTriees = [...enregistrements].sort(function (a, b) {
return Number(a.id) - Number(b.id);
});
let html = "";
donneesTriees.forEach(function (enregistrement) {
html += creerLigne(enregistrement);
});
tableauIndices.innerHTML = html;
compteur.textContent =
donneesTriees.length +
(donneesTriees.length > 1 ? " enregistrements" : " enregistrement");
}
/*
* =========================================================
* AFFICHER DONNEES
* =========================================================
*/
function afficherDonnees(data) {
tousLesEnregistrements = Array.isArray(data) ? data : [];
construireFiltres();
appliquerFiltre();
}
/*
* =========================================================
* CHARGER DONNEES
* =========================================================
*/
async function chargerDonnees() {
btnActualiser.disabled = true;
btnActualiser.textContent = "↻ Chargement...";
afficherMessage("Chargement des indices et des actions...", "info");
try {
const response = await fetch(API_URL, {
method: "GET",
credentials: "same-origin",
cache: "no-store",
headers: {
Accept: "application/json",
},
});
if (!response.ok) {
throw new Error("Erreur HTTP " + response.status);
}
const result = await response.json();
if (!result || result.status !== "success") {
throw new Error(
result && result.message
? result.message
: "L'API a retourné une erreur.",
);
}
afficherDonnees(result.data);
cacherMessage();
} catch (error) {
console.error("Erreur chargement indices/actions :", error);
tableauIndices.innerHTML = `
<tr>
<td
colspan="9"
class="px-6 py-10
text-center
text-rose-400"
>
Impossible de charger les données.
</td>
</tr>
`;
compteur.textContent = "Erreur de chargement";
afficherMessage(
"Erreur lors de la récupération des données : " + error.message,
"error",
);
} finally {
btnActualiser.disabled = false;
btnActualiser.textContent = "↻ Actualiser";
}
}
/*
* =========================================================
* OUVRIR MODAL
* =========================================================
*/
function ouvrirModal() {
formNouvelEnregistrement.reset();
/*
* Valeurs par défaut
*/
nouvelActif.checked = true;
nouvelAfficher.checked = true;
messageNouveau.classList.add("hidden");
modalNouvelEnregistrement.classList.remove("hidden");
modalNouvelEnregistrement.classList.add("flex");
setTimeout(function () {
nouvelIndice.focus();
}, 50);
}
/*
* =========================================================
* FERMER MODAL
* =========================================================
*/
function fermerModal() {
modalNouvelEnregistrement.classList.add("hidden");
modalNouvelEnregistrement.classList.remove("flex");
}
/*
* =========================================================
* MESSAGE MODAL
* =========================================================
*/
function afficherMessageNouveau(texte, type = "error") {
messageNouveau.textContent = texte;
messageNouveau.classList.remove(
"hidden",
"bg-rose-900/50",
"border-rose-700",
"text-rose-300",
"bg-emerald-900/50",
"border-emerald-700",
"text-emerald-300",
);
messageNouveau.classList.add("border");
if (type === "success") {
messageNouveau.classList.add(
"bg-emerald-900/50",
"border-emerald-700",
"text-emerald-300",
);
} else {
messageNouveau.classList.add(
"bg-rose-900/50",
"border-rose-700",
"text-rose-300",
);
}
}
/*
* =========================================================
* SAUVEGARDER NOUVEL ENREGISTREMENT
* =========================================================
*/
async function sauvegarderNouvelEnregistrement(event) {
event.preventDefault();
/*
* Lecture des champs
*/
const donnees = {
indice: nouvelIndice.value.trim(),
nom: nouveauNom.value.trim(),
isin: nouvelIsin.value.trim().toUpperCase(),
symbole: nouveauSymbole.value.trim().toUpperCase(),
actif: nouvelActif.checked ? 1 : 0,
afficher: nouvelAfficher.checked ? 1 : 0,
return listeFinale;
};
/*
* Vérification
*/
remplirUnBandeau("ticker-cac40", preparerDonneesTicker("CAC40"));
remplirUnBandeau("ticker-ibex35", preparerDonneesTicker("IBEX35"));
remplirUnBandeau("ticker-dowjones", preparerDonneesTicker("DOWJONES"));
}
if (!donnees.indice || !donnees.nom || !donnees.isin || !donnees.symbole) {
afficherMessageNouveau("Tous les champs texte sont obligatoires.");
function remplirUnBandeau(elementId, donneesTriees) {
const container = document.getElementById(elementId);
if (!container) return;
if (donneesTriees.length === 0) {
container.innerHTML = `<span class="px-4 text-slate-500 text-xs">Aucune donnée disponible</span>`;
return;
}
/*
* Désactivation bouton
*/
let blocs = "";
donneesTriees.forEach((item) => {
const isPositive = parseFloat(item.variation_pct || 0) >= 0;
const colorClass = isPositive ? "text-emerald-400" : "text-rose-400";
const sign = isPositive ? "+" : "";
const coursFormate = parseFloat(item.cours || 0).toLocaleString("fr-FR", {
minimumFractionDigits: 2,
});
const variationFormatee = parseFloat(item.variation_pct || 0).toFixed(2);
btnSauvegarder.disabled = true;
btnSauvegarder.textContent = "Sauvegarde...";
messageNouveau.classList.add("hidden");
try {
const response = await fetch(API_CREATE_URL, {
method: "POST",
credentials: "same-origin",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify(donnees),
blocs += `
<div class="inline-flex items-center px-6 space-x-3 border-r border-slate-800/80 whitespace-nowrap">
<span class="font-bold text-white">${item.nom}</span>
<span class="text-slate-300">${coursFormate}</span>
<span class="${colorClass} font-semibold">${sign}${variationFormatee}%</span>
</div>
`;
});
if (!response.ok) {
throw new Error("Erreur HTTP " + response.status);
container.innerHTML = blocs + blocs + blocs;
const dureeTotale = Math.max(20, donneesTriees.length * 4);
container.style.animationDuration = `${dureeTotale}s`;
}
const result = await response.json();
if (!result || result.status !== "success") {
throw new Error(
result && result.message
? result.message
: "Impossible de créer l'enregistrement.",
);
}
/*
* Fermeture
*/
fermerModal();
/*
* Message
*/
afficherMessage("L'enregistrement a été créé avec succès.", "success");
/*
* Rechargement
*/
await chargerDonnees();
} catch (error) {
console.error("Erreur création enregistrement :", error);
afficherMessageNouveau(error.message, "error");
} finally {
btnSauvegarder.disabled = false;
btnSauvegarder.textContent = "Sauvegarder";
}
}
/*
* =========================================================
* EVENEMENTS
* =========================================================
*/
filtreIndice.addEventListener("change", function () {
appliquerFiltre();
});
btnActualiser.addEventListener("click", function () {
// Démarrage
chargerDonnees();
});
btnNouveau.addEventListener("click", function () {
ouvrirModal();
});
btnFermerModal.addEventListener("click", function () {
fermerModal();
});
btnAnnuler.addEventListener("click", function () {
fermerModal();
});
formNouvelEnregistrement.addEventListener(
"submit",
sauvegarderNouvelEnregistrement,
);
/*
* Fermer en cliquant sur le fond
*/
modalNouvelEnregistrement.addEventListener("click", function (event) {
if (event.target === modalNouvelEnregistrement) {
fermerModal();
}
});
/*
* Touche Echap
*/
document.addEventListener("keydown", function (event) {
if (
event.key === "Escape" &&
!modalNouvelEnregistrement.classList.contains("hidden")
) {
fermerModal();
}
});
/*
* =========================================================
* INITIALISATION
* =========================================================
*/
document.addEventListener("DOMContentLoaded", function () {
chargerDonnees();
});
setInterval(chargerDonnees, 60000);