Files
crm-ms1/v4_ci4/app/Models/RadarModel.php

273 lines
9.0 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace App\Models;
use Config\Database;
/**
* IDEE-5 / MSOP-4 / MSOP-3 — radar_threads + skills + analyse IA.
*/
class RadarModel
{
private function db()
{
return Database::connect();
}
public function tablesExist(): bool
{
$row = $this->db()->query("SHOW TABLES LIKE 'radar_threads'")->getRowArray();
return ! empty($row);
}
public function hasAnalysisColumns(): bool
{
static $cached = null;
if ($cached !== null) {
return $cached;
}
$row = $this->db()->query("SHOW COLUMNS FROM radar_threads LIKE 'analysis'")->getRowArray();
$cached = ! empty($row);
return $cached;
}
public function hasGmailMessageIdColumn(): bool
{
static $cached = null;
if ($cached !== null) {
return $cached;
}
$row = $this->db()->query("SHOW COLUMNS FROM radar_threads LIKE 'gmail_message_id'")->getRowArray();
$cached = ! empty($row);
return $cached;
}
public function saveGmailMessageId(int $threadId, string $messageId): void
{
if (! $this->hasGmailMessageIdColumn() || $messageId === '') {
return;
}
$this->db()->table('radar_threads')->where('id', $threadId)->update([
'gmail_message_id' => \mb_substr($messageId, 0, 64),
]);
}
/**
* Fils dossards sans analyse IA, du plus récent au plus ancien.
*
* @return list<array<string, mixed>>
*/
public function listThreadsNeedingDossardAnalysis(int $limit = 20): array
{
if (! $this->hasAnalysisColumns()) {
return [];
}
$msgCol = $this->hasGmailMessageIdColumn() ? ', t.gmail_message_id' : ', \'\' AS gmail_message_id';
$sql = 'SELECT t.id, t.gmail_thread_id, t.mailbox, t.subject, t.from_email, t.summary'
. $msgCol
. ' FROM radar_threads t
INNER JOIN radar_thread_skills s ON s.thread_id = t.id AND s.skill = ?
WHERE t.is_example = 0
AND (t.analysis IS NULL OR t.analysis = \'\')
ORDER BY t.received_at DESC, t.id DESC
LIMIT ' . (int) $limit;
return $this->db()->query($sql, ['dossards'])->getResultArray();
}
public function countThreadsNeedingDossardAnalysis(): int
{
if (! $this->hasAnalysisColumns()) {
return 0;
}
$sql = 'SELECT COUNT(*) AS c FROM radar_threads t
INNER JOIN radar_thread_skills s ON s.thread_id = t.id AND s.skill = ?
WHERE t.is_example = 0
AND (t.analysis IS NULL OR t.analysis = \'\')';
$row = $this->db()->query($sql, ['dossards'])->getRowArray();
return (int) ($row['c'] ?? 0);
}
public function listExceptions(): array
{
return $this->db()->table('radar_exceptions')
->where('active', 1)
->orderBy('kind', 'ASC')
->orderBy('value', 'ASC')
->get()
->getResultArray();
}
public function listThreads(): array
{
$rows = $this->db()->table('radar_threads')
->orderBy('received_at', 'DESC')
->orderBy('id', 'DESC')
->get()
->getResultArray();
if ($rows === []) {
return [];
}
$ids = array_column($rows, 'id');
$skillRows = $this->db()->table('radar_thread_skills')
->whereIn('thread_id', $ids)
->get()
->getResultArray();
$byThread = [];
foreach ($skillRows as $s) {
$byThread[(int) $s['thread_id']][] = $s['skill'];
}
foreach ($rows as &$row) {
$id = (int) $row['id'];
$row['skills'] = $byThread[$id] ?? [];
$row['client_url'] = ! empty($row['client_id'])
? '/index.php/clients/form/' . (int) $row['client_id']
: '';
$row['projet_url'] = ! empty($row['projet_id'])
? '/index.php/projets/form/' . (int) $row['projet_id']
: '';
}
unset($row);
return $rows;
}
/**
* Historique dossards pour le même expéditeur (hors fil courant), du plus ancien au plus récent.
*
* @return list<array<string, mixed>>
*/
public function listPriorDossardThreads(string $fromEmail, string $excludeGmailThreadId, int $limit = 5): array
{
$fromEmail = \strtolower(\trim($fromEmail));
if ($fromEmail === '') {
return [];
}
$sql = 'SELECT t.subject, t.summary, t.received_at'
. ($this->hasAnalysisColumns() ? ', t.analysis' : ', NULL AS analysis')
. ' FROM radar_threads t
INNER JOIN radar_thread_skills s ON s.thread_id = t.id AND s.skill = ?
WHERE t.from_email = ?
AND t.gmail_thread_id <> ?
AND t.is_example = 0
ORDER BY t.received_at ASC, t.id ASC
LIMIT ' . (int) $limit;
return $this->db()->query($sql, ['dossards', $fromEmail, $excludeGmailThreadId])->getResultArray();
}
/**
* @param array<string, mixed> $data
* @param list<string> $skills
* @return array{op:string,id:int}
*/
public function upsertThread(array $data, array $skills = []): array
{
$db = $this->db();
$existing = $db->table('radar_threads')
->where('gmail_thread_id', $data['gmail_thread_id'])
->get()
->getRowArray();
if ($existing) {
$update = [
'mailbox' => $data['mailbox'],
'subject' => $data['subject'],
'from_email' => $data['from_email'],
'from_name' => $data['from_name'],
'gmail_url' => $data['gmail_url'],
'summary' => $data['summary'],
'status' => $data['status'],
'confidence' => $data['confidence'],
'received_at' => $data['received_at'],
'is_example' => 0,
];
// Ne pas écraser une analyse IA déjà présente avec le seul snippet.
if ($this->hasAnalysisColumns() && ! empty($existing['analysis'])) {
unset($update['confidence']);
}
$db->table('radar_threads')->where('id', $existing['id'])->update($update);
$threadId = (int) $existing['id'];
$op = 'update';
} else {
$insert = [
'gmail_thread_id' => $data['gmail_thread_id'],
'mailbox' => $data['mailbox'],
'subject' => $data['subject'],
'from_email' => $data['from_email'],
'from_name' => $data['from_name'],
'gmail_url' => $data['gmail_url'],
'summary' => $data['summary'],
'status' => $data['status'],
'confidence' => $data['confidence'],
'is_example' => (int) ($data['is_example'] ?? 0),
'received_at' => $data['received_at'],
'proposed_client_no' => $data['proposed_client_no'] ?? '',
];
$db->table('radar_threads')->insert($insert);
$threadId = (int) $db->insertID();
$op = 'insert';
}
foreach ($skills as $skill) {
$skill = \strtolower(\trim($skill));
if ($skill === '') {
continue;
}
$exists = $db->table('radar_thread_skills')
->where('thread_id', $threadId)
->where('skill', $skill)
->countAllResults();
if ($exists === 0) {
$db->table('radar_thread_skills')->insert([
'thread_id' => $threadId,
'skill' => $skill,
]);
}
}
return ['op' => $op, 'id' => $threadId];
}
public function saveAnalysis(int $threadId, string $analysis, string $delta, int $confidence): void
{
if (! $this->hasAnalysisColumns()) {
// Fallback : ranger lanalyse dans summary si colonnes absentes.
$this->db()->table('radar_threads')->where('id', $threadId)->update([
'summary' => \mb_substr($analysis, 0, 1000),
'confidence' => $confidence,
]);
return;
}
$this->db()->table('radar_threads')->where('id', $threadId)->update([
'analysis' => $analysis,
'analysis_delta' => $delta,
'confidence' => $confidence,
]);
}
public function needsAnalysis(int $threadId): bool
{
if (! $this->hasAnalysisColumns()) {
// Sans sql/MSOP-3-radar-analysis.sql on ne lance pas lIA en boucle.
return false;
}
$row = $this->db()->table('radar_threads')->select('analysis')->where('id', $threadId)->get()->getRowArray();
return empty($row['analysis']);
}
}