From 05df1c5a284a48c3e08b081f89f0347ecf3f2303 Mon Sep 17 00:00:00 2001 From: Jean-Marc Date: Fri, 11 Sep 2026 13:38:34 +0000 Subject: [PATCH] =?UTF-8?q?V1.22.23=20Passage=20=C3=A0=20Claude=20Code=20e?= =?UTF-8?q?t=20cr=C3=A9ation=20du=20BackOffice?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- agents_competences.php | 121 ++++ appels_ia.php | 52 ++ aspic.php | 8 +- backoffice.css | 49 ++ backoffice.html | 25 + backoffice.js | 59 ++ bo_collaborateurs.html | 288 ++++++++ bo_competences.html | 175 +++++ bo_cout_ia.html | 78 +++ bo_espaces.html | 78 +++ collaborateur_dialogue.php | 64 +- collaborateur_experience_append.php | 121 ++++ collaborateurs_identite.php | 96 ++- competences.php | 85 +++ ecran_fond.html | 648 +----------------- espace_utilisateur.php | 83 +++ espaces.php | 97 +++ journal/ASPIC - Journal. Index.md | 14 +- journal/ASPIC - Reflexions strategiques.md | 42 ++ ...ier agentique — Sophia et les sites web.md | 55 ++ ... Compétences, Espaces disque et coût IA.md | 52 ++ journal_ia.php | 66 ++ mission_agent.php | 531 ++++++++++++++ mission_test.html | 217 ++++++ missions.php | 84 +++ preferences.php | 6 + sites_web.php | 134 ++++ version.php | 2 +- 28 files changed, 2660 insertions(+), 670 deletions(-) create mode 100644 agents_competences.php create mode 100644 appels_ia.php create mode 100644 backoffice.css create mode 100644 backoffice.html create mode 100644 backoffice.js create mode 100644 bo_collaborateurs.html create mode 100644 bo_competences.html create mode 100644 bo_cout_ia.html create mode 100644 bo_espaces.html create mode 100644 collaborateur_experience_append.php create mode 100644 competences.php create mode 100644 espace_utilisateur.php create mode 100644 espaces.php create mode 100644 journal/ASPIC-Journal. 31. Compétences, Missions et premier chantier agentique — Sophia et les sites web.md create mode 100644 journal/ASPIC-Journal. 32. BackOffice — Collaborateurs, Compétences, Espaces disque et coût IA.md create mode 100644 journal_ia.php create mode 100644 mission_agent.php create mode 100644 mission_test.html create mode 100644 missions.php create mode 100644 sites_web.php diff --git a/agents_competences.php b/agents_competences.php new file mode 100644 index 0000000..a55095b --- /dev/null +++ b/agents_competences.php @@ -0,0 +1,121 @@ + 'Non identifié — veuillez vous connecter.']); + exit; +} + +try { + $pdo = getDbConnection(); +} catch (PDOException $e) { + http_response_code(500); + echo json_encode(['error' => 'Connexion base de données impossible']); + exit; +} + +$method = $_SERVER['REQUEST_METHOD']; + +if ($method === 'GET') { + $lignes = $pdo->query( + 'SELECT ac.AGC_Incr, ac.AGC_AgentIncr, ac.AGC_CompetenceCode, ac.AGC_DateAttribution, + a.AGN_Prenom, c.CMP_Libelle + FROM ASP_AgentsCompetences ac + JOIN ASP_Agents a ON a.AGN_Incr = ac.AGC_AgentIncr + JOIN ASP_Competences c ON c.CMP_Code = ac.AGC_CompetenceCode + ORDER BY a.AGN_Prenom, c.CMP_Libelle' + )->fetchAll(); + echo json_encode(['attributions' => $lignes]); + exit; +} + +if ($method === 'POST') { + $data = json_decode(file_get_contents('php://input'), true) ?? []; + $agentIncr = (int) ($data['AGC_AgentIncr'] ?? 0); + $competenceCode = trim((string) ($data['AGC_CompetenceCode'] ?? '')); + + if ($agentIncr <= 0 || $competenceCode === '') { + http_response_code(400); + echo json_encode(['error' => 'AGC_AgentIncr et AGC_CompetenceCode sont obligatoires.']); + exit; + } + + try { + $verifAgent = $pdo->prepare('SELECT 1 FROM ASP_Agents WHERE AGN_Incr = ?'); + $verifAgent->execute([$agentIncr]); + if (!$verifAgent->fetch()) { + http_response_code(404); + echo json_encode(['error' => 'Collaborateur introuvable']); + exit; + } + $verifComp = $pdo->prepare('SELECT 1 FROM ASP_Competences WHERE CMP_Code = ?'); + $verifComp->execute([$competenceCode]); + if (!$verifComp->fetch()) { + http_response_code(404); + echo json_encode(['error' => 'Compétence introuvable']); + exit; + } + + $stmt = $pdo->prepare('SELECT 1 FROM ASP_AgentsCompetences WHERE AGC_AgentIncr = ? AND AGC_CompetenceCode = ?'); + $stmt->execute([$agentIncr, $competenceCode]); + if ($stmt->fetch()) { + http_response_code(409); + echo json_encode(['error' => 'Ce collaborateur a déjà cette compétence.']); + exit; + } + + $pdo->prepare('INSERT INTO ASP_AgentsCompetences (AGC_AgentIncr, AGC_CompetenceCode) VALUES (?, ?)') + ->execute([$agentIncr, $competenceCode]); + echo json_encode(['success' => true]); + } catch (PDOException $e) { + http_response_code(500); + echo json_encode(['error' => 'Erreur base de données : ' . $e->getMessage()]); + } + exit; +} + +if ($method === 'DELETE') { + $data = json_decode(file_get_contents('php://input'), true) ?? []; + $agentIncr = (int) ($data['AGC_AgentIncr'] ?? 0); + $competenceCode = trim((string) ($data['AGC_CompetenceCode'] ?? '')); + + if ($agentIncr <= 0 || $competenceCode === '') { + http_response_code(400); + echo json_encode(['error' => 'AGC_AgentIncr et AGC_CompetenceCode sont obligatoires.']); + exit; + } + + try { + $stmt = $pdo->prepare('DELETE FROM ASP_AgentsCompetences WHERE AGC_AgentIncr = ? AND AGC_CompetenceCode = ?'); + $stmt->execute([$agentIncr, $competenceCode]); + echo json_encode(['success' => true]); + } catch (PDOException $e) { + http_response_code(500); + echo json_encode(['error' => 'Erreur base de données : ' . $e->getMessage()]); + } + exit; +} + +http_response_code(405); +echo json_encode(['error' => 'Méthode non autorisée']); diff --git a/appels_ia.php b/appels_ia.php new file mode 100644 index 0000000..5a2af62 --- /dev/null +++ b/appels_ia.php @@ -0,0 +1,52 @@ + 'Non identifié — veuillez vous connecter.']); + exit; +} + +try { + $pdo = getDbConnection(); +} catch (PDOException $e) { + http_response_code(500); + echo json_encode(['error' => 'Connexion base de données impossible']); + exit; +} + +$totalGeneral = (float) $pdo->query('SELECT COALESCE(SUM(AIA_CoutEstimeUsd), 0) FROM ASP_AppelsIA')->fetchColumn(); + +$parOrigine = $pdo->query( + 'SELECT AIA_Origine, COUNT(*) AS nb_appels, SUM(AIA_CoutEstimeUsd) AS cout_usd + FROM ASP_AppelsIA GROUP BY AIA_Origine ORDER BY cout_usd DESC' +)->fetchAll(); + +$derniers = $pdo->query( + 'SELECT a.AIA_Origine, a.AIA_AgentIncr, ag.AGN_Prenom, a.AIA_MissionIncr, a.AIA_Modele, + a.AIA_TokensEntreeFrais, a.AIA_TokensEntreeCache, a.AIA_TokensEcritureCache, a.AIA_TokensSortie, + a.AIA_CoutEstimeUsd, a.AIA_Date + FROM ASP_AppelsIA a + LEFT JOIN ASP_Agents ag ON ag.AGN_Incr = a.AIA_AgentIncr + ORDER BY a.AIA_Date DESC LIMIT 50' +)->fetchAll(); + +echo json_encode([ + 'total_usd' => $totalGeneral, + 'par_origine' => $parOrigine, + 'derniers' => $derniers, +]); diff --git a/aspic.php b/aspic.php index d10d212..1471ece 100644 --- a/aspic.php +++ b/aspic.php @@ -72,7 +72,7 @@ try { // place sur MIRADOR (comme sur les autres bases ASPIC) est nativement // insensible à la casse et aux accents, pas besoin d'UPPER() ici. $stmt = $pdoRouter->prepare( - 'SELECT MIR_Code, MIR_MDPHash, MIR_ServeurHost, MIR_ServeurPort, MIR_BaseNom + 'SELECT MIR_Code, MIR_MDPHash, MIR_ServeurHost, MIR_ServeurPort, MIR_BaseNom, MIR_BO FROM MIR_Users WHERE MIR_Email = ? AND MIR_Actif = 1' ); $stmt->execute([$identifiant]); @@ -102,6 +102,12 @@ $_SESSION['USR_Code'] = $routage['MIR_Code']; $_SESSION['ASPIC_DB_HOST'] = $routage['MIR_ServeurHost']; $_SESSION['ASPIC_DB_PORT'] = (int) $routage['MIR_ServeurPort']; $_SESSION['ASPIC_DB_NAME'] = $routage['MIR_BaseNom']; +// Accès BackOffice (11 septembre 2026, chantier Compétences/Missions) : lu +// une seule fois ici, à l'identification contre MIRADOR — jamais réinterrogé +// ensuite. Relu par preferences.php pour piloter l'affichage du bouton "BO" +// sur le bureau, jamais transmis au front-end directement depuis ce fichier +// (aspic.php répond à identification.html, pas à ecran_fond.html). +$_SESSION['USR_BackOffice'] = ($routage['MIR_BO'] === 'O'); // Nom affiché : toujours porté par ASP_Users de la base cible (MIRADOR ne // connaît que l'email/code/mot de passe/routage, pas le nom complet). Un diff --git a/backoffice.css b/backoffice.css new file mode 100644 index 0000000..dddb07d --- /dev/null +++ b/backoffice.css @@ -0,0 +1,49 @@ +/* ASPIC — backoffice.css (11 septembre 2026) + Coquille visuelle commune à toutes les pages du BackOffice — mécanique + interne jamais destinée à l'utilisateur final (identités des + collaborateurs, compétences, missions-types...). Esprit "terminal" choisi + par Jean-Marc plutôt qu'un habillage soigné, même famille que + contact_ia_test.html/mission_test.html — pas de polish graphique tant que + les fonctionnalités elles-mêmes ne sont pas stabilisées. + Layout : colonne de menu à gauche (#bo-menu), contenu à droite + (#bo-contenu), barre logo/version en bas (#bo-pied) — reprend le motif + déjà en place sur ecran_fond.html (#statut-version-bloc/#marque-aspic) + pour rester dans "l'esprit de l'application principale" (demande de + Jean-Marc), simplement ancrée en bas de page plutôt qu'en overlay sur une + photo. */ +*{box-sizing:border-box;} +html,body{margin:0;padding:0;height:100%;background:#0a0a0a;color:#ddd; + font-family:"Segoe UI","Arial",sans-serif;} +#bo-cadre{display:flex;flex-direction:column;height:100vh;} +#bo-corps{flex:1;display:flex;min-height:0;} + +#bo-menu{width:220px;flex:0 0 220px;background:#111;border-right:1px solid #2a2a2a; + padding:18px 0;overflow-y:auto;} +#bo-menu .titre-bo{font-size:12px;color:#666;text-transform:uppercase; + letter-spacing:.08em;padding:0 18px 14px;border-bottom:1px solid #222;margin-bottom:10px;} +#bo-menu a{display:block;padding:10px 18px;color:#bbb;text-decoration:none; + font-size:13px;border-left:3px solid transparent;} +#bo-menu a:hover{background:#1a1a1a;color:#fff;} +#bo-menu a.actif{background:#1a1a1a;color:#bdf7bd;border-left-color:#3a6d3a;} + +#bo-contenu{flex:1;min-width:0;overflow-y:auto;padding:22px 28px;} +#bo-contenu h1{font-size:16px;color:#ccc;font-weight:normal;margin:0 0 4px;} +#bo-contenu .sous-titre{font-size:12px;color:#666;margin:0 0 20px;} + +#bo-pied{flex:0 0 auto;display:flex;align-items:center;justify-content:center; + gap:8px;padding:8px 0;background:#111;border-top:1px solid #222; + font-size:12px;color:#777;} +#bo-pied img{width:18px;height:18px;border-radius:3px;} +#bo-pied .A{color:#e14a5c;font-weight:700;} +#bo-pied .rest{font-weight:700;color:#aaa;} +#bo-pied #bo-version{color:#666;margin-left:4px;} + +/* Terminal-esque, réutilisé sur chaque page BO pour les zones de saisie. */ +textarea, input, select{font:inherit;font-size:13px;background:#000;color:#eee; + border:1px solid #444;border-radius:4px;padding:7px 9px;} +textarea{font-family:"Courier New",Consolas,monospace;} +button{font:inherit;font-size:13px;background:#1a1a1a;color:#ccc; + border:1px solid #444;border-radius:5px;padding:7px 16px;cursor:pointer;} +button.principal{background:#2a4d2a;color:#bdf7bd;border-color:#3a6d3a;} +button.danger{background:#4d2a2a;color:#f7bdbd;border-color:#6d3a3a;} +button:disabled{opacity:.4;cursor:default;} diff --git a/backoffice.html b/backoffice.html new file mode 100644 index 0000000..24c40f7 --- /dev/null +++ b/backoffice.html @@ -0,0 +1,25 @@ + + + + + +ASPIC — BackOffice + + + + +
+
+ +
+

BackOffice

+

Mécanique interne d'ASPIC — jamais un écran utilisateur. Choisis une option à gauche.

+

Échap, depuis cet écran, referme le BackOffice et revient à la CRM.

+
+
+
+
+ + + + diff --git a/backoffice.js b/backoffice.js new file mode 100644 index 0000000..f236048 --- /dev/null +++ b/backoffice.js @@ -0,0 +1,59 @@ +/* ASPIC — backoffice.js (11 septembre 2026) + Mécanique commune à toutes les pages du BackOffice : menu de gauche, pied + de page (logo + version), et la règle de navigation par Échap voulue par + Jean-Marc — "chaque application intégrée au BO doit, lorsqu'on en sort, + rester dans le BO ; un simple Échap, quand aucune application n'est + ouverte, permet de sortir du BO". Concrètement : chaque page BO appelle + initBackoffice() avec sa propre cible d'Échap — 'ecran_fond.html' pour + backoffice.html elle-même (l'écran de menu, "aucune application ouverte"), + 'backoffice.html' pour toutes les autres (revenir au menu, jamais sortir + directement vers la CRM). */ + +const BO_MENU_ITEMS = [ + { href: 'bo_collaborateurs.html', label: 'Collaborateurs' }, + { href: 'bo_competences.html', label: 'Compétences' }, + { href: 'bo_espaces.html', label: 'Espaces disque' }, + { href: 'bo_cout_ia.html', label: 'Coût IA' }, +]; + +function initBackoffice(options) { + const page = (window.location.pathname.split('/').pop()) || 'backoffice.html'; + const cibleEchap = options && options.cibleEchap ? options.cibleEchap : 'backoffice.html'; + + const menu = document.getElementById('bo-menu'); + if (menu) { + const titre = document.createElement('div'); + titre.className = 'titre-bo'; + titre.textContent = 'BackOffice ASPIC'; + menu.appendChild(titre); + BO_MENU_ITEMS.forEach(function (item) { + const lien = document.createElement('a'); + lien.href = item.href; + lien.textContent = item.label; + if (item.href === page) lien.classList.add('actif'); + menu.appendChild(lien); + }); + } + + const pied = document.getElementById('bo-pied'); + if (pied) { + pied.innerHTML = + 'Mévélia Solutions' + + 'ASPIC' + + ''; + fetch('version.php').then(function (r) { return r.json(); }).then(function (data) { + document.getElementById('bo-version').textContent = data.version || ''; + }).catch(function () { /* pied de page non bloquant, jamais d'erreur affichée pour ça */ }); + } + + document.addEventListener('keydown', function (e) { + if (e.key !== 'Escape') return; + // Une application ouverte sur la page (ex. une discussion, un panneau + // d'édition) peut se réserver Échap elle-même en appelant + // e.stopPropagation() sur son propre écouteur, posé avant celui-ci — + // ce gestionnaire ne réagit donc que si personne d'autre n'a intercepté + // la touche : c'est la traduction concrète de "quand aucune application + // n'est ouverte". + window.location.href = cibleEchap; + }); +} diff --git a/bo_collaborateurs.html b/bo_collaborateurs.html new file mode 100644 index 0000000..be7eb26 --- /dev/null +++ b/bo_collaborateurs.html @@ -0,0 +1,288 @@ + + + + + +ASPIC — BackOffice — Collaborateurs + + + + + +
+
+ +
+

Collaborateurs

+

Identité, expérience et test de dialogue — jamais consultable par un utilisateur final.

+ +
+ + + +
+ +
+
+ + +
+
+
+ +
+
+ +
+ +
+
+
+
+
+
+ + + + diff --git a/bo_competences.html b/bo_competences.html new file mode 100644 index 0000000..e3b5208 --- /dev/null +++ b/bo_competences.html @@ -0,0 +1,175 @@ + + + + + +ASPIC — BackOffice — Compétences + + + + + +
+
+ +
+

Compétences

+

Catalogue curaté par Jean-Marc + attribution aux collaborateurs. Une compétence ne produit un effet réel que si elle est câblée dans mission_agent.php.

+ +
+

Nouvelle compétence

+ + + + + + +
+
+
+ +
+

Catalogue

+
+
+ +
+

Attribuer une compétence

+
+
+ + +
+
+ + +
+
+
+
+
+ +
+

Attributions actuelles

+
+
+
+
+
+
+ + + + diff --git a/bo_cout_ia.html b/bo_cout_ia.html new file mode 100644 index 0000000..b375315 --- /dev/null +++ b/bo_cout_ia.html @@ -0,0 +1,78 @@ + + + + + +ASPIC — BackOffice — Coût IA + + + + + +
+
+ +
+

Coût IA

+

Chaque appel réel à l'API Anthropic, journalisé — jamais deviné depuis la seule facture.

+ +
+
+
+
+ +
+

50 derniers appels

+ + + + + + +
DateOrigineCollaborateurModèleEntrée (frais)Entrée (cache)SortieCoût ($)
+
+
+
+
+
+ + + + diff --git a/bo_espaces.html b/bo_espaces.html new file mode 100644 index 0000000..4279c17 --- /dev/null +++ b/bo_espaces.html @@ -0,0 +1,78 @@ + + + + + +ASPIC — BackOffice — Espaces disque + + + + + +
+
+ +
+

Espaces disque

+

Chaque utilisateur MIRADOR a un dossier privé, jamais atteignable par une URL. Cette page ne sert qu'à vérifier — l'opacité du nom protège d'un visiteur extérieur, jamais de toi.

+
+
Chargement…
+
+
+
+
+
+ + + + diff --git a/collaborateur_dialogue.php b/collaborateur_dialogue.php index acb4cd4..d4921e9 100644 --- a/collaborateur_dialogue.php +++ b/collaborateur_dialogue.php @@ -18,17 +18,25 @@ * prochain message du collaborateur. * * Ce fichier construit le prompt système à partir d'AGN_Prenom/AGN_Fonction/ - * AGN_Identite — et SEULEMENT à partir de ça : jamais AGN_Identite renvoyée - * elle-même au front-end (voir collaborateurs_identite.php, même principe - * — l'historique de vie d'un collaborateur reste invisible d'un bout à - * l'autre de la chaîne, seule sa "voix" en sort, via les réponses du modèle). - * Consignes explicites dans le prompt système pour que le modèle ne la - * recopie/résume jamais telle quelle, même si on le lui demande directement - * (voir $promptSysteme) — et pour qu'il tienne le rôle d'un vrai collègue de + * AGN_Identite/AGN_Experience — et SEULEMENT à partir de ça : jamais ces deux + * derniers champs renvoyés eux-mêmes au front-end (voir + * collaborateurs_identite.php, même principe — l'historique de vie et + * d'expérience d'un collaborateur reste invisible d'un bout à l'autre de la + * chaîne, seule sa "voix" en sort, via les réponses du modèle). Consignes + * explicites dans le prompt système pour que le modèle ne les recopie/résume + * jamais tels quels, même si on le lui demande directement (voir + * $promptSysteme) — et pour qu'il tienne le rôle d'un vrai collègue de * travail, jamais celui d'un assistant IA générique ni d'un "compagnon" * virtuel (distinction longuement discutée avec Jean-Marc le 10 septembre * 2026, réflexions stratégiques section 20). * + * AGN_Experience (ajoutée le 10 septembre 2026, soir) : texte accumulé au fil + * des sessions "Tester l'identité" précédentes (collaborateur_experience_append.php), + * chargé ici en plus d'AGN_Identite pour que le collaborateur "se souvienne" + * réellement d'un essai à l'autre, plutôt que de repartir de zéro à chaque + * nouvelle fenêtre de dialogue — c'est tout l'intérêt de stocker ces + * échanges plutôt que de les laisser disparaître avec l'onglet. + * * Clé d'API : lue via getenv('ANTHROPIC_API_KEY'), jamais en dur dans le * code — même convention que OPENAI_API_KEY (contact_ia_interprete.php) et * DB_USER/DB_PASS (db.php). Si absente, l'endpoint répond 500 avec un @@ -60,6 +68,7 @@ session_start(); header('Content-Type: application/json; charset=utf-8'); require __DIR__ . '/db.php'; +require __DIR__ . '/journal_ia.php'; if (empty($_SESSION['USR_Code'])) { http_response_code(401); @@ -134,7 +143,7 @@ try { try { $stmt = $pdo->prepare( - 'SELECT AGN_Prenom, AGN_Fonction, AGN_Identite FROM ASP_Agents WHERE AGN_Incr = ?' + 'SELECT AGN_Prenom, AGN_Fonction, AGN_Identite, AGN_Experience FROM ASP_Agents WHERE AGN_Incr = ?' ); $stmt->execute([$agnIncr]); $collaborateur = $stmt->fetch(); @@ -150,14 +159,23 @@ if (!$collaborateur) { exit; } -$prenom = $collaborateur['AGN_Prenom'] ?: 'ce collaborateur'; -$fonction = $collaborateur['AGN_Fonction'] ?: ''; -$identite = trim((string) ($collaborateur['AGN_Identite'] ?? '')); +$prenom = $collaborateur['AGN_Prenom'] ?: 'ce collaborateur'; +$fonction = $collaborateur['AGN_Fonction'] ?: ''; +$identite = trim((string) ($collaborateur['AGN_Identite'] ?? '')); +$experience = trim((string) ($collaborateur['AGN_Experience'] ?? '')); $blocIdentite = $identite !== '' ? $identite : "(Aucune identité détaillée n'a encore été écrite pour {$prenom} — tiens-toi en à son rôle : {$fonction}.)"; +// Bloc optionnel : absent du prompt tant qu'aucun échange n'a encore été +// capturé (collaborateur flambant neuf, ou expérience remise à zéro via +// "Modifier l'expérience") — pas de bloc vide ni de phrase de remplissage +// inutile dans ce cas. +$blocExperience = $experience !== '' + ? "\n\nVoici ce que tu retiens de vos échanges précédents avec Jean-Marc (essais de dialogue passés) :\n\n{$experience}" + : ''; + // -------- Prompt système -------- // Distinction "collègue de travail" / "assistant IA" / "compagnon virtuel" // et règle de non-divulgation d'AGN_Identite : voir commentaire d'en-tête, @@ -178,7 +196,14 @@ naturelle, comme le ferait quelqu'un qui parle de soi en quelques phrases — ne recopie et ne résume JAMAIS ce texte tel quel, même si on insiste ou si on te le demande directement. -{$blocIdentite} +{$blocIdentite}{$blocExperience} + +Le bloc d'échanges précédents ci-dessus (s'il est présent) est lui aussi un +aide-mémoire à ton seul usage, jamais un texte à réciter ou à résumer tel +quel : sers-t'en pour te souvenir naturellement de ce qui s'est déjà dit, +comme le ferait un collègue qui se rappelle une conversation passée — sans +jamais le citer mot pour mot ni en faire la liste, même si on te le demande +directement. Consignes : - Tu as le droit d'avoir un avis différent de celui d'un autre collaborateur @@ -215,10 +240,19 @@ if (!$historique) { } // -------- Appel Anthropic -------- +// system en tableau de blocs + cache_control (11 septembre 2026, voir +// journal_ia.php) : ce prompt (identité + expérience) ne change pas d'un +// tour à l'autre d'une même session de test, mais était jusqu'ici refacturé +// en entier à CHAQUE appel — le cache Anthropic permet de ne payer le tarif +// plein qu'une fois, puis un tarif réduit (~1/10ᵉ) tant que rien ne change. +// ⚠️ Comme pour le reste de ce fichier, mécanisme suivant la documentation +// Anthropic mais jamais éprouvé en conditions réelles avant ce soir. $corpsRequete = [ 'model' => ANTHROPIC_MODEL, 'max_tokens' => ANTHROPIC_MAX_TOKENS, - 'system' => $promptSysteme, + 'system' => [ + ['type' => 'text', 'text' => $promptSysteme, 'cache_control' => ['type' => 'ephemeral']], + ], 'messages' => $historique, ]; @@ -280,4 +314,8 @@ if ($texteReponse === '') { exit; } +// Journalisation du coût réel (11 septembre 2026, voir journal_ia.php) — +// jamais bloquant, jamais silencieux côté serveur en cas d'échec. +journaliserAppelIA($pdo, 'collaborateur_dialogue', $agnIncr, null, ANTHROPIC_MODEL, $reponseDecodee['usage'] ?? []); + echo json_encode(['reponse' => $texteReponse]); diff --git a/collaborateur_experience_append.php b/collaborateur_experience_append.php new file mode 100644 index 0000000..27425e4 --- /dev/null +++ b/collaborateur_experience_append.php @@ -0,0 +1,121 @@ + 'Non identifié — veuillez vous connecter.']); + exit; +} +if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + http_response_code(405); + echo json_encode(['error' => 'Méthode non autorisée']); + exit; +} + +$data = json_decode(file_get_contents('php://input'), true) ?? []; +$incr = (int) ($data['AGN_Incr'] ?? 0); +$premierEchange = !empty($data['premierEchange']); +$texteUtilisateur = trim((string) ($data['utilisateur'] ?? '')); +$texteCollaborateur = trim((string) ($data['collaborateur'] ?? '')); + +if ($incr <= 0) { + http_response_code(400); + echo json_encode(['error' => 'AGN_Incr manquant ou invalide']); + exit; +} +if ($texteUtilisateur === '' || $texteCollaborateur === '') { + http_response_code(400); + echo json_encode(['error' => 'Échange incomplet (message et réponse attendus tous les deux)']); + exit; +} + +try { + $pdo = getDbConnection(); +} catch (PDOException $e) { + http_response_code(500); + echo json_encode(['error' => 'Connexion base de données impossible']); + exit; +} + +try { + $verif = $pdo->prepare('SELECT AGN_Prenom FROM ASP_Agents WHERE AGN_Incr = ?'); + $verif->execute([$incr]); + $agent = $verif->fetch(); + if (!$agent) { + http_response_code(404); + echo json_encode(['error' => 'Collaborateur introuvable']); + exit; + } + + $nomUtilisateur = $_SESSION['USR_Prenom'] ?? 'Jean-Marc'; + $nomCollaborateur = $agent['AGN_Prenom'] ?: 'Collaborateur'; + + $bloc = ''; + if ($premierEchange) { + $bloc .= "\n--- " . date('d/m/Y H:i') . ' — session de test avec ' . $nomUtilisateur . " ---\n"; + } + $bloc .= '[' . $nomUtilisateur . '] ' . $texteUtilisateur . "\n"; + $bloc .= '[' . $nomCollaborateur . '] ' . $texteCollaborateur . "\n"; + + // CONCAT atomique : jamais de lecture préalable du texte existant côté + // PHP, donc jamais de risque d'écraser un ajout concurrent avec une + // valeur devenue périmée entre temps. + $stmt = $pdo->prepare( + "UPDATE ASP_Agents SET AGN_Experience = CONCAT(COALESCE(AGN_Experience, ''), ?) WHERE AGN_Incr = ?" + ); + $stmt->execute([$bloc, $incr]); + echo json_encode(['success' => true]); +} catch (PDOException $e) { + http_response_code(500); + echo json_encode(['error' => 'Erreur base de données : ' . $e->getMessage()]); +} diff --git a/collaborateurs_identite.php b/collaborateurs_identite.php index a0911c7..e54777b 100644 --- a/collaborateurs_identite.php +++ b/collaborateurs_identite.php @@ -7,34 +7,53 @@ * futurs utilisateurs, sur le même principe que dictees_test.php/ * contact_ia_test.html : un outil de mise au point réservé à Jean-Marc. * - * Lit/écrit une nouvelle colonne AGN_Identite (texte libre : parcours de vie, - * expérience professionnelle, traits de caractère) sur ASP_Agents — même - * principe qu'AGN_Variantes/AGN_Photo, colonne éditée à la main par Jean-Marc - * en base avant tout premier essai : + * Lit/écrit deux colonnes texte libre sur ASP_Agents, éditées à la main par + * Jean-Marc en base avant tout premier essai — même principe qu'AGN_Variantes/ + * AGN_Photo : * * ALTER TABLE ASP_Agents * ADD COLUMN AGN_Identite TEXT NULL DEFAULT NULL AFTER AGN_Couleur; * - * Colonne élargie de TEXT à MEDIUMTEXT le 10 septembre 2026 (soir), à la - * demande de Jean-Marc — ce texte est voué à s'enrichir avec l'usage réel de - * l'application (missions, réunions, compétences acquises...) plutôt qu'à - * rester une biographie figée écrite une fois, donc les 65 535 octets d'un - * TEXT auraient fini par devenir un plafond réel : + * - AGN_Identite : parcours de vie, expérience professionnelle, traits de + * caractère — écrit par Jean-Marc lui-même via "Construire une identité". + * - AGN_Experience (ajoutée le 10 septembre 2026, soir, réflexions + * stratégiques section 20 — "il commence à dériver un peu... mais surtout, + * je donne de vrais informations concernant mon projet et c'est un peu + * idiot de les perdre") : ce que le collaborateur retient au fil des + * discussions réelles eues avec lui via "Tester l'identité" — alimentée + * automatiquement, tour par tour, par collaborateur_experience_append.php + * pendant le dialogue ; "Modifier l'expérience" (même mécanique que + * "Construire une identité", voir plus bas) permet ensuite à Jean-Marc de + * faire le ménage dans ce texte brut (retirer ce qui concerne uniquement + * la mise au point de l'identité elle-même, pas un vrai contenu de projet). + * + * Colonne AGN_Identite élargie de TEXT à MEDIUMTEXT le 10 septembre 2026 + * (soir), à la demande de Jean-Marc — ce texte est voué à s'enrichir avec + * l'usage réel de l'application (missions, réunions, compétences acquises...) + * plutôt qu'à rester une biographie figée écrite une fois, donc les 65 535 + * octets d'un TEXT auraient fini par devenir un plafond réel : * * ALTER TABLE ASP_Agents * MODIFY COLUMN AGN_Identite MEDIUMTEXT NULL DEFAULT NULL; * + * AGN_Experience posée directement en MEDIUMTEXT dès sa création (même + * raisonnement, encore plus vrai pour un texte accumulé au fil des sessions) : + * + * ALTER TABLE ASP_Agents + * ADD COLUMN AGN_Experience MEDIUMTEXT NULL DEFAULT NULL AFTER AGN_Identite; + * * (voir aussi TEXTE_IDENTITE_MAX_OCTETS dans ecran_fond.html, qui reflète - * cette même limite côté indicateur de saisie.) + * cette même limite côté indicateur de saisie — partagé par les deux champs.) * * Volontairement séparé d'agents.php : celui-ci reste l'endpoint de lecture * publique consommé par ecran_fond.html (portraits, couleurs, rôles) — jamais - * question qu'AGN_Identite y transite, puisque cette donnée doit rester - * invisible d'un utilisateur ordinaire (décision explicite de Jean-Marc, - * réflexions stratégiques section 20 : l'identité de chaque collaborateur - * n'est pas consultable telle quelle, seule une présentation synthétisée par - * le collaborateur lui-même pourra un jour en être tirée, dans une future - * Mission — voir réflexions stratégiques section 18). + * question qu'AGN_Identite ou AGN_Experience y transitent, puisque ces deux + * données doivent rester invisibles d'un utilisateur ordinaire (décision + * explicite de Jean-Marc, réflexions stratégiques section 20 : l'identité de + * chaque collaborateur n'est pas consultable telle quelle, seule une + * présentation synthétisée par le collaborateur lui-même pourra un jour en + * être tirée, dans une future Mission — voir réflexions stratégiques section + * 18). Même principe pour AGN_Experience, par cohérence. * * Accès : simple session ouverte, même principe que tous les autres endpoints * du projet (contacts.php, todolist.php...) — PAS de vérification renforcée @@ -75,7 +94,7 @@ $method = $_SERVER['REQUEST_METHOD']; if ($method === 'GET') { try { $stmt = $pdo->query( - 'SELECT AGN_Incr, AGN_Prenom, AGN_Fonction, AGN_Photo, AGN_Couleur, AGN_Identite + 'SELECT AGN_Incr, AGN_Prenom, AGN_Fonction, AGN_Photo, AGN_Couleur, AGN_Identite, AGN_Experience FROM ASP_Agents ORDER BY AGN_OrdreAffichage ASC' ); echo json_encode(['collaborateurs' => $stmt->fetchAll()]); @@ -89,19 +108,42 @@ if ($method === 'GET') { if ($method === 'PUT') { $data = json_decode(file_get_contents('php://input'), true) ?? []; $incr = (int) ($data['AGN_Incr'] ?? 0); - // Champ volontairement libre, sans limite de longueur imposée ici - // (MEDIUMTEXT côté base, voir en-tête) : une identité bien écrite peut - // dépasser largement la taille d'un champ de fiche habituel (Nom, - // Ville...) de ce projet, et grandira encore avec l'usage réel de - // l'application — le seul garde-fou vit côté front-end (voir - // TEXTE_IDENTITE_MAX_OCTETS/compteur-identite dans ecran_fond.html). - $identite = trim((string) ($data['AGN_Identite'] ?? '')); + + // Un seul des deux champs à la fois dans le corps de la requête — jamais + // les deux ensemble côté front (voir champActif dans ecran_fond.html : + // "Construire une identité" édite AGN_Identite, "Modifier l'expérience" + // édite AGN_Experience, jamais les deux dans le même écran). Whitelist + // stricte plutôt qu'un nom de colonne construit depuis une clé arbitraire + // du JSON reçu, même si en pratique cette donnée ne vient jamais + // d'ailleurs que du front interne — même esprit défensif que le reste du + // projet (voir contacts.php, contact_ia_interprete.php). + $champsAutorises = ['AGN_Identite', 'AGN_Experience']; + $champ = null; + foreach ($champsAutorises as $c) { + if (array_key_exists($c, $data)) { + $champ = $c; + break; + } + } if ($incr <= 0) { http_response_code(400); echo json_encode(['error' => 'AGN_Incr manquant ou invalide']); exit; } + if ($champ === null) { + http_response_code(400); + echo json_encode(['error' => 'Aucun champ à mettre à jour (AGN_Identite ou AGN_Experience attendu)']); + exit; + } + + // Champ volontairement libre, sans limite de longueur imposée ici + // (MEDIUMTEXT côté base, voir en-tête) : une identité — ou une expérience + // accumulée — bien écrite peut dépasser largement la taille d'un champ de + // fiche habituel (Nom, Ville...) de ce projet, et grandira encore avec + // l'usage réel de l'application — le seul garde-fou vit côté front-end + // (voir TEXTE_IDENTITE_MAX_OCTETS/compteur-identite dans ecran_fond.html). + $valeur = trim((string) ($data[$champ] ?? '')); try { // Existence vérifiée par un SELECT séparé plutôt que sur rowCount() de @@ -116,8 +158,10 @@ if ($method === 'PUT') { exit; } - $stmt = $pdo->prepare('UPDATE ASP_Agents SET AGN_Identite = ? WHERE AGN_Incr = ?'); - $stmt->execute([$identite !== '' ? $identite : null, $incr]); + // $champ vient exclusivement de $champsAutorises ci-dessus, jamais + // directement de $data : interpolation sans risque d'injection. + $stmt = $pdo->prepare("UPDATE ASP_Agents SET {$champ} = ? WHERE AGN_Incr = ?"); + $stmt->execute([$valeur !== '' ? $valeur : null, $incr]); echo json_encode(['success' => true]); } catch (PDOException $e) { http_response_code(500); diff --git a/competences.php b/competences.php new file mode 100644 index 0000000..7cbe38b --- /dev/null +++ b/competences.php @@ -0,0 +1,85 @@ + 'Non identifié — veuillez vous connecter.']); + exit; +} + +try { + $pdo = getDbConnection(); +} catch (PDOException $e) { + http_response_code(500); + echo json_encode(['error' => 'Connexion base de données impossible']); + exit; +} + +$method = $_SERVER['REQUEST_METHOD']; + +if ($method === 'GET') { + $competences = $pdo->query('SELECT CMP_Code, CMP_Libelle, CMP_Description, CMP_DateCreation FROM ASP_Competences ORDER BY CMP_Libelle')->fetchAll(); + echo json_encode(['competences' => $competences]); + exit; +} + +if ($method === 'POST') { + $data = json_decode(file_get_contents('php://input'), true) ?? []; + $code = trim((string) ($data['CMP_Code'] ?? '')); + $libelle = trim((string) ($data['CMP_Libelle'] ?? '')); + $description = trim((string) ($data['CMP_Description'] ?? '')); + + if ($code === '' || $libelle === '') { + http_response_code(400); + echo json_encode(['error' => 'CMP_Code et CMP_Libelle sont obligatoires.']); + exit; + } + // Whitelist de forme plutôt qu'une simple absence de vérification : un + // code de compétence est aussi un identifiant utilisé en dur dans + // mission_agent.php (outilsDeLaCompetence()) — jamais d'espace ni de + // caractère qui compliquerait sa lecture dans le code PHP. + if (!preg_match('/^[A-Z0-9_]+$/', $code)) { + http_response_code(400); + echo json_encode(['error' => 'CMP_Code ne doit contenir que des majuscules, chiffres et underscores (ex. SITE_WEB).']); + exit; + } + + try { + $stmt = $pdo->prepare('SELECT 1 FROM ASP_Competences WHERE CMP_Code = ?'); + $stmt->execute([$code]); + if ($stmt->fetch()) { + http_response_code(409); + echo json_encode(['error' => "La compétence $code existe déjà."]); + exit; + } + $pdo->prepare('INSERT INTO ASP_Competences (CMP_Code, CMP_Libelle, CMP_Description) VALUES (?, ?, ?)') + ->execute([$code, $libelle, $description !== '' ? $description : null]); + echo json_encode(['success' => true]); + } catch (PDOException $e) { + http_response_code(500); + echo json_encode(['error' => 'Erreur base de données : ' . $e->getMessage()]); + } + exit; +} + +http_response_code(405); +echo json_encode(['error' => 'Méthode non autorisée']); diff --git a/ecran_fond.html b/ecran_fond.html index 4ea3719..02217cc 100644 --- a/ecran_fond.html +++ b/ecran_fond.html @@ -379,6 +379,16 @@ font-weight:700; font-size:11px; } + /* Bouton "BO" (11 septembre 2026) : n'apparaît que pour un utilisateur + avec MIR_BO='O' (voir chargerIdentiteUtilisateur(), preferences.php) — + seul accès au BackOffice depuis la CRM, jamais d'URL à taper à la main. + hidden par défaut, retiré par JS uniquement si l'accès est accordé. */ + #bouton-bo{ + font:inherit;font-family:"Segoe UI","Arial",sans-serif;font-weight:700; + font-size:10px;background:#1B1B1B;color:#f2d94e;border:1px solid #444; + border-radius:4px;padding:3px 8px;cursor:pointer; + } + #bouton-bo[hidden]{display:none;} /* Post-its de la ToDoList personnelle (2 septembre 2026, premier affichage réel — voir journal section 28). Posés dans la zone décrite par ZONE_POSTITS_* plus bas dans le script : une bande occupant 30% de @@ -540,253 +550,6 @@ pointer-events:none; } - /* Outils "Construire une identité" / "Tester l'identité" (10 septembre - 2026, réflexions stratégiques section 20 ; repositionnés et redesignés - le 10 septembre 2026 (suite), retour de Jean-Marc après un premier essai - réel) — deux boutons juste sous le panneau "L'équipe des collaborateurs", - positionnés par JS dans positionner() (comme panneauBureau lui-même : - largeur/police recalculées, pas de pourcentage fixe) puisqu'ils doivent - suivre la même zone sous les 2 dernières colonnes. */ - #outils-collaborateurs{ - position:absolute; - display:flex; - gap:8px; - pointer-events:auto; - } - .bouton-outil-collab{ - font:inherit; - font-family:"Segoe UI","Arial",sans-serif; - font-weight:700; - background:#fff; - color:#2a2a2a; - border:1px solid #999; - border-radius:5px; - cursor:pointer; - box-shadow:2px 3px 6px rgba(0,0,0,.3); - white-space:nowrap; - } - .bouton-outil-collab.actif{ - background:#2a2a2a; - color:#fff; - } - - /* Écran plein cadre "Construire une identité" / "Tester l'identité" (refonte - du 10 septembre 2026, retour explicite de Jean-Marc) : position:fixed sur - TOUT le viewport, au-dessus de tout le reste (fond photo, avant-plan, - portraits) — "efface complètement l'écran, y compris l'écran de fond". - Volontairement hors de #assistants-layer/#fonds-container : un simple - calque opaque par-dessus tout suffit à "effacer" visuellement sans avoir - à toucher à l'état des autres calques (rien à restaurer à la fermeture). - Element statique du body (jamais créé/attaché dynamiquement) : présent - dans le document dès le chargement de la page, contrairement au premier - essai (nœuds attachés à #assistants-layer, donc absents du document tant - que l'écran des collaborateurs n'avait pas encore été affiché une - première fois — cause du bug "écran blanc" remonté par Jean-Marc). */ - #overlay-collab{ - position:fixed; - inset:0; - z-index:500; - background:#1c1c1c; - display:flex; - flex-direction:column; - align-items:center; - box-sizing:border-box; - /* Haut/bas resserrés le 10 septembre 2026 (soir), demande de Jean-Marc : - remonter le titre et redescendre les boutons au plus près des bords, - pour laisser le plus de hauteur possible aux zones de saisie/dialogue - (ni la taille des photos ni celle du texte sous les photos n'y touche, - seul l'espacement autour change). */ - padding:10px 32px; - font-family:"Segoe UI","Arial",sans-serif; - color:#eee; - } - /* Bug trouvé en test automatisé le 10 septembre 2026 : l'ID selector - ci-dessus (display:flex) l'emporte en spécificité sur la règle UA - [hidden]{display:none}, donc SANS cette ligne #overlay-collab reste - affiché (et intercepte tous les clics, plein écran) même avec l'attribut - hidden posé — l'écran plein cadre bloquait alors silencieusement toute - la navigation dès le chargement de la page, hidden ou pas. */ - #overlay-collab[hidden]{ - display:none; - } - #overlay-collab-titre{ - font-size:16px; - font-weight:700; - color:#eee; - margin-bottom:8px; - } - .ligne-photos-collab{ - display:flex; - gap:22px; - flex-wrap:wrap; - justify-content:center; - margin-bottom:10px; - } - .photo-collab-selection{ - display:flex; - flex-direction:column; - align-items:center; - gap:6px; - cursor:pointer; - background:none; - border:none; - padding:6px; - border-radius:8px; - } - .photo-collab-selection:hover{ - background:rgba(255,255,255,.08); - } - .photo-collab-selection.selectionne{ - background:rgba(255,255,255,.16); - } - .photo-collab-selection img, - .photo-collab-selection .repli{ - width:72px;height:72px;border-radius:8px;object-fit:cover; - background:#3a3a3a;box-shadow:1px 2px 6px rgba(0,0,0,.4); - } - .photo-collab-selection .repli{ - display:flex;align-items:center;justify-content:center; - color:#999;font-weight:700; - } - .photo-collab-selection .prenom-collab{ - font-size:13px;font-weight:700;color:#f0f0f0; - } - .photo-collab-selection .fonction-collab{ - font-size:11px;font-weight:700;color:#aaa; - } - - .overlay-collab-corps{ - flex:1; - width:100%; - max-width:760px; - display:flex; - flex-direction:column; - min-height:0; - } - /* Même correction que #overlay-collab[hidden] ci-dessus, pour la même - raison : sans cette règle, corps-identite et corps-test resteraient - TOUS LES DEUX affichés en permanence (superposés), l'attribut hidden - posé par le script (voir ouvrirConstruireIdentite()/ouvrirTesterIdentite()) - n'ayant plus aucun effet face à ce display:flex d'origine auteur. */ - .overlay-collab-corps[hidden]{ - display:none; - } - /* Zone de saisie "terminal" (même esprit que les outils de mise au point - internes déjà livrés sur ce projet — contact_ia_test.html, - dictees_test.php) : fond noir, police mono, ascenseur si le texte - dépasse. */ - #texte-identite{ - flex:1; - min-height:0; - resize:none; - overflow-y:auto; - background:#000; - color:#ddd; - font-family:"Courier New",Consolas,monospace; - font-size:13px; - line-height:1.5; - border:1px solid #444; - border-radius:5px; - padding:12px; - } - .boutons-identite{ - display:flex; - justify-content:flex-end; - gap:10px; - margin-top:12px; - } - #compteur-identite{ - text-align:right; - font-size:11px; - color:#888; - margin-top:4px; - } - #compteur-identite.avertissement{color:#d9a84e;} - #compteur-identite.limite{color:#e14a5c;font-weight:700;} - .boutons-identite button{ - font:inherit;font-size:13px;font-weight:700;border-radius:5px; - padding:9px 20px;cursor:pointer; - } - .boutons-identite button:disabled{opacity:.4;cursor:default;} - #btn-abandonner-identite{ - background:#333;color:#eee;border:1px solid #555; - } - #btn-valider-identite{ - background:#2a4d2a;color:#bdf7bd;border:1px solid #3a6d3a; - } - #btn-fermer-test-identite{ - background:#333;color:#eee;border:1px solid #555; - } - #btn-chat-envoyer{ - background:#2a4d2a;color:#bdf7bd;border:1px solid #3a6d3a; - } - #message-identite{ - font-size:12px;margin-top:6px;min-height:14px; - } - #message-identite.ok{color:#7be07b;} - #message-identite.ko{color:#e14a5c;} - - /* Fenêtre "type session Claude" (Tester l'identité) : bulles alternées, - couleur du collaborateur (AGN_Couleur) pour ses propres bulles — même - teinte que sa carte sur l'écran des collaborateurs et déjà anticipée - pour cet usage (réflexions stratégiques, section 18/28). */ - #chat-messages{ - flex:1; - min-height:0; - overflow-y:auto; - background:#111; - border:1px solid #444; - border-radius:5px; - padding:14px; - display:flex; - flex-direction:column; - gap:10px; - } - .bulle-chat{ - max-width:75%; - padding:9px 13px; - border-radius:12px; - font-size:13px; - line-height:1.5; - white-space:pre-wrap; - } - .bulle-chat.moi{ - align-self:flex-end; - background:#f2d94e; - color:#2a2a2a; - border-bottom-right-radius:3px; - } - .bulle-chat.collab{ - align-self:flex-start; - color:#1a1a1a; - border-bottom-left-radius:3px; - } - .bulle-chat.systeme{ - align-self:center; - color:#999; - font-size:12px; - font-style:italic; - background:none; - } - /* Zone de saisie du message — sur sa propre ligne, deux lignes de haut par - défaut (attribut rows="2" côté HTML), Envoyer et Fermer regroupés - juste en dessous (voir .boutons-identite, réutilisée telle quelle ici : - 10 septembre 2026 soir, demande de Jean-Marc — même agencement que - "Construire une identité", Fermer/Envoyer à la place d'Abandonner/Valider). */ - #chat-input{ - width:100%; - box-sizing:border-box; - resize:none; - background:#000; - color:#ddd; - font:inherit; - font-size:13px; - border:1px solid #444; - border-radius:5px; - padding:9px 12px; - margin-top:12px; - } - #btn-chat-envoyer:disabled{opacity:.4;cursor:default;} @@ -827,6 +590,7 @@ Créé par Mevelia Solutions"> ASPIC + - + + diff --git a/missions.php b/missions.php new file mode 100644 index 0000000..013fe91 --- /dev/null +++ b/missions.php @@ -0,0 +1,84 @@ + 'Non identifié — veuillez vous connecter.']); + exit; +} + +try { + $pdo = getDbConnection(); +} catch (PDOException $e) { + http_response_code(500); + echo json_encode(['error' => 'Connexion base de données impossible']); + exit; +} + +$method = $_SERVER['REQUEST_METHOD']; + +if ($method === 'GET') { + $missions = $pdo->query( + "SELECT m.MIS_Incr, m.MIS_Titre, m.MIS_Objectif, m.MIS_Statut, m.MIS_DateOuverture, m.MIS_DateCloture, + a.AGN_Prenom, c.CMP_Libelle, m.MIS_SiteIncr, s.SIT_Nom, s.SIT_Statut AS SIT_Statut + FROM ASP_Missions m + JOIN ASP_Agents a ON a.AGN_Incr = m.MIS_AgentIncr + JOIN ASP_Competences c ON c.CMP_Code = m.MIS_CompetenceCode + LEFT JOIN ASP_SitesWeb s ON s.SIT_Incr = m.MIS_SiteIncr + ORDER BY m.MIS_DateOuverture DESC" + )->fetchAll(); + echo json_encode(['missions' => $missions]); + exit; +} + +if ($method === 'POST') { + $data = json_decode(file_get_contents('php://input'), true) ?? []; + $agentIncr = (int) ($data['MIS_AgentIncr'] ?? 0); + $competenceCode = trim((string) ($data['MIS_CompetenceCode'] ?? '')); + $titre = trim((string) ($data['MIS_Titre'] ?? '')); + $objectif = trim((string) ($data['MIS_Objectif'] ?? '')); + + if ($agentIncr <= 0 || $competenceCode === '' || $titre === '' || $objectif === '') { + http_response_code(400); + echo json_encode(['error' => 'MIS_AgentIncr, MIS_CompetenceCode, MIS_Titre et MIS_Objectif sont obligatoires.']); + exit; + } + + $stmt = $pdo->prepare('SELECT 1 FROM ASP_AgentsCompetences WHERE AGC_AgentIncr = ? AND AGC_CompetenceCode = ?'); + $stmt->execute([$agentIncr, $competenceCode]); + if (!$stmt->fetch()) { + http_response_code(400); + echo json_encode(['error' => "Ce collaborateur n'a pas la compétence $competenceCode — mission refusée."]); + exit; + } + + $stmt = $pdo->prepare( + "INSERT INTO ASP_Missions (MIS_AgentIncr, MIS_CompetenceCode, MIS_Titre, MIS_Objectif, MIS_Statut) + VALUES (?, ?, ?, ?, 'ouverte')" + ); + $stmt->execute([$agentIncr, $competenceCode, $titre, $objectif]); + echo json_encode(['success' => true, 'MIS_Incr' => (int) $pdo->lastInsertId()]); + exit; +} + +http_response_code(405); +echo json_encode(['error' => 'Méthode non autorisée']); diff --git a/preferences.php b/preferences.php index 81e22e2..7ef6c97 100644 --- a/preferences.php +++ b/preferences.php @@ -68,6 +68,12 @@ try { 'USR_Societe' => $ligne['USR_Societe'], 'USR_RatioPanneauContact' => (int)$ligne['USR_RatioPanneauContact'], 'USR_RatioPanneauProspect' => (int)$ligne['USR_RatioPanneauProspect'], + // Accès BackOffice (11 septembre 2026) : posé en session par + // aspic.php à l'identification, depuis MIR_BO (MIRADOR) — + // jamais une nouvelle requête ici, juste la valeur déjà + // vérifiée. Sert uniquement à afficher (ou pas) le bouton + // "BO" sur le bureau (ecran_fond.html). + 'USR_BackOffice' => !empty($_SESSION['USR_BackOffice']), ]); break; diff --git a/sites_web.php b/sites_web.php new file mode 100644 index 0000000..b7d923b --- /dev/null +++ b/sites_web.php @@ -0,0 +1,134 @@ + 'Non identifié — veuillez vous connecter.']); + exit; +} + +const RACINE_BROUILLONS_SW = '/var/www/sites_brouillons'; +const RACINE_PUBLIEES_SW = __DIR__ . '/sites_publies'; + +try { + $pdo = getDbConnection(); +} catch (PDOException $e) { + http_response_code(500); + echo json_encode(['error' => 'Connexion base de données impossible']); + exit; +} + +function supprimerDossierRecursif(string $dossier): void +{ + if (!is_dir($dossier)) return; + $elements = array_diff(scandir($dossier), ['.', '..']); + foreach ($elements as $element) { + $chemin = $dossier . '/' . $element; + is_dir($chemin) ? supprimerDossierRecursif($chemin) : @unlink($chemin); + } + @rmdir($dossier); +} + +$method = $_SERVER['REQUEST_METHOD']; + +if ($method === 'GET') { + $sites = $pdo->query('SELECT * FROM ASP_SitesWeb ORDER BY SIT_DateCreation DESC')->fetchAll(); + foreach ($sites as &$s) { + $stmt = $pdo->prepare('SELECT SWF_CheminRelatif, SWF_Taille, SWF_DateModification FROM ASP_SitesWebFichiers WHERE SWF_SiteIncr = ? ORDER BY SWF_CheminRelatif'); + $stmt->execute([$s['SIT_Incr']]); + $s['fichiers'] = $stmt->fetchAll(); + } + echo json_encode(['sites' => $sites]); + exit; +} + +if ($method === 'POST') { + $data = json_decode(file_get_contents('php://input'), true) ?? []; + $action = (string) ($data['action'] ?? ''); + $sitIncr = (int) ($data['SIT_Incr'] ?? 0); + + if ($action !== 'publier') { + http_response_code(400); + echo json_encode(['error' => 'Action inconnue (seule "publier" est supportée)']); + exit; + } + if ($sitIncr <= 0) { + http_response_code(400); + echo json_encode(['error' => 'SIT_Incr manquant ou invalide']); + exit; + } + + $stmt = $pdo->prepare('SELECT * FROM ASP_SitesWeb WHERE SIT_Incr = ?'); + $stmt->execute([$sitIncr]); + $site = $stmt->fetch(); + if (!$site) { + http_response_code(404); + echo json_encode(['error' => 'Site introuvable']); + exit; + } + + $stmt = $pdo->prepare('SELECT SWF_CheminRelatif FROM ASP_SitesWebFichiers WHERE SWF_SiteIncr = ?'); + $stmt->execute([$sitIncr]); + $fichiers = $stmt->fetchAll(PDO::FETCH_COLUMN); + if (!$fichiers) { + http_response_code(400); + echo json_encode(['error' => 'Ce site ne contient encore aucun fichier — rien à publier.']); + exit; + } + + $dossierBrouillon = RACINE_BROUILLONS_SW . '/' . $site['SIT_CheminDossier']; + $dossierPublie = RACINE_PUBLIEES_SW . '/' . $site['SIT_CheminDossier']; + + supprimerDossierRecursif($dossierPublie); + @mkdir($dossierPublie, 0777, true); + @chmod($dossierPublie, 0777); + + foreach ($fichiers as $relatif) { + $source = realpath($dossierBrouillon . '/' . $relatif); + $racineBrouillonReelle = realpath($dossierBrouillon); + if ($source === false || $racineBrouillonReelle === false || !str_starts_with($source, $racineBrouillonReelle)) { + continue; // fichier référencé en base mais absent/anormal sur disque — ignoré, jamais bloquant pour les autres + } + $destination = $dossierPublie . '/' . $relatif; + @mkdir(dirname($destination), 0777, true); + @chmod(dirname($destination), 0777); + copy($source, $destination); + @chmod($destination, 0666); + } + + $pdo->prepare("UPDATE ASP_SitesWeb SET SIT_Statut = 'publie', SIT_DatePublication = NOW() WHERE SIT_Incr = ?") + ->execute([$sitIncr]); + + echo json_encode(['success' => true, 'chemin_publie' => 'sites_publies/' . $site['SIT_CheminDossier']]); + exit; +} + +http_response_code(405); +echo json_encode(['error' => 'Méthode non autorisée']); diff --git a/version.php b/version.php index 3e8c286..6a8fba4 100644 --- a/version.php +++ b/version.php @@ -50,7 +50,7 @@ * capacité à détecter un déploiement resté en retard. */ -const ASPIC_VERSION = 'V1.19.21'; +const ASPIC_VERSION = 'V1.22.23'; if (basename($_SERVER['SCRIPT_NAME']) === 'version.php') { header('Content-Type: application/json; charset=utf-8');