File: /homepages/35/d993672390/htdocs/dermiteseborrheique/includes/triggers.php
<?php
/**
* triggers.php — Gestion des déclencheurs connus
*
* - Récupération des déclencheurs pour affichage
* - Croisement avec les ingrédients d'un produit
* - Liaison items ↔ known_triggers
* - Stats communautaires
*/
declare(strict_types=1);
if (!defined('DS_APP')) die('Accès direct interdit.');
class Triggers
{
// ── Labels UI ─────────────────────────────────────────────────────────────
public static function typeLabel(string $type, string $lang = 'fr'): string
{
return match($type) {
'scientific' => $lang === 'fr' ? '🔬 Confirmé scientifiquement' : '🔬 Scientifically confirmed',
'eu_regulated' => $lang === 'fr' ? '🇪🇺 Allergène réglementé EU' : '🇪🇺 EU regulated allergen',
'community' => $lang === 'fr' ? '👥 Signalé par la communauté' : '👥 Community reported',
default => $type,
};
}
public static function severityLabel(string $severity, string $lang = 'fr'): string
{
return match($severity) {
'high' => $lang === 'fr' ? 'Élevé' : 'High',
'medium' => $lang === 'fr' ? 'Modéré' : 'Moderate',
'low' => $lang === 'fr' ? 'Faible' : 'Low',
default => $severity,
};
}
public static function categoryLabel(string $cat, string $lang = 'fr'): string
{
return match($cat) {
'food' => $lang === 'fr' ? 'Alimentation' : 'Food',
'cosmetic_ingredient' => $lang === 'fr' ? 'Ingrédient cosmétique' : 'Cosmetic ingredient',
'environmental' => $lang === 'fr' ? 'Facteur environnemental' : 'Environmental',
'medication' => $lang === 'fr' ? 'Médicament' : 'Medication',
'lifestyle' => $lang === 'fr' ? 'Mode de vie' : 'Lifestyle',
default => $cat,
};
}
// ── Récupération ──────────────────────────────────────────────────────────
/**
* Tous les déclencheurs actifs, groupés par catégorie
*/
public static function getAll(bool $dsSpecificOnly = false): array
{
$where = $dsSpecificOnly ? 'AND ds_specific = 1' : '';
$rows = db_fetch_all(
"SELECT * FROM known_triggers
WHERE is_active = 1 {$where}
ORDER BY category, severity DESC, name_fr"
);
$grouped = [];
foreach ($rows as $row) {
$grouped[$row['category']][] = $row;
}
return $grouped;
}
/**
* Déclencheurs pour une catégorie donnée
*/
public static function getByCategory(string $category): array
{
return db_fetch_all(
"SELECT * FROM known_triggers
WHERE category = ? AND is_active = 1
ORDER BY severity DESC, name_fr",
[$category]
);
}
/**
* Cherche un déclencheur par son nom INCI (pour le matching produit)
*/
public static function findByInci(string $inciName): ?array
{
$inciLower = mb_strtolower(trim($inciName));
return db_fetch(
"SELECT * FROM known_triggers
WHERE LOWER(inci_name) = ? AND is_active = 1
LIMIT 1",
[$inciLower]
);
}
/**
* Recherche fulltext dans les déclencheurs
*/
public static function search(string $query, string $lang = 'fr'): array
{
$nameCol = $lang === 'fr' ? 'name_fr' : 'name_en';
return db_fetch_all(
"SELECT * FROM known_triggers
WHERE is_active = 1
AND ({$nameCol} LIKE ? OR inci_name LIKE ? OR description_fr LIKE ?)
ORDER BY ds_specific DESC, severity DESC
LIMIT 20",
["%{$query}%", "%{$query}%", "%{$query}%"]
);
}
// ── Croisement produit ↔ déclencheurs ─────────────────────────────────────
/**
* Analyse les ingrédients d'un produit et retourne les déclencheurs connus détectés
* Crée aussi les entrées dans item_triggers
*/
public static function matchItemTriggers(int $itemId): array
{
$item = db_fetch("SELECT * FROM items WHERE id = ?", [$itemId]);
if (!$item) return [];
$found = [];
// Récupérer tous les déclencheurs avec INCI
$knownWithInci = db_fetch_all(
"SELECT * FROM known_triggers WHERE inci_name IS NOT NULL AND is_active = 1"
);
$ingredientsLower = mb_strtolower($item['ingredients'] ?? '');
// Ingrédients parsés (plus fiables)
$parsed = [];
if (!empty($item['ingredients_parsed'])) {
$parsedData = is_string($item['ingredients_parsed'])
? json_decode($item['ingredients_parsed'], true)
: $item['ingredients_parsed'];
foreach ((array)$parsedData as $ing) {
if (!empty($ing['text'])) {
$parsed[] = mb_strtolower(trim($ing['text']));
}
}
}
foreach ($knownWithInci as $trigger) {
$inciLower = mb_strtolower($trigger['inci_name']);
$matchType = null;
// Chercher dans les ingrédients parsés (exact)
foreach ($parsed as $ing) {
if (str_contains($ing, $inciLower)) {
$matchType = 'exact';
break;
}
}
// Fallback texte brut (partial)
if (!$matchType && str_contains($ingredientsLower, $inciLower)) {
$matchType = 'partial';
}
if ($matchType) {
// Insérer dans item_triggers (ignorer les doublons)
try {
db_execute(
"INSERT IGNORE INTO item_triggers (item_id, trigger_id, match_type)
VALUES (?, ?, ?)",
[$itemId, $trigger['id'], $matchType]
);
} catch (Throwable) {}
$found[] = array_merge($trigger, ['match_type' => $matchType]);
}
}
return $found;
}
/**
* Récupère les déclencheurs connus liés à un item (depuis item_triggers)
*/
public static function getItemTriggers(int $itemId): array
{
return db_fetch_all(
"SELECT kt.*, it.match_type
FROM item_triggers it
JOIN known_triggers kt ON it.trigger_id = kt.id
WHERE it.item_id = ? AND kt.is_active = 1
ORDER BY kt.severity DESC",
[$itemId]
);
}
// ── Stats communautaires ──────────────────────────────────────────────────
/**
* Top déclencheurs signalés par la communauté (corrélations agrégées)
* Anonymisé : seuil minimum de N utilisateurs pour apparaître
*/
public static function getCommunityTop(int $limit = 10, int $minUsers = 5): array
{
return db_fetch_all(
"SELECT
i.name,
i.category,
i.barcode,
COUNT(DISTINCT c.user_id) AS user_count,
ROUND(AVG(c.score), 1) AS avg_score,
SUM(c.occurrences_crisis) AS total_crises,
kt.name_fr AS known_trigger_name,
kt.trigger_type,
kt.severity
FROM correlations c
JOIN items i ON c.item_id = i.id
LEFT JOIN item_triggers it ON i.id = it.item_id
LEFT JOIN known_triggers kt ON it.trigger_id = kt.id
WHERE c.score >= 50
AND c.status IN ('suspect','confirmed')
GROUP BY i.id
HAVING user_count >= ?
ORDER BY avg_score DESC, user_count DESC
LIMIT ?",
[$minUsers, $limit]
);
}
/**
* Marque un déclencheur comme "confirmé par l'utilisateur" sur son profil
* (stocké dans la table correlations via le status)
*/
public static function userConfirmTrigger(int $userId, int $itemId): bool
{
return db_execute(
"UPDATE correlations
SET status = 'confirmed'
WHERE user_id = ? AND item_id = ?",
[$userId, $itemId]
) > 0;
}
/**
* Ajoute une notification à l'utilisateur si un déclencheur connu est dans ses items
*/
public static function notifyUserOfKnownTriggers(int $userId): int
{
// Trouver les items utilisés par cet user qui ont des déclencheurs connus "high"
$items = db_fetch_all(
"SELECT DISTINCT i.id, i.name, kt.name_fr AS trigger_name, kt.severity
FROM entry_items ei
JOIN journal_entries je ON ei.entry_id = je.id
JOIN items i ON ei.item_id = i.id
JOIN item_triggers it ON i.id = it.item_id
JOIN known_triggers kt ON it.trigger_id = kt.id
WHERE je.user_id = ?
AND kt.severity = 'high'
AND kt.ds_specific = 1
AND je.entry_date > DATE_SUB(CURDATE(), INTERVAL 30 DAY)
LIMIT 5",
[$userId]
);
$count = 0;
foreach ($items as $item) {
// Vérifier si notif déjà envoyée pour cet item
$existing = db_count(
"SELECT COUNT(*) FROM notifications
WHERE user_id = ? AND type = 'known_trigger'
AND JSON_EXTRACT(meta, '$.item_id') = ?
AND created_at > DATE_SUB(NOW(), INTERVAL 30 DAY)",
[$userId, $item['id']]
);
if ($existing) continue;
db_insert(
"INSERT INTO notifications (user_id, type, title, body, meta)
VALUES (?, 'known_trigger', ?, ?, ?)",
[
$userId,
"⚠️ Déclencheur connu détecté dans " . $item['name'],
"{$item['name']} contient {$item['trigger_name']}, un déclencheur scientifiquement associé à la dermite séborrhéique.",
json_encode(['item_id' => $item['id'], 'trigger' => $item['trigger_name']]),
]
);
$count++;
}
return $count;
}
}