diff --git a/.cursor/rules/projet/10-radar-mail.mdc b/.cursor/rules/projet/10-radar-mail.mdc new file mode 100644 index 00000000..7d5ab1f9 --- /dev/null +++ b/.cursor/rules/projet/10-radar-mail.mdc @@ -0,0 +1,14 @@ +--- +description: Radar mail — proto CRM v4 (IDEE-4 / IDEE-5) +alwaysApply: true +--- + +# Radar mail (CRM) + +Épique **MSOP-1**. Idée : **IDEE-4**. Proto écran : **MSOP-2**. + +- UI : CI4 / Raven `/v4/radar` — pas SmartAdmin. +- Périmètre : tout `@ms1timing.com` moins `radar_exceptions` (v0 : `confirmation@`). Pas de palier `info@`. +- Moteur (plus tard) : Gmail Push + Gemini. L’écran lit `radar_threads`. +- Un fil = N skills. Vérité = CRM (on n’invente pas un client). +- Pas dans MS1 Inscription. diff --git a/.cursor/rules/projet/10-stack.mdc b/.cursor/rules/projet/10-stack.mdc index 7a565e13..c5d8d6fc 100644 --- a/.cursor/rules/projet/10-stack.mdc +++ b/.cursor/rules/projet/10-stack.mdc @@ -6,6 +6,6 @@ alwaysApply: true # Projet CRM MS1 — stack - Stack : PHP applicatif CRM (`application/`, `v4/`, `v4_ci4/`, etc.). -- Préfixe JIRA : confirmer avec l'utilisateur (souvent ticket CRM / MS1 selon le contexte). +- Préfixe JIRA développement : **`MSOP`** (espace MS1 Ops). Idées = `IDEE` (pas de code). Inscription = `MSIN`. - Workspace local typique : `C:\dev_v4\crm-ms1`. - Ne pas committer `credentials.json` ni secrets. diff --git a/CRM MS1 — dev.session.sql b/CRM MS1 — dev.session.sql new file mode 100644 index 00000000..e69de29b diff --git a/MS1 Inscription (via CRM host).session.sql b/MS1 Inscription (via CRM host).session.sql new file mode 100644 index 00000000..e69de29b diff --git a/sql/IDEE-4-radar-proto.sql b/sql/IDEE-4-radar-proto.sql new file mode 100644 index 00000000..03934a87 --- /dev/null +++ b/sql/IDEE-4-radar-proto.sql @@ -0,0 +1,129 @@ +-- IDEE-4 / IDEE-5 — Radar mail proto (CRM) +-- Tables pour l’écran /v4/radar. Moteur Gmail Push + Gemini : story suivante. +-- Exécuter sur la BD CRM de DEV (devcrmms1_dev), pas en prod. + +CREATE TABLE IF NOT EXISTS radar_exceptions ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT, + kind ENUM('mailbox', 'sender') NOT NULL DEFAULT 'mailbox', + value VARCHAR(190) NOT NULL, + note VARCHAR(255) NOT NULL DEFAULT '', + active TINYINT(1) NOT NULL DEFAULT 1, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id), + UNIQUE KEY uq_radar_exc (kind, value) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE IF NOT EXISTS radar_threads ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT, + gmail_thread_id VARCHAR(64) NOT NULL, + mailbox VARCHAR(190) NOT NULL DEFAULT '', + subject VARCHAR(255) NOT NULL DEFAULT '', + from_email VARCHAR(190) NOT NULL DEFAULT '', + from_name VARCHAR(190) NOT NULL DEFAULT '', + gmail_url VARCHAR(500) NOT NULL DEFAULT '', + client_id INT UNSIGNED NULL DEFAULT NULL, + projet_id INT UNSIGNED NULL DEFAULT NULL, + proposed_client_no VARCHAR(32) NOT NULL DEFAULT '', + status VARCHAR(32) NOT NULL DEFAULT 'a_classer', + summary VARCHAR(500) NOT NULL DEFAULT '', + confidence TINYINT UNSIGNED NOT NULL DEFAULT 0, + is_example TINYINT(1) NOT NULL DEFAULT 0, + received_at DATETIME NULL DEFAULT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (id), + UNIQUE KEY uq_radar_thread (gmail_thread_id), + KEY idx_radar_status (status), + KEY idx_radar_received (received_at) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE IF NOT EXISTS radar_thread_skills ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT, + thread_id INT UNSIGNED NOT NULL, + skill VARCHAR(32) NOT NULL, + PRIMARY KEY (id), + UNIQUE KEY uq_radar_skill (thread_id, skill), + KEY idx_radar_skill (skill) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE IF NOT EXISTS radar_rules ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT, + match_type ENUM('email', 'domain') NOT NULL DEFAULT 'email', + match_value VARCHAR(190) NOT NULL, + client_id INT UNSIGNED NULL DEFAULT NULL, + projet_id INT UNSIGNED NULL DEFAULT NULL, + skill VARCHAR(32) NOT NULL DEFAULT '', + source VARCHAR(32) NOT NULL DEFAULT 'human', + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id), + UNIQUE KEY uq_radar_rule (match_type, match_value, skill) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +INSERT INTO radar_exceptions (kind, value, note, active) +SELECT 'mailbox', 'confirmation@ms1timing.com', 'Mails transactionnels inscription — ne pas ouvrir', 1 +FROM DUAL +WHERE NOT EXISTS ( + SELECT 1 FROM radar_exceptions + WHERE kind = 'mailbox' AND value = 'confirmation@ms1timing.com' +); + +INSERT INTO radar_threads ( + gmail_thread_id, mailbox, subject, from_email, from_name, gmail_url, + proposed_client_no, status, summary, confidence, is_example, received_at +) +SELECT + 'proto-ex-1', + 'desjardins.lp@ms1timing.com', + 'Logos dossards + fichiers départ site', + 'marie@course-exemple.ca', + 'Marie Exemple', + '', + '1842', + 'a_classer', + 'Deux sujets dans le même fil : logos pour l’imprimeur, et fichiers à déposer pour le site d’inscription.', + 72, + 1, + NOW() +FROM DUAL +WHERE NOT EXISTS (SELECT 1 FROM radar_threads WHERE gmail_thread_id = 'proto-ex-1'); + +INSERT INTO radar_thread_skills (thread_id, skill) +SELECT t.id, 'dossards' FROM radar_threads t +WHERE t.gmail_thread_id = 'proto-ex-1' + AND NOT EXISTS ( + SELECT 1 FROM radar_thread_skills s WHERE s.thread_id = t.id AND s.skill = 'dossards' + ); + +INSERT INTO radar_thread_skills (thread_id, skill) +SELECT t.id, 'site' FROM radar_threads t +WHERE t.gmail_thread_id = 'proto-ex-1' + AND NOT EXISTS ( + SELECT 1 FROM radar_thread_skills s WHERE s.thread_id = t.id AND s.skill = 'site' + ); + +INSERT INTO radar_threads ( + gmail_thread_id, mailbox, subject, from_email, from_name, gmail_url, + proposed_client_no, status, summary, confidence, is_example, received_at +) +SELECT + 'proto-ex-2', + 'info@ms1timing.com', + 'Question impression dossards', + 'nouveau@inconnu.example', + 'Contact inconnu', + '', + '', + 'a_mettre_dans_crm', + 'Adresse absente du CRM. Créer le contact / projet, puis relancer — le fil doit se recoller tout seul.', + 40, + 1, + DATE_SUB(NOW(), INTERVAL 25 MINUTE) +FROM DUAL +WHERE NOT EXISTS (SELECT 1 FROM radar_threads WHERE gmail_thread_id = 'proto-ex-2'); + +INSERT INTO radar_thread_skills (thread_id, skill) +SELECT t.id, 'dossards' FROM radar_threads t +WHERE t.gmail_thread_id = 'proto-ex-2' + AND NOT EXISTS ( + SELECT 1 FROM radar_thread_skills s WHERE s.thread_id = t.id AND s.skill = 'dossards' + ); diff --git a/v4_ci4/app/Config/Routes.php b/v4_ci4/app/Config/Routes.php index b6691c65..5da96127 100644 --- a/v4_ci4/app/Config/Routes.php +++ b/v4_ci4/app/Config/Routes.php @@ -7,6 +7,10 @@ use CodeIgniter\Router\RouteCollection; */ $routes->get('/', 'Home::index'); +// IDEE-5 — proto Radar (écran CRM v4) +$routes->get('radar', 'Radar::index'); +$routes->get('radar/feed', 'Radar::feed'); + $routes->get('inventaire/redirect/(:segment)', 'Inventaire::redirect/$1'); $routes->get('inventaire/test/(:segment)', 'Inventaire::test/$1'); diff --git a/v4_ci4/app/Controllers/Radar.php b/v4_ci4/app/Controllers/Radar.php new file mode 100644 index 00000000..f85a26c4 --- /dev/null +++ b/v4_ci4/app/Controllers/Radar.php @@ -0,0 +1,41 @@ +tablesExist(); + + return view('radar/index', [ + 'ready' => $ready, + 'exceptions' => $ready ? $model->listExceptions() : [], + 'threads' => $ready ? $model->listThreads() : [], + 'feedUrl' => site_url('v4/radar/feed'), + ]); + } + + public function feed() + { + $model = new RadarModel(); + if (!$model->tablesExist()) { + return $this->response->setJSON([ + 'ok' => false, + 'threads' => [], + ]); + } + + return $this->response->setJSON([ + 'ok' => true, + 'at' => date('c'), + 'threads' => $model->listThreads(), + ]); + } +} diff --git a/v4_ci4/app/Models/RadarModel.php b/v4_ci4/app/Models/RadarModel.php new file mode 100644 index 00000000..753ea375 --- /dev/null +++ b/v4_ci4/app/Models/RadarModel.php @@ -0,0 +1,72 @@ +db()->query("SHOW TABLES LIKE 'radar_threads'")->getRowArray(); + + return !empty($row); + } + + 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; + } +} diff --git a/v4_ci4/app/Views/menus/men_list.php b/v4_ci4/app/Views/menus/men_list.php index c344e891..78301bf0 100644 --- a/v4_ci4/app/Views/menus/men_list.php +++ b/v4_ci4/app/Views/menus/men_list.php @@ -20,6 +20,12 @@