Update project documentation in 10-stack.mdc with JIRA prefix details, add new routes for Radar functionality in Routes.php, and include Radar link in the sidebar menu of men_list.php.
This commit is contained in:
14
.cursor/rules/projet/10-radar-mail.mdc
Normal file
14
.cursor/rules/projet/10-radar-mail.mdc
Normal file
@ -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.
|
||||
@ -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.
|
||||
|
||||
0
CRM MS1 — dev.session.sql
Normal file
0
CRM MS1 — dev.session.sql
Normal file
0
MS1 Inscription (via CRM host).session.sql
Normal file
0
MS1 Inscription (via CRM host).session.sql
Normal file
129
sql/IDEE-4-radar-proto.sql
Normal file
129
sql/IDEE-4-radar-proto.sql
Normal file
@ -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'
|
||||
);
|
||||
@ -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');
|
||||
|
||||
|
||||
41
v4_ci4/app/Controllers/Radar.php
Normal file
41
v4_ci4/app/Controllers/Radar.php
Normal file
@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Models\RadarModel;
|
||||
|
||||
/**
|
||||
* IDEE-5 — proto écran Radar (CI4 / Raven).
|
||||
*/
|
||||
class Radar extends BaseController
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$model = new RadarModel();
|
||||
$ready = $model->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(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
72
v4_ci4/app/Models/RadarModel.php
Normal file
72
v4_ci4/app/Models/RadarModel.php
Normal file
@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Config\Database;
|
||||
|
||||
/**
|
||||
* IDEE-5 — lecture proto Radar (exceptions + fils + skills).
|
||||
* Le moteur Gmail/Gemini n’écrit pas encore ici.
|
||||
*/
|
||||
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 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;
|
||||
}
|
||||
}
|
||||
@ -20,6 +20,12 @@
|
||||
<div class="flex flex-col h-full overflow-y-auto" data-simplebar>
|
||||
<div class="flex flex-col py-3 px-3">
|
||||
<nav class="flex flex-col space-y-0.5" id="SidebarCollapseGroup">
|
||||
<a href="/v4/radar" class="sidebar-link">
|
||||
<span class="sidebar-icon">
|
||||
<span class="icon-[lucide--mail]"></span>
|
||||
</span>
|
||||
<span class="sidebar-text">Radar mail</span>
|
||||
</a>
|
||||
<a href="/v4/dynamic/list/inventaire" class="sidebar-link ">
|
||||
<span class="sidebar-icon">
|
||||
<span class="icon-[lucide--file-chart-column]"></span>
|
||||
|
||||
229
v4_ci4/app/Views/radar/index.php
Normal file
229
v4_ci4/app/Views/radar/index.php
Normal file
@ -0,0 +1,229 @@
|
||||
<?php
|
||||
$title = 'Radar mail';
|
||||
$menu = 'menus/men_list';
|
||||
$header = 'headers/hea_default';
|
||||
$this->setVar('menu', $menu);
|
||||
$this->setVar('header', $header);
|
||||
$this->setVar('title', $title);
|
||||
|
||||
$ready = $ready ?? false;
|
||||
$exceptions = $exceptions ?? [];
|
||||
$threads = $threads ?? [];
|
||||
$feedUrl = $feedUrl ?? site_url('v4/radar/feed');
|
||||
|
||||
$statusLabel = static function (string $status): string {
|
||||
return match ($status) {
|
||||
'classe' => 'Classé',
|
||||
'a_classer' => 'À classer',
|
||||
'a_mettre_dans_crm' => 'À mettre dans le CRM',
|
||||
'ignore' => 'Ignoré',
|
||||
default => $status,
|
||||
};
|
||||
};
|
||||
$statusClass = static function (string $status): string {
|
||||
return match ($status) {
|
||||
'classe' => 'bg-emerald-100 text-emerald-800 dark:bg-emerald-900/40 dark:text-emerald-200',
|
||||
'a_mettre_dans_crm' => 'bg-amber-100 text-amber-900 dark:bg-amber-900/40 dark:text-amber-200',
|
||||
'ignore' => 'bg-zinc-200 text-zinc-700 dark:bg-zinc-800 dark:text-zinc-300',
|
||||
default => 'bg-indigo-100 text-indigo-800 dark:bg-indigo-900/40 dark:text-indigo-200',
|
||||
};
|
||||
};
|
||||
?>
|
||||
|
||||
<?= $this->extend('layouts/default') ?>
|
||||
<?= $this->section('content') ?>
|
||||
|
||||
<div class="space-y-6">
|
||||
<div class="flex flex-wrap items-end justify-between gap-3">
|
||||
<div>
|
||||
<p class="text-xs font-medium uppercase tracking-wide text-zinc-500">CRM v4 · IDEE-5</p>
|
||||
<h1 class="text-2xl font-semibold text-zinc-900 dark:text-white">Radar mail</h1>
|
||||
<p class="mt-1 text-sm text-zinc-600 dark:text-zinc-400">
|
||||
Tout <span class="font-medium">@ms1timing.com</span> moins les exceptions.
|
||||
Un fil peut porter plusieurs sujets. Le CRM est la vérité — on n’invente pas un client.
|
||||
</p>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<p class="text-xs text-zinc-500">Fil mis à jour</p>
|
||||
<p id="radar-clock" class="text-sm font-medium text-zinc-800 dark:text-zinc-200">—</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if (!$ready): ?>
|
||||
<div class="rounded-xl border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-950 dark:border-amber-800 dark:bg-amber-950/40 dark:text-amber-100">
|
||||
Tables absentes. Exécuter <code class="font-mono">sql/IDEE-4-radar-proto.sql</code> sur la BD CRM de <strong>dev</strong>, puis recharger.
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="rounded-xl border border-zinc-200 bg-white px-4 py-3 text-sm text-zinc-600 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-300">
|
||||
Moteur Gmail Push + Gemini : pas encore branché. Cet écran lit la table
|
||||
<code class="font-mono">radar_threads</code> (deux exemples proto pour valider la saveur).
|
||||
Périmètre cible : domaine entier, pas une seule boîte.
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="grid gap-4 lg:grid-cols-3">
|
||||
<div class="rounded-xl border border-zinc-200 bg-white p-4 dark:border-zinc-800 dark:bg-zinc-900 lg:col-span-1">
|
||||
<h2 class="text-sm font-semibold text-zinc-900 dark:text-white">Exceptions</h2>
|
||||
<p class="mt-1 text-xs text-zinc-500">On exclut, on ne tient pas de liste blanche.</p>
|
||||
<ul class="mt-3 space-y-2 text-sm" id="radar-exceptions">
|
||||
<?php if ($exceptions === []): ?>
|
||||
<li class="text-zinc-400">Aucune</li>
|
||||
<?php else: ?>
|
||||
<?php foreach ($exceptions as $ex): ?>
|
||||
<li class="flex flex-col rounded-lg bg-zinc-50 px-3 py-2 dark:bg-zinc-800/60">
|
||||
<span class="font-mono text-xs text-zinc-500"><?= esc($ex['kind']) ?></span>
|
||||
<span class="font-medium"><?= esc($ex['value']) ?></span>
|
||||
<?php if ($ex['note'] !== ''): ?>
|
||||
<span class="text-xs text-zinc-500"><?= esc($ex['note']) ?></span>
|
||||
<?php endif; ?>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border border-zinc-200 bg-white p-4 dark:border-zinc-800 dark:bg-zinc-900 lg:col-span-2">
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<h2 class="text-sm font-semibold text-zinc-900 dark:text-white">Signaux</h2>
|
||||
<span class="text-xs text-zinc-400" id="radar-count"><?= count($threads) ?> fil(s)</span>
|
||||
</div>
|
||||
<div id="radar-feed" class="space-y-3">
|
||||
<?php if ($threads === []): ?>
|
||||
<p class="text-sm text-zinc-400">Aucun signal pour l’instant.</p>
|
||||
<?php else: ?>
|
||||
<?php foreach ($threads as $t): ?>
|
||||
<article class="rounded-lg border border-zinc-100 p-3 dark:border-zinc-800">
|
||||
<div class="flex flex-wrap items-start justify-between gap-2">
|
||||
<div>
|
||||
<p class="font-medium text-zinc-900 dark:text-white"><?= esc($t['subject'] ?: '(sans objet)') ?></p>
|
||||
<p class="text-xs text-zinc-500">
|
||||
<?= esc($t['from_name'] !== '' ? $t['from_name'] . ' · ' : '') ?><?= esc($t['from_email']) ?>
|
||||
· <?= esc($t['mailbox']) ?>
|
||||
</p>
|
||||
</div>
|
||||
<span class="inline-flex rounded-full px-2 py-0.5 text-xs font-medium <?= $statusClass($t['status']) ?>">
|
||||
<?= esc($statusLabel($t['status'])) ?>
|
||||
</span>
|
||||
</div>
|
||||
<div class="mt-2 flex flex-wrap gap-1">
|
||||
<?php foreach ($t['skills'] as $sk): ?>
|
||||
<span class="rounded-md bg-zinc-100 px-2 py-0.5 text-xs font-medium text-zinc-700 dark:bg-zinc-800 dark:text-zinc-200"><?= esc($sk) ?></span>
|
||||
<?php endforeach; ?>
|
||||
<?php if (!empty($t['proposed_client_no'])): ?>
|
||||
<span class="rounded-md bg-zinc-100 px-2 py-0.5 text-xs font-mono text-zinc-700 dark:bg-zinc-800 dark:text-zinc-200">#<?= esc($t['proposed_client_no']) ?></span>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($t['is_example'])): ?>
|
||||
<span class="rounded-md border border-dashed border-zinc-300 px-2 py-0.5 text-xs text-zinc-500">exemple proto</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php if ($t['summary'] !== ''): ?>
|
||||
<p class="mt-2 text-sm text-zinc-600 dark:text-zinc-400"><?= esc($t['summary']) ?></p>
|
||||
<?php endif; ?>
|
||||
<div class="mt-2 flex flex-wrap gap-3 text-xs">
|
||||
<?php if (!empty($t['projet_url'])): ?>
|
||||
<a class="font-medium text-primary hover:underline" href="<?= esc($t['projet_url']) ?>">Ouvrir le projet CRM</a>
|
||||
<?php elseif ($t['status'] === 'a_mettre_dans_crm'): ?>
|
||||
<a class="font-medium text-amber-700 hover:underline dark:text-amber-300" href="/index.php/clients">Créer dans le CRM, puis relancer</a>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($t['client_url'])): ?>
|
||||
<a class="text-zinc-500 hover:underline" href="<?= esc($t['client_url']) ?>">Fiche client</a>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($t['gmail_url'])): ?>
|
||||
<a class="text-zinc-500 hover:underline" href="<?= esc($t['gmail_url']) ?>" target="_blank" rel="noopener">Gmail</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</article>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('scripts') ?>
|
||||
<script>
|
||||
(function () {
|
||||
const feedUrl = <?= json_encode($feedUrl) ?>;
|
||||
const root = document.getElementById('radar-feed');
|
||||
const clock = document.getElementById('radar-clock');
|
||||
const countEl = document.getElementById('radar-count');
|
||||
if (!root || !feedUrl) return;
|
||||
|
||||
const statusLabel = {
|
||||
classe: 'Classé',
|
||||
a_classer: 'À classer',
|
||||
a_mettre_dans_crm: 'À mettre dans le CRM',
|
||||
ignore: 'Ignoré'
|
||||
};
|
||||
const statusClass = {
|
||||
classe: 'bg-emerald-100 text-emerald-800 dark:bg-emerald-900/40 dark:text-emerald-200',
|
||||
a_mettre_dans_crm: 'bg-amber-100 text-amber-900 dark:bg-amber-900/40 dark:text-amber-200',
|
||||
ignore: 'bg-zinc-200 text-zinc-700 dark:bg-zinc-800 dark:text-zinc-300',
|
||||
a_classer: 'bg-indigo-100 text-indigo-800 dark:bg-indigo-900/40 dark:text-indigo-200'
|
||||
};
|
||||
|
||||
function esc(s) {
|
||||
const d = document.createElement('div');
|
||||
d.textContent = s == null ? '' : String(s);
|
||||
return d.innerHTML;
|
||||
}
|
||||
|
||||
function render(threads) {
|
||||
countEl.textContent = (threads.length || 0) + ' fil(s)';
|
||||
if (!threads.length) {
|
||||
root.innerHTML = '<p class="text-sm text-zinc-400">Aucun signal pour l’instant.</p>';
|
||||
return;
|
||||
}
|
||||
root.innerHTML = threads.map(function (t) {
|
||||
const st = t.status || 'a_classer';
|
||||
const skills = (t.skills || []).map(function (sk) {
|
||||
return '<span class="rounded-md bg-zinc-100 px-2 py-0.5 text-xs font-medium text-zinc-700 dark:bg-zinc-800 dark:text-zinc-200">' + esc(sk) + '</span>';
|
||||
}).join('');
|
||||
const num = t.proposed_client_no
|
||||
? '<span class="rounded-md bg-zinc-100 px-2 py-0.5 text-xs font-mono text-zinc-700 dark:bg-zinc-800 dark:text-zinc-200">#' + esc(t.proposed_client_no) + '</span>'
|
||||
: '';
|
||||
const ex = t.is_example && t.is_example !== '0'
|
||||
? '<span class="rounded-md border border-dashed border-zinc-300 px-2 py-0.5 text-xs text-zinc-500">exemple proto</span>'
|
||||
: '';
|
||||
let links = '';
|
||||
if (t.projet_url) {
|
||||
links += '<a class="font-medium text-primary hover:underline" href="' + esc(t.projet_url) + '">Ouvrir le projet CRM</a>';
|
||||
} else if (st === 'a_mettre_dans_crm') {
|
||||
links += '<a class="font-medium text-amber-700 hover:underline dark:text-amber-300" href="/index.php/clients">Créer dans le CRM, puis relancer</a>';
|
||||
}
|
||||
if (t.client_url) {
|
||||
links += '<a class="text-zinc-500 hover:underline" href="' + esc(t.client_url) + '">Fiche client</a>';
|
||||
}
|
||||
if (t.gmail_url) {
|
||||
links += '<a class="text-zinc-500 hover:underline" href="' + esc(t.gmail_url) + '" target="_blank" rel="noopener">Gmail</a>';
|
||||
}
|
||||
return '<article class="rounded-lg border border-zinc-100 p-3 dark:border-zinc-800">'
|
||||
+ '<div class="flex flex-wrap items-start justify-between gap-2"><div>'
|
||||
+ '<p class="font-medium text-zinc-900 dark:text-white">' + esc(t.subject || '(sans objet)') + '</p>'
|
||||
+ '<p class="text-xs text-zinc-500">' + esc((t.from_name ? t.from_name + ' · ' : '') + t.from_email) + ' · ' + esc(t.mailbox) + '</p>'
|
||||
+ '</div><span class="inline-flex rounded-full px-2 py-0.5 text-xs font-medium ' + (statusClass[st] || statusClass.a_classer) + '">'
|
||||
+ esc(statusLabel[st] || st) + '</span></div>'
|
||||
+ '<div class="mt-2 flex flex-wrap gap-1">' + skills + num + ex + '</div>'
|
||||
+ (t.summary ? '<p class="mt-2 text-sm text-zinc-600 dark:text-zinc-400">' + esc(t.summary) + '</p>' : '')
|
||||
+ '<div class="mt-2 flex flex-wrap gap-3 text-xs">' + links + '</div></article>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
async function tick() {
|
||||
try {
|
||||
const res = await fetch(feedUrl, { credentials: 'same-origin' });
|
||||
const data = await res.json();
|
||||
if (clock) clock.textContent = new Date().toLocaleTimeString('fr-CA');
|
||||
if (data && data.ok) render(data.threads || []);
|
||||
} catch (e) {
|
||||
if (clock) clock.textContent = 'hors ligne';
|
||||
}
|
||||
}
|
||||
|
||||
tick();
|
||||
setInterval(tick, 4000);
|
||||
})();
|
||||
</script>
|
||||
<?= $this->endSection() ?>
|
||||
Reference in New Issue
Block a user