Initial commit

This commit is contained in:
jerem
2026-06-13 13:32:38 +02:00
commit 528d994ea8
9 changed files with 1003 additions and 0 deletions

300
static/index.html Normal file
View File

@@ -0,0 +1,300 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>AutoMood — Prospection</title>
<style>
:root { --accent: #5b4dbf; --fond: #f5f4fa; --bordure: #ddd; }
* { box-sizing: border-box; }
body { font-family: -apple-system, "Segoe UI", sans-serif; margin: 0; background: var(--fond); color: #222; }
header { background: var(--accent); color: #fff; padding: 14px 24px; }
header h1 { margin: 0; font-size: 20px; }
main { max-width: 1200px; margin: 24px auto; padding: 0 16px; }
section { background: #fff; border-radius: 10px; padding: 20px; margin-bottom: 24px; box-shadow: 0 1px 4px rgba(0,0,0,.08); }
h2 { margin-top: 0; font-size: 16px; }
.ligne-url { display: flex; gap: 8px; }
.ligne-url input { flex: 1; }
input, button { font: inherit; padding: 8px 10px; border-radius: 6px; border: 1px solid var(--bordure); }
input:focus { outline: 2px solid var(--accent); border-color: transparent; }
button { background: var(--accent); color: #fff; border: none; cursor: pointer; }
button:disabled { opacity: .5; cursor: wait; }
button.secondaire { background: #eee; color: #222; }
button.danger { background: #c0392b; }
#message { margin: 10px 0 0; padding: 10px; border-radius: 6px; display: none; }
#message.erreur { display: block; background: #fdecea; color: #b03a2e; }
#message.info { display: block; background: #eaf2fd; color: #1a5276; }
#formulaire { display: none; margin-top: 16px; }
.grille { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 12px; }
.champ label { display: block; font-size: 12px; color: #666; margin-bottom: 3px; }
.champ input { width: 100%; }
.actions { margin-top: 14px; display: flex; gap: 8px; }
.carte { border: 1px solid var(--bordure); border-radius: 8px; margin-bottom: 10px; background: #fff; }
.carte-tete { display: flex; flex-wrap: wrap; gap: 6px 14px; align-items: center; padding: 10px 12px; cursor: pointer; }
.carte-tete:hover { background: #faf9fd; }
.carte-titre { display: flex; align-items: center; gap: 8px; min-width: 260px; flex: 1; }
.carte-titre strong { font-size: 14px; }
.badge { background: var(--accent); color: #fff; border-radius: 99px; padding: 2px 9px; font-size: 11px; white-space: nowrap; }
.carte-infos { display: flex; flex-wrap: wrap; gap: 4px 16px; font-size: 13px; align-items: center; }
.sous { color: #666; font-size: 12px; }
.carte-boutons { display: flex; gap: 6px; margin-left: auto; }
.carte-boutons button { padding: 4px 9px; font-size: 12px; }
.carte-detail { padding: 12px; border-top: 1px solid var(--bordure); background: #fbfaff; }
.vide { color: #999; font-style: italic; }
.barre-liste { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
.spinner { display: inline-block; width: 14px; height: 14px; border: 2px solid #fff; border-top-color: transparent; border-radius: 50%; animation: tourne .8s linear infinite; vertical-align: -2px; margin-right: 6px; }
@keyframes tourne { to { transform: rotate(360deg); } }
</style>
</head>
<body>
<header><h1>🎸 AutoMood — Prospection de lieux de concert</h1></header>
<main>
<section>
<h2>Nouveau prospect</h2>
<div class="ligne-url">
<input id="url" type="url" placeholder="https://www.facebook.com/nom-du-lieu" autofocus>
<button id="analyser">Analyser</button>
<button id="connexion" class="secondaire" title="À faire une seule fois : la session est mémorisée">🔑 Connexion Facebook</button>
</div>
<div id="message"></div>
<form id="formulaire">
<div class="grille" id="grille-champs"></div>
<div class="actions">
<button type="submit">Ajouter au fichier</button>
<button type="button" class="secondaire" id="annuler">Annuler</button>
</div>
</form>
</section>
<section>
<div class="barre-liste">
<h2 style="margin:0">Liste des prospects <span id="compteur"></span></h2>
<a href="/api/export"><button type="button" class="secondaire">Télécharger le CSV</button></a>
</div>
<input id="recherche" type="search" placeholder="Filtrer par nom, ville, type, département…" style="width:100%;margin-bottom:12px">
<div id="liste"></div>
</section>
</main>
<script>
const COLONNES = ["Nom du prospect","Département","Ville","Code Postal","Adresse","Date d'ajout",
"Date de contact","Nom de contact","Téléphone","Email","Infos du lieu","Type","Lien Facebook"];
const $ = id => document.getElementById(id);
function message(texte, classe) {
const m = $("message");
m.textContent = texte;
m.className = classe || "";
}
// --- Formulaire de nouveau prospect ---
function construireFormulaire(valeurs) {
const grille = $("grille-champs");
grille.innerHTML = "";
for (const col of COLONNES) {
const div = document.createElement("div");
div.className = "champ";
const label = document.createElement("label");
label.textContent = col;
const input = document.createElement("input");
input.name = col;
input.value = valeurs[col] || "";
if (col === "Date de contact") input.placeholder = "à remplir plus tard";
div.append(label, input);
grille.append(div);
}
$("formulaire").style.display = "block";
}
$("connexion").addEventListener("click", async () => {
const bouton = $("connexion");
bouton.disabled = true;
$("analyser").disabled = true;
bouton.innerHTML = '<span class="spinner"></span>En attente de votre connexion…';
message("Une fenêtre Chromium s'ouvre sur la page de connexion Facebook. Connectez-vous tranquillement (vous avez 5 minutes), la fenêtre se fermera toute seule.", "info");
try {
const rep = await fetch("/api/login", { method: "POST" });
const donnees = await rep.json();
message(rep.ok ? "Connexion Facebook enregistrée ✔ Vous pouvez analyser des pages." : (donnees.message || "Échec de la connexion."), rep.ok ? "info" : "erreur");
} catch {
message("Le serveur ne répond pas. Relancez ./run.sh.", "erreur");
} finally {
bouton.disabled = false;
$("analyser").disabled = false;
bouton.textContent = "🔑 Connexion Facebook";
}
});
$("analyser").addEventListener("click", async () => {
const url = $("url").value.trim();
if (!url) { message("Collez d'abord un lien Facebook.", "erreur"); return; }
const bouton = $("analyser");
bouton.disabled = true;
bouton.innerHTML = '<span class="spinner"></span>Analyse…';
message("Analyse en cours, une fenêtre Chromium s'ouvre quelques secondes…", "info");
try {
const rep = await fetch("/api/scrape", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ url }),
});
const donnees = await rep.json();
if (!rep.ok) { message(donnees.message || "Erreur lors de l'analyse.", "erreur"); return; }
message("Champs pré-remplis : vérifiez et corrigez avant d'ajouter.", "info");
construireFormulaire(donnees);
} catch {
message("Le serveur ne répond pas. Relancez ./run.sh.", "erreur");
} finally {
bouton.disabled = false;
bouton.textContent = "Analyser";
}
});
$("formulaire").addEventListener("submit", async (e) => {
e.preventDefault();
const ligne = {};
for (const input of $("grille-champs").querySelectorAll("input")) ligne[input.name] = input.value;
const rep = await fetch("/api/prospects", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(ligne),
});
if (rep.ok) {
$("formulaire").style.display = "none";
$("url").value = "";
message("Prospect ajouté ✔", "info");
chargerListe();
} else {
message("Échec de l'ajout.", "erreur");
}
});
$("annuler").addEventListener("click", () => {
$("formulaire").style.display = "none";
message("", "");
});
// --- Liste des prospects en fiches ---
let prospects = [];
const echap = (t) => (t || "").replace(/[&<>"]/g, c => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" }[c]));
async function chargerListe() {
prospects = await (await fetch("/api/prospects")).json();
afficherListe();
}
function afficherListe() {
const filtre = $("recherche").value.trim().toLowerCase();
const visibles = prospects.filter(p =>
!filtre || COLONNES.some(c => (p[c] || "").toLowerCase().includes(filtre)));
$("compteur").textContent = filtre ? `(${visibles.length}/${prospects.length})` : `(${prospects.length})`;
const liste = $("liste");
liste.innerHTML = "";
if (!visibles.length) {
liste.innerHTML = `<p class="vide">${prospects.length ? "Aucun prospect ne correspond au filtre." : "Aucun prospect pour le moment."}</p>`;
return;
}
for (const p of visibles) liste.append(carte(p));
}
function carte(p) {
const div = document.createElement("div");
div.className = "carte";
const tete = document.createElement("div");
tete.className = "carte-tete";
const lieu = [p["Ville"], p["Département"]].filter(Boolean).join(" · ");
const contact = p["Date de contact"]
? `✅ Contacté le ${echap(p["Date de contact"])}${p["Nom de contact"] ? ` (${echap(p["Nom de contact"])})` : ""}`
: "⏳ À contacter";
tete.innerHTML = `
<div class="carte-titre">
<strong>${echap(p["Nom du prospect"]) || "<i>Sans nom</i>"}</strong>
${p["Type"] ? `<span class="badge">${echap(p["Type"])}</span>` : ""}
</div>
<div class="carte-infos">
${lieu ? `<span class="sous">📍 ${echap(lieu)}</span>` : ""}
${p["Téléphone"] ? `<span>📞 ${echap(p["Téléphone"])}</span>` : ""}
${p["Email"] ? `<span>✉️ ${echap(p["Email"])}</span>` : ""}
<span class="sous">${contact}</span>
</div>`;
const boutons = document.createElement("div");
boutons.className = "carte-boutons";
if (p["Lien Facebook"]) {
const fb = document.createElement("button");
fb.className = "secondaire";
fb.textContent = "Facebook ↗";
fb.addEventListener("click", (e) => { e.stopPropagation(); window.open(p["Lien Facebook"], "_blank"); });
boutons.append(fb);
}
const suppr = document.createElement("button");
suppr.className = "danger";
suppr.textContent = "✕";
suppr.title = "Supprimer";
suppr.addEventListener("click", async (e) => {
e.stopPropagation();
if (!confirm(`Supprimer « ${p["Nom du prospect"] || "cette ligne"} » ?`)) return;
await fetch(`/api/prospects/${p.index}`, { method: "DELETE" });
chargerListe();
});
boutons.append(suppr);
tete.append(boutons);
const detail = document.createElement("div");
detail.className = "carte-detail";
detail.style.display = "none";
tete.addEventListener("click", () => {
const ouvert = detail.style.display !== "none";
if (!ouvert && !detail.hasChildNodes()) construireDetail(detail, p);
detail.style.display = ouvert ? "none" : "block";
});
div.append(tete, detail);
return div;
}
function construireDetail(detail, p) {
const grille = document.createElement("div");
grille.className = "grille";
for (const col of COLONNES) {
const champ = document.createElement("div");
champ.className = "champ";
const label = document.createElement("label");
label.textContent = col;
const input = document.createElement("input");
input.name = col;
input.value = p[col] || "";
champ.append(label, input);
grille.append(champ);
}
const actions = document.createElement("div");
actions.className = "actions";
const enregistrer = document.createElement("button");
enregistrer.textContent = "Enregistrer";
enregistrer.addEventListener("click", async () => {
const donnees = {};
for (const input of grille.querySelectorAll("input")) donnees[input.name] = input.value;
const rep = await fetch(`/api/prospects/${p.index}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(donnees),
});
if (rep.ok) chargerListe();
});
actions.append(enregistrer);
detail.append(grille, actions);
}
$("recherche").addEventListener("input", afficherListe);
chargerListe();
</script>
</body>
</html>