HEX
Server: Apache
System: Linux info 3.0 #1337 SMP Tue Jan 01 00:00:00 CEST 2000 all GNU/Linux
User: u115237990 (7145895)
PHP: 8.3.32
Disabled: NONE
Upload Files
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;
    }
}