Spaces:
Sleeping
Sleeping
Commit ·
01cf033
1
Parent(s): 373caa7
Corrige unidade do campo de reajuste (percent vs valor)
Browse files- frontend-react/index.html +7 -0
- frontend-react/src/style.css +146 -109
- frontend-react/src/views/Dashboard.jsx +1 -3
- src/delivery/email_sender.py +229 -110
frontend-react/index.html
CHANGED
|
@@ -3,6 +3,13 @@
|
|
| 3 |
<head>
|
| 4 |
<meta charset="utf-8" />
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
<title>Analytical-Force</title>
|
| 7 |
</head>
|
| 8 |
<body>
|
|
|
|
| 3 |
<head>
|
| 4 |
<meta charset="utf-8" />
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
| 6 |
+
<meta name="theme-color" content="#0018FF" />
|
| 7 |
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
| 8 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
| 9 |
+
<link
|
| 10 |
+
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap"
|
| 11 |
+
rel="stylesheet"
|
| 12 |
+
/>
|
| 13 |
<title>Analytical-Force</title>
|
| 14 |
</head>
|
| 15 |
<body>
|
frontend-react/src/style.css
CHANGED
|
@@ -1,166 +1,203 @@
|
|
| 1 |
:root{
|
| 2 |
/* Paleta Penso (02 - Cores.pdf): Blue #0018FF, Blue Tech #0004DD, Black Intense #060606. */
|
| 3 |
--c1:#0018FF;--c2:#0004DD;--c3:#FFC453;
|
| 4 |
-
--bg:#
|
| 5 |
-
--txt:#
|
| 6 |
--ok:#16C79A;--high:#F05454;--med:#FFC453;--low:#eab308;
|
| 7 |
-
--blue:#0018FF;--green:#16C79A;--red:#F05454;--amber:#FFC453;--violet:#
|
| 8 |
--penso-blue:#0018FF;--penso-bluetech:#0004DD;--penso-black:#171717;--penso-blackx:#060606;
|
| 9 |
-
--radius:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
}
|
| 11 |
-
[data-theme="dark"]{--bg:#
|
| 12 |
-
--shadow:0
|
|
|
|
|
|
|
| 13 |
*{box-sizing:border-box}
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
a{color:var(--c1)}
|
| 19 |
.app{display:flex;min-height:100vh}
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
.
|
| 23 |
-
|
| 24 |
-
.sb-brand
|
| 25 |
-
.sb-brand
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
nav a
|
| 30 |
-
|
| 31 |
-
nav a.
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
.mainwrap{margin-left:var(--sb);flex:1;min-width:0;display:flex;flex-direction:column}
|
| 36 |
-
.topbar{position:sticky;top:0;z-index:30;background:rgba(255,255,255,.
|
| 37 |
-
border-bottom:1px solid var(--line);padding:
|
| 38 |
-
[data-theme="dark"] .topbar{background:rgba(
|
| 39 |
-
.hamb{display:none;background:var(--surface);border:1px solid var(--line);color:var(--txt);border-radius:
|
| 40 |
-
.vtitle{font-size:
|
| 41 |
-
background:linear-gradient(90deg,var(--c1),
|
| 42 |
.spacer{flex:1}
|
| 43 |
-
.pill{display:flex;align-items:center;gap:7px;background:var(--surface);border:1px solid var(--line);color:var(--muted);font-size:12px;padding:7px
|
| 44 |
-
.dot{width:
|
| 45 |
.dot.on{background:#22c55e;animation:pulse 1.8s infinite} .dot.off{background:#ef4444}
|
| 46 |
@keyframes pulse{0%{box-shadow:0 0 0 0 rgba(34,197,94,.5)}70%{box-shadow:0 0 0 8px rgba(34,197,94,0)}100%{box-shadow:0 0 0 0 rgba(34,197,94,0)}}
|
| 47 |
-
.sel{appearance:none;background:var(--surface);border:1px solid var(--line);color:var(--txt);border-radius:
|
| 48 |
-
padding:9px 30px 9px
|
| 49 |
-
background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='%
|
| 50 |
.sel:focus{outline:none;border-color:var(--c1)}
|
| 51 |
-
.iconbtn{width:38px;height:38px;border-radius:
|
| 52 |
-
.iconbtn:hover{border-color:var(--c1)}
|
| 53 |
-
.btn{border:0;border-radius:
|
| 54 |
-
.btn:hover{filter:brightness(1.
|
| 55 |
-
.btn-primary{background:linear-gradient(100deg,var(--c1),
|
| 56 |
.btn-ghost{background:var(--surface);color:var(--c1);border:1px solid var(--line)}
|
| 57 |
-
.btn-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
.
|
| 63 |
-
|
| 64 |
-
.sec-h
|
|
|
|
|
|
|
|
|
|
| 65 |
.meta{display:flex;gap:8px;flex-wrap:wrap}
|
| 66 |
-
.chip{background:var(--surface2);border:1px solid var(--line);color:var(--txt);border-radius:999px;padding:6px
|
| 67 |
.chip.good{background:#dcfce7;color:#15803d;border-color:#bbf7d0} .chip.bad{background:#ffe4e6;color:#be123c;border-color:#fecdd3}
|
| 68 |
-
.hero{border-radius:
|
| 69 |
-
|
| 70 |
-
.hero
|
| 71 |
-
.hero
|
| 72 |
-
.
|
|
|
|
|
|
|
| 73 |
background-size:200% 100%;animation:shimmer 1.4s infinite}
|
| 74 |
@keyframes shimmer{0%{background-position:200% 0}100%{background-position:-200% 0}}
|
| 75 |
@keyframes spin{from{transform:rotate(0deg)}to{transform:rotate(360deg)}}
|
| 76 |
.topload{height:3px;border-radius:999px;overflow:hidden;background:var(--line);margin-bottom:14px;position:relative}
|
| 77 |
.topload::after{content:"";position:absolute;inset:0;width:34%;border-radius:999px;background:linear-gradient(90deg,var(--c1),var(--c3));animation:slide 1.1s infinite}
|
| 78 |
-
.fade-view{animation:fadeUp .
|
| 79 |
.errcard{display:flex;align-items:flex-start;gap:12px}
|
| 80 |
.errcard .ic{font-size:22px;line-height:1}
|
| 81 |
.errcard .btns{display:flex;gap:8px;margin-top:10px}
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
.
|
| 85 |
-
.kpi
|
| 86 |
-
.kpi
|
| 87 |
-
.kpi
|
| 88 |
-
.kpi.
|
| 89 |
-
.kpi
|
| 90 |
-
.kpi.
|
| 91 |
-
.kpi.
|
| 92 |
-
.kpi.
|
| 93 |
-
.kpi.
|
|
|
|
|
|
|
|
|
|
| 94 |
.split{display:grid;grid-template-columns:1.4fr .6fr;gap:18px;align-items:start} @media(max-width:900px){.split{grid-template-columns:1fr}}
|
| 95 |
.filters{display:flex;gap:7px;flex-wrap:wrap;margin-bottom:14px}
|
| 96 |
-
.fchip{font-size:12px;padding:7px
|
| 97 |
-
.fchip
|
| 98 |
-
.
|
| 99 |
-
|
| 100 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
.alert details{margin-top:9px} .alert summary{cursor:pointer;font-size:12px;color:var(--c1);font-weight:700}
|
| 102 |
.recs{margin:9px 0 0;padding-left:18px} .recs li{font-size:12.5px;color:var(--muted);margin-bottom:4px} .recs a{color:var(--c1)}
|
| 103 |
-
.plan{white-space:pre-wrap;background:var(--surface2);border:1px dashed var(--line);border-radius:
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
.dcard .dh .ct{background:rgba(255,255,255,.28);color:#fff;border-radius:999px;padding:2px 10px;font-size:11px}
|
| 108 |
-
.dcard ul{margin:0;padding:
|
| 109 |
-
.dcard a{text-decoration:none;color:var(--c1)} .dcard a:hover{text-decoration:underline} .dcard .info{color:var(--faint)}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
.tabs{display:flex;gap:7px;flex-wrap:wrap;margin-bottom:14px}
|
| 111 |
-
.tab{padding:8px
|
| 112 |
-
.tab
|
| 113 |
-
.
|
|
|
|
| 114 |
.panel.active{display:block} .panel h2{font-size:16px;color:var(--c1)} .panel table{border-collapse:collapse;width:100%} .panel td,.panel th{border:1px solid var(--line);padding:6px 9px;font-size:13px}
|
| 115 |
table.owners{width:100%;border-collapse:collapse}
|
| 116 |
-
.owners th,.owners td{border-bottom:1px solid var(--line);padding:
|
| 117 |
-
.owners th{color:var(--muted);font-size:11px;text-transform:uppercase;letter-spacing:.
|
| 118 |
-
.owners tbody tr:hover{background:var(--surface2)}
|
| 119 |
-
.empty{text-align:center;color:var(--faint);padding:
|
| 120 |
-
.err{background:#ffe4e6;border:1px solid #fecdd3;color:#be123c;padding:
|
| 121 |
label.lbl{display:block;font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.05em;margin:0 0 7px;font-weight:700}
|
| 122 |
-
input[type=text],input[type=date],input[type=password],input[type=email]{width:100%;padding:11px
|
| 123 |
-
input:focus{outline:none;border-color:var(--c1);box-shadow:0 0 0
|
| 124 |
.switch{display:inline-flex;align-items:center;gap:10px;cursor:pointer;font-size:14px}
|
| 125 |
.switch input{display:none}.track{width:42px;height:24px;border-radius:999px;background:#cbd0e0;position:relative;transition:.2s}
|
| 126 |
[data-theme="dark"] .track{background:#39406a}
|
| 127 |
.track::after{content:"";position:absolute;top:3px;left:3px;width:18px;height:18px;border-radius:50%;background:#fff;transition:.2s}
|
| 128 |
-
.switch input:checked + .track{background:linear-gradient(100deg,var(--c1),
|
| 129 |
.bar{height:8px;background:var(--line);border-radius:999px;overflow:hidden;position:relative;margin:10px 0}
|
| 130 |
.bar::after{content:"";position:absolute;inset:0;width:38%;border-radius:999px;background:linear-gradient(90deg,var(--c1),var(--c3));animation:slide 1.25s infinite}
|
| 131 |
@keyframes slide{0%{transform:translateX(-110%)}100%{transform:translateX(330%)}}
|
| 132 |
-
.integr{display:flex;align-items:center;gap:
|
| 133 |
.integr span{font-size:11px;text-transform:uppercase;letter-spacing:.08em;color:var(--faint);font-weight:700}
|
| 134 |
-
.integr img{height:20px;opacity:.
|
| 135 |
-
.backdrop{position:fixed;inset:0;background:rgba(
|
| 136 |
-
.backdrop.open{display:flex} .modal{background:var(--surface);border:1px solid var(--line);border-radius:
|
| 137 |
-
.modal .mh{display:flex;justify-content:space-between;align-items:center;margin-bottom:
|
| 138 |
-
.modal .x{background:var(--surface2);border:1px solid var(--line);border-radius:
|
| 139 |
-
#toasts{position:fixed;right:
|
| 140 |
-
.toast{background:var(--txt);color:var(--surface);padding:
|
| 141 |
.toast.ok{background:linear-gradient(100deg,#16a34a,#22c55e);color:#fff} .toast.bad{background:linear-gradient(100deg,#e11d48,#f43f5e);color:#fff}
|
| 142 |
.cclist{list-style:none;margin:12px 0 0;padding:0;display:flex;flex-direction:column;gap:8px}
|
| 143 |
-
.ccrow{display:flex;align-items:center;justify-content:space-between;gap:10px;background:var(--surface2);border:1px solid var(--line);border-radius:
|
| 144 |
.ccrow button{background:none;border:0;color:var(--red);cursor:pointer;font-size:13px;font-weight:700}
|
| 145 |
.ccadd{display:flex;gap:8px;margin-top:14px}
|
| 146 |
-
@media(max-width:860px){.sidebar{transform:translateX(-100%)} .sidebar.open{transform:none} .mainwrap{margin-left:0} .hamb{display:block} .split{grid-template-columns:1fr}}
|
| 147 |
|
| 148 |
-
/* ---- Reforço visual:
|
| 149 |
-
.disclaimer{margin-top:
|
| 150 |
.af-glow{position:relative;overflow:hidden}
|
| 151 |
.af-glow::before{content:"";position:absolute;inset:-2px;border-radius:var(--radius);padding:2px;
|
| 152 |
-
background:linear-gradient(120deg,var(--c1),var(--c3),
|
| 153 |
-webkit-mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);-webkit-mask-composite:xor;mask-composite:exclude;
|
| 154 |
-
animation:af-borderflow 5s ease infinite;opacity:.
|
| 155 |
@keyframes af-borderflow{0%{background-position:0% 50%}50%{background-position:100% 50%}100%{background-position:0% 50%}}
|
| 156 |
-
.kpis .kpi{animation:fadeUp .45s
|
| 157 |
.kpis .kpi:nth-child(1){animation-delay:.02s} .kpis .kpi:nth-child(2){animation-delay:.06s}
|
| 158 |
.kpis .kpi:nth-child(3){animation-delay:.10s} .kpis .kpi:nth-child(4){animation-delay:.14s}
|
| 159 |
.kpis .kpi:nth-child(5){animation-delay:.18s} .kpis .kpi:nth-child(6){animation-delay:.22s}
|
| 160 |
.kpis .kpi:nth-child(7){animation-delay:.26s} .kpis .kpi:nth-child(8){animation-delay:.30s}
|
| 161 |
.kpis .kpi:nth-child(9){animation-delay:.34s} .kpis .kpi:nth-child(10){animation-delay:.38s}
|
| 162 |
.kpi:hover .ic{transform:scale(1.08) rotate(-4deg);transition:.2s}
|
| 163 |
-
.dcard{transition:transform .2s ease,box-shadow .2s ease;animation:fadeUp .4s ease both}
|
| 164 |
-
.dcard:hover{transform:translateY(-4px) scale(1.01);box-shadow:0 14px 30px rgba(0,4,221,.14)}
|
| 165 |
-
.dgrid .dcard:nth-child(1){animation-delay:.03s} .dgrid .dcard:nth-child(2){animation-delay:.08s}
|
| 166 |
-
.dgrid .dcard:nth-child(3){animation-delay:.13s} .dgrid .dcard:nth-child(4){animation-delay:.18s}
|
|
|
|
| 1 |
:root{
|
| 2 |
/* Paleta Penso (02 - Cores.pdf): Blue #0018FF, Blue Tech #0004DD, Black Intense #060606. */
|
| 3 |
--c1:#0018FF;--c2:#0004DD;--c3:#FFC453;
|
| 4 |
+
--bg:#F6F7FB;--surface:#ffffff;--surface2:#F5F6FA;--line:#EBEDF3;
|
| 5 |
+
--txt:#12141C;--muted:#68707E;--faint:#9AA1B0;
|
| 6 |
--ok:#16C79A;--high:#F05454;--med:#FFC453;--low:#eab308;
|
| 7 |
+
--blue:#0018FF;--green:#16C79A;--red:#F05454;--amber:#FFC453;--violet:#7C3AED;--teal:#0EA5A0;
|
| 8 |
--penso-blue:#0018FF;--penso-bluetech:#0004DD;--penso-black:#171717;--penso-blackx:#060606;
|
| 9 |
+
--radius:20px;--radius-sm:13px;
|
| 10 |
+
--shadow:0 1px 2px rgba(16,24,40,.04),0 8px 24px -6px rgba(16,24,40,.10);
|
| 11 |
+
--shadow-md:0 4px 10px rgba(16,24,40,.05),0 16px 36px -8px rgba(16,24,40,.14);
|
| 12 |
+
--shadow-lg:0 20px 50px -12px rgba(6,6,20,.35);
|
| 13 |
+
--sb:260px;
|
| 14 |
+
--font:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',system-ui,Arial,sans-serif;
|
| 15 |
}
|
| 16 |
+
[data-theme="dark"]{--bg:#0A0B10;--surface:#14151F;--surface2:#191b28;--line:#262a3a;--txt:#EEF0F8;--muted:#9aa3c0;--faint:#6c7595;
|
| 17 |
+
--shadow:0 1px 2px rgba(0,0,0,.3),0 8px 24px -6px rgba(0,0,0,.45);
|
| 18 |
+
--shadow-md:0 4px 10px rgba(0,0,0,.35),0 16px 36px -8px rgba(0,0,0,.55);
|
| 19 |
+
--shadow-lg:0 26px 60px rgba(0,0,0,.6)}
|
| 20 |
*{box-sizing:border-box}
|
| 21 |
+
::selection{background:rgba(0,24,255,.18)}
|
| 22 |
+
::-webkit-scrollbar{width:10px;height:10px}
|
| 23 |
+
::-webkit-scrollbar-track{background:transparent}
|
| 24 |
+
::-webkit-scrollbar-thumb{background:var(--line);border-radius:999px;border:2px solid var(--bg)}
|
| 25 |
+
::-webkit-scrollbar-thumb:hover{background:var(--faint)}
|
| 26 |
+
body{margin:0;font-family:var(--font);color:var(--txt);letter-spacing:-.1px;
|
| 27 |
+
background:var(--bg);
|
| 28 |
+
min-height:100vh;-webkit-font-smoothing:antialiased}
|
| 29 |
a{color:var(--c1)}
|
| 30 |
.app{display:flex;min-height:100vh}
|
| 31 |
+
|
| 32 |
+
/* ---------------------------- Sidebar ---------------------------- */
|
| 33 |
+
.sidebar{width:var(--sb);background:linear-gradient(175deg,#050608 0%,#080B1E 38%,#0006C4 82%,#0018FF 130%);color:#fff;position:fixed;inset:0 auto 0 0;
|
| 34 |
+
display:flex;flex-direction:column;padding:20px 16px;z-index:40;transition:transform .28s cubic-bezier(.4,0,.2,1)}
|
| 35 |
+
.sb-brand{display:flex;align-items:center;gap:12px;padding:4px 8px 20px;margin-bottom:10px;border-bottom:1px solid rgba(255,255,255,.12)}
|
| 36 |
+
.sb-brand .logo{width:42px;height:42px;border-radius:14px;background:rgba(255,255,255,.14);display:grid;place-items:center;font-size:20px}
|
| 37 |
+
.sb-brand .penso-logo{background:linear-gradient(135deg,#ffffff,#eef2ff);color:#0018FF;font-weight:900;font-size:19px;font-family:var(--font);box-shadow:0 6px 16px rgba(0,0,0,.35)}
|
| 38 |
+
.sb-brand h1{font-size:15.5px;margin:0;font-weight:800;letter-spacing:-.2px} .sb-brand .s{font-size:11px;color:#B9C7FF;margin-top:1px}
|
| 39 |
+
nav{display:flex;flex-direction:column;gap:2px;overflow:auto;margin-top:8px;flex:1}
|
| 40 |
+
nav a{display:flex;align-items:center;gap:12px;color:#C7D0F0;text-decoration:none;padding:10px 12px;border-radius:13px;
|
| 41 |
+
font-size:13.5px;font-weight:600;cursor:pointer;transition:.16s ease;position:relative}
|
| 42 |
+
nav a .i{width:30px;height:30px;flex:none;text-align:center;font-size:15px;border-radius:9px;background:rgba(255,255,255,.06);
|
| 43 |
+
display:inline-flex;align-items:center;justify-content:center;transition:.16s ease}
|
| 44 |
+
nav a:hover{background:rgba(255,255,255,.08);color:#fff}
|
| 45 |
+
nav a:hover .i{background:rgba(255,255,255,.14)}
|
| 46 |
+
nav a.active{background:linear-gradient(100deg,#ffffff,#f3f5ff);color:#0018FF;box-shadow:0 8px 20px rgba(0,0,0,.28)}
|
| 47 |
+
nav a.active .i{background:rgba(0,24,255,.12)}
|
| 48 |
+
.sb-foot{margin-top:auto;padding-top:16px;display:flex;align-items:center;gap:10px;border-top:1px solid rgba(255,255,255,.12)}
|
| 49 |
+
.sb-foot img{width:38px;height:38px;border-radius:50%;border:2px solid rgba(255,255,255,.5)}
|
| 50 |
+
.sb-foot .n{font-size:12.5px;font-weight:700} .sb-foot .r{font-size:11px;color:#B9C7FF}
|
| 51 |
+
|
| 52 |
+
/* ---------------------------- Layout ---------------------------- */
|
| 53 |
.mainwrap{margin-left:var(--sb);flex:1;min-width:0;display:flex;flex-direction:column}
|
| 54 |
+
.topbar{position:sticky;top:0;z-index:30;background:rgba(255,255,255,.72);backdrop-filter:blur(14px) saturate(1.4);
|
| 55 |
+
border-bottom:1px solid var(--line);padding:14px 26px;display:flex;align-items:center;gap:10px;flex-wrap:wrap}
|
| 56 |
+
[data-theme="dark"] .topbar{background:rgba(15,16,26,.72)}
|
| 57 |
+
.hamb{display:none;background:var(--surface);border:1px solid var(--line);color:var(--txt);border-radius:12px;width:38px;height:38px;font-size:18px;cursor:pointer}
|
| 58 |
+
.vtitle{font-size:21px;font-weight:800;margin:0;letter-spacing:-.5px;
|
| 59 |
+
background:linear-gradient(90deg,var(--c1),#5b3bff);-webkit-background-clip:text;background-clip:text;color:transparent}
|
| 60 |
.spacer{flex:1}
|
| 61 |
+
.pill{display:flex;align-items:center;gap:7px;background:var(--surface);border:1px solid var(--line);color:var(--muted);font-size:12px;padding:7px 13px;border-radius:999px;font-weight:600;box-shadow:var(--shadow)}
|
| 62 |
+
.dot{width:8px;height:8px;border-radius:50%;background:#9aa3b2}
|
| 63 |
.dot.on{background:#22c55e;animation:pulse 1.8s infinite} .dot.off{background:#ef4444}
|
| 64 |
@keyframes pulse{0%{box-shadow:0 0 0 0 rgba(34,197,94,.5)}70%{box-shadow:0 0 0 8px rgba(34,197,94,0)}100%{box-shadow:0 0 0 0 rgba(34,197,94,0)}}
|
| 65 |
+
.sel{appearance:none;background:var(--surface);border:1px solid var(--line);color:var(--txt);border-radius:12px;
|
| 66 |
+
padding:9px 30px 9px 13px;font-size:13px;font-weight:600;cursor:pointer;box-shadow:var(--shadow);
|
| 67 |
+
background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='%2368707E'%3E%3Cpath d='M6 9 1 4h10z'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 11px center}
|
| 68 |
.sel:focus{outline:none;border-color:var(--c1)}
|
| 69 |
+
.iconbtn{width:38px;height:38px;border-radius:12px;border:1px solid var(--line);background:var(--surface);color:var(--txt);cursor:pointer;font-size:16px;box-shadow:var(--shadow);transition:.15s}
|
| 70 |
+
.iconbtn:hover{border-color:var(--c1);transform:translateY(-1px)}
|
| 71 |
+
.btn{border:0;border-radius:13px;padding:10px 19px;font-size:14px;font-weight:700;cursor:pointer;transition:.15s;display:inline-flex;align-items:center;gap:8px}
|
| 72 |
+
.btn:hover{filter:brightness(1.06);transform:translateY(-1px)} .btn:active{transform:translateY(1px)} .btn:disabled{opacity:.55;cursor:not-allowed;transform:none}
|
| 73 |
+
.btn-primary{background:linear-gradient(100deg,var(--c1),#4a3bff);color:#fff;box-shadow:0 10px 24px -6px rgba(0,24,255,.45)}
|
| 74 |
.btn-ghost{background:var(--surface);color:var(--c1);border:1px solid var(--line)}
|
| 75 |
+
.btn-ghost:hover{background:var(--surface2)}
|
| 76 |
+
.btn-sm{padding:8px 14px;font-size:12.5px;border-radius:11px}
|
| 77 |
+
main{padding:26px 28px 40px;flex:1;max-width:1400px;width:100%;margin:0 auto}
|
| 78 |
+
|
| 79 |
+
/* ---------------------------- Cards ---------------------------- */
|
| 80 |
+
.card{background:var(--surface);border:1px solid var(--line);border-radius:var(--radius);box-shadow:var(--shadow);padding:24px;margin-bottom:20px;animation:fadeUp .5s cubic-bezier(.16,1,.3,1) both}
|
| 81 |
+
@keyframes fadeUp{from{opacity:0;transform:translateY(14px)}to{opacity:1;transform:none}}
|
| 82 |
+
.sec-h{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:18px;flex-wrap:wrap}
|
| 83 |
+
.sh-left{display:flex;align-items:center;gap:12px}
|
| 84 |
+
.sh-ic{width:40px;height:40px;border-radius:13px;display:grid;place-items:center;font-size:19px;color:#fff;flex:none;box-shadow:0 6px 16px -4px rgba(0,0,0,.25)}
|
| 85 |
+
.sec-h h2{font-size:15.5px;margin:0;color:var(--txt);font-weight:800;letter-spacing:-.3px}
|
| 86 |
.meta{display:flex;gap:8px;flex-wrap:wrap}
|
| 87 |
+
.chip{background:var(--surface2);border:1px solid var(--line);color:var(--txt);border-radius:999px;padding:6px 13px;font-size:12.5px;font-weight:600}
|
| 88 |
.chip.good{background:#dcfce7;color:#15803d;border-color:#bbf7d0} .chip.bad{background:#ffe4e6;color:#be123c;border-color:#fecdd3}
|
| 89 |
+
.hero{border-radius:18px;padding:20px 22px;background:linear-gradient(135deg,rgba(0,24,255,.06),rgba(124,58,237,.05) 55%,rgba(255,196,83,.06));
|
| 90 |
+
border:1px solid rgba(0,24,255,.10);position:relative;margin-bottom:18px;overflow:hidden}
|
| 91 |
+
.hero::before{content:"";position:absolute;left:0;top:14px;bottom:14px;width:4px;border-radius:999px;background:linear-gradient(180deg,var(--c1),#7C3AED)}
|
| 92 |
+
.hero .h{font-size:11.5px;font-weight:800;color:var(--c1);text-transform:uppercase;letter-spacing:.07em;margin-bottom:8px;display:flex;align-items:center;gap:6px}
|
| 93 |
+
.hero p{margin:0 0 11px;font-size:14.5px;line-height:1.68;color:var(--txt)} .hero p:last-child{margin-bottom:0}
|
| 94 |
+
.hero strong{color:var(--c1)}
|
| 95 |
+
.skeleton{border-radius:12px;background:linear-gradient(100deg,var(--surface2) 30%,var(--line) 50%,var(--surface2) 70%);
|
| 96 |
background-size:200% 100%;animation:shimmer 1.4s infinite}
|
| 97 |
@keyframes shimmer{0%{background-position:200% 0}100%{background-position:-200% 0}}
|
| 98 |
@keyframes spin{from{transform:rotate(0deg)}to{transform:rotate(360deg)}}
|
| 99 |
.topload{height:3px;border-radius:999px;overflow:hidden;background:var(--line);margin-bottom:14px;position:relative}
|
| 100 |
.topload::after{content:"";position:absolute;inset:0;width:34%;border-radius:999px;background:linear-gradient(90deg,var(--c1),var(--c3));animation:slide 1.1s infinite}
|
| 101 |
+
.fade-view{animation:fadeUp .35s cubic-bezier(.16,1,.3,1) both}
|
| 102 |
.errcard{display:flex;align-items:flex-start;gap:12px}
|
| 103 |
.errcard .ic{font-size:22px;line-height:1}
|
| 104 |
.errcard .btns{display:flex;gap:8px;margin-top:10px}
|
| 105 |
+
|
| 106 |
+
/* ---------------------------- KPIs ---------------------------- */
|
| 107 |
+
.kpis{display:grid;grid-template-columns:repeat(auto-fit,minmax(178px,1fr));gap:14px}
|
| 108 |
+
.kpi{border:1px solid var(--line);border-radius:17px;padding:18px;background:var(--surface);transition:.2s cubic-bezier(.16,1,.3,1);position:relative;overflow:hidden}
|
| 109 |
+
.kpi::after{content:"";position:absolute;inset:0;border-radius:inherit;padding:1px;opacity:0;transition:.2s;pointer-events:none}
|
| 110 |
+
.kpi:hover{transform:translateY(-4px);box-shadow:var(--shadow-md);border-color:transparent}
|
| 111 |
+
.kpi .ic{width:44px;height:44px;border-radius:13px;display:grid;place-items:center;font-size:21px;margin-bottom:12px;box-shadow:0 6px 14px -4px rgba(0,0,0,.18)}
|
| 112 |
+
.kpi .lab{font-size:11.5px;color:var(--muted);font-weight:700;text-transform:uppercase;letter-spacing:.04em}
|
| 113 |
+
.kpi .val{font-size:24px;font-weight:800;margin-top:3px;letter-spacing:-.6px;color:var(--txt)}
|
| 114 |
+
.kpi.blue .ic{background:linear-gradient(135deg,#3b6bff,#0018FF);color:#fff} .kpi.blue .val{color:#0018FF}
|
| 115 |
+
.kpi.green .ic{background:linear-gradient(135deg,#34d399,#0d9488);color:#fff} .kpi.green .val{color:#0d9488}
|
| 116 |
+
.kpi.red .ic{background:linear-gradient(135deg,#fb7185,#e11d48);color:#fff} .kpi.red .val{color:#e11d48}
|
| 117 |
+
.kpi.amber .ic{background:linear-gradient(135deg,#fbbf24,#d97706);color:#fff} .kpi.amber .val{color:#b45309}
|
| 118 |
+
.kpi.violet .ic{background:linear-gradient(135deg,#a78bfa,#7c3aed);color:#fff} .kpi.violet .val{color:#7c3aed}
|
| 119 |
+
.kpi.teal .ic{background:linear-gradient(135deg,#5eead4,#0d9488);color:#fff} .kpi.teal .val{color:#0d9488}
|
| 120 |
.split{display:grid;grid-template-columns:1.4fr .6fr;gap:18px;align-items:start} @media(max-width:900px){.split{grid-template-columns:1fr}}
|
| 121 |
.filters{display:flex;gap:7px;flex-wrap:wrap;margin-bottom:14px}
|
| 122 |
+
.fchip{font-size:12px;padding:7px 14px;border-radius:999px;border:1px solid var(--line);background:var(--surface2);color:var(--muted);cursor:pointer;font-weight:700;transition:.15s}
|
| 123 |
+
.fchip:hover{border-color:var(--c1);color:var(--c1)}
|
| 124 |
+
.fchip.active{background:linear-gradient(100deg,var(--c1),#4a3bff);color:#fff;border-color:transparent;box-shadow:0 8px 18px -6px rgba(0,24,255,.4)}
|
| 125 |
+
|
| 126 |
+
/* ---------------------------- Alertas ---------------------------- */
|
| 127 |
+
.alert{border:1px solid var(--line);background:var(--surface);padding:15px 17px;margin-bottom:12px;border-radius:16px;
|
| 128 |
+
animation:fadeUp .4s cubic-bezier(.16,1,.3,1) both;position:relative;padding-left:20px;transition:.15s}
|
| 129 |
+
.alert::before{content:"";position:absolute;left:0;top:10px;bottom:10px;width:4px;border-radius:999px;background:#9aa3b2}
|
| 130 |
+
.alert:hover{box-shadow:var(--shadow)}
|
| 131 |
+
.alert .badge{display:inline-block;color:#fff;font-size:10px;font-weight:800;padding:3px 10px;border-radius:999px;margin-right:7px;letter-spacing:.03em}
|
| 132 |
+
.alert .t{font-weight:800;font-size:14.5px;letter-spacing:-.2px} .alert .d{color:var(--muted);font-size:13px;margin-top:6px;line-height:1.5} .alert .a{font-size:13px;margin-top:8px}
|
| 133 |
.alert details{margin-top:9px} .alert summary{cursor:pointer;font-size:12px;color:var(--c1);font-weight:700}
|
| 134 |
.recs{margin:9px 0 0;padding-left:18px} .recs li{font-size:12.5px;color:var(--muted);margin-bottom:4px} .recs a{color:var(--c1)}
|
| 135 |
+
.plan{white-space:pre-wrap;background:var(--surface2);border:1px dashed var(--line);border-radius:12px;padding:11px 13px;font-size:12.5px;margin-top:9px;line-height:1.5}
|
| 136 |
+
|
| 137 |
+
/* ---------------------------- Grades de destaque ---------------------------- */
|
| 138 |
+
.dgrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(255px,1fr));gap:14px}
|
| 139 |
+
.dcard{border:1px solid var(--line);border-radius:17px;background:var(--surface);overflow:hidden;box-shadow:var(--shadow);
|
| 140 |
+
transition:transform .2s cubic-bezier(.16,1,.3,1),box-shadow .2s;animation:fadeUp .45s cubic-bezier(.16,1,.3,1) both}
|
| 141 |
+
.dcard:hover{transform:translateY(-4px);box-shadow:var(--shadow-md)}
|
| 142 |
+
.dcard .dh{display:flex;justify-content:space-between;align-items:center;padding:13px 15px;font-size:13px;font-weight:800;color:#fff}
|
| 143 |
.dcard .dh .ct{background:rgba(255,255,255,.28);color:#fff;border-radius:999px;padding:2px 10px;font-size:11px}
|
| 144 |
+
.dcard ul{margin:0;padding:11px 15px 13px 29px;max-height:280px;overflow:auto} .dcard li{font-size:12.5px;margin-bottom:5px;line-height:1.5}
|
| 145 |
+
.dcard a{text-decoration:none;color:var(--c1);font-weight:600} .dcard a:hover{text-decoration:underline} .dcard .info{color:var(--faint)}
|
| 146 |
+
.dgrid .dcard:nth-child(1){animation-delay:.03s} .dgrid .dcard:nth-child(2){animation-delay:.08s}
|
| 147 |
+
.dgrid .dcard:nth-child(3){animation-delay:.13s} .dgrid .dcard:nth-child(4){animation-delay:.18s}
|
| 148 |
+
|
| 149 |
+
/* ---------------------------- Abas / painéis ---------------------------- */
|
| 150 |
.tabs{display:flex;gap:7px;flex-wrap:wrap;margin-bottom:14px}
|
| 151 |
+
.tab{padding:8px 15px;border-radius:999px;background:var(--surface2);border:1px solid var(--line);color:var(--muted);font-size:12.5px;font-weight:700;cursor:pointer;transition:.15s}
|
| 152 |
+
.tab:hover{color:var(--c1)}
|
| 153 |
+
.tab.active{background:linear-gradient(100deg,var(--c1),#4a3bff);color:#fff;border-color:transparent}
|
| 154 |
+
.panel{display:none;border:1px solid var(--line);border-radius:16px;padding:18px 22px;background:var(--surface);max-height:600px;overflow:auto}
|
| 155 |
.panel.active{display:block} .panel h2{font-size:16px;color:var(--c1)} .panel table{border-collapse:collapse;width:100%} .panel td,.panel th{border:1px solid var(--line);padding:6px 9px;font-size:13px}
|
| 156 |
table.owners{width:100%;border-collapse:collapse}
|
| 157 |
+
.owners th,.owners td{border-bottom:1px solid var(--line);padding:10px 11px;font-size:13px;text-align:left}
|
| 158 |
+
.owners th{color:var(--muted);font-size:11px;text-transform:uppercase;letter-spacing:.05em;font-weight:700}
|
| 159 |
+
.owners tbody tr{transition:.12s} .owners tbody tr:hover{background:var(--surface2)}
|
| 160 |
+
.empty{text-align:center;color:var(--faint);padding:48px 20px;font-size:14px}
|
| 161 |
+
.err{background:#ffe4e6;border:1px solid #fecdd3;color:#be123c;padding:14px 16px;border-radius:14px;font-size:14px;white-space:pre-wrap}
|
| 162 |
label.lbl{display:block;font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.05em;margin:0 0 7px;font-weight:700}
|
| 163 |
+
input[type=text],input[type=date],input[type=password],input[type=email]{width:100%;padding:11px 14px;border:1px solid var(--line);border-radius:13px;font-size:14px;background:var(--surface2);color:var(--txt);font-family:var(--font);transition:.15s}
|
| 164 |
+
input:focus{outline:none;border-color:var(--c1);box-shadow:0 0 0 4px rgba(0,24,255,.10);background:var(--surface)}
|
| 165 |
.switch{display:inline-flex;align-items:center;gap:10px;cursor:pointer;font-size:14px}
|
| 166 |
.switch input{display:none}.track{width:42px;height:24px;border-radius:999px;background:#cbd0e0;position:relative;transition:.2s}
|
| 167 |
[data-theme="dark"] .track{background:#39406a}
|
| 168 |
.track::after{content:"";position:absolute;top:3px;left:3px;width:18px;height:18px;border-radius:50%;background:#fff;transition:.2s}
|
| 169 |
+
.switch input:checked + .track{background:linear-gradient(100deg,var(--c1),#4a3bff)} .switch input:checked + .track::after{transform:translateX(18px)}
|
| 170 |
.bar{height:8px;background:var(--line);border-radius:999px;overflow:hidden;position:relative;margin:10px 0}
|
| 171 |
.bar::after{content:"";position:absolute;inset:0;width:38%;border-radius:999px;background:linear-gradient(90deg,var(--c1),var(--c3));animation:slide 1.25s infinite}
|
| 172 |
@keyframes slide{0%{transform:translateX(-110%)}100%{transform:translateX(330%)}}
|
| 173 |
+
.integr{display:flex;align-items:center;gap:18px;flex-wrap:wrap;justify-content:center;padding:20px;border-top:1px solid var(--line);margin-top:8px}
|
| 174 |
.integr span{font-size:11px;text-transform:uppercase;letter-spacing:.08em;color:var(--faint);font-weight:700}
|
| 175 |
+
.integr img{height:20px;opacity:.55;transition:.2s} .integr img:hover{opacity:1;transform:translateY(-2px)}
|
| 176 |
+
.backdrop{position:fixed;inset:0;background:rgba(10,8,20,.55);backdrop-filter:blur(6px);display:none;align-items:flex-start;justify-content:center;padding:60px 16px;z-index:60}
|
| 177 |
+
.backdrop.open{display:flex} .modal{background:var(--surface);border:1px solid var(--line);border-radius:20px;max-width:520px;width:100%;padding:26px;box-shadow:var(--shadow-lg);animation:fadeUp .3s cubic-bezier(.16,1,.3,1) both}
|
| 178 |
+
.modal .mh{display:flex;justify-content:space-between;align-items:center;margin-bottom:18px}.modal h3{margin:0;font-size:17px;letter-spacing:-.3px}
|
| 179 |
+
.modal .x{background:var(--surface2);border:1px solid var(--line);border-radius:11px;cursor:pointer;font-size:15px;padding:5px 11px;color:var(--txt)}
|
| 180 |
+
#toasts{position:fixed;right:20px;bottom:20px;display:flex;flex-direction:column;gap:9px;z-index:70}
|
| 181 |
+
.toast{background:var(--txt);color:var(--surface);padding:13px 17px;border-radius:14px;font-size:13px;box-shadow:var(--shadow-lg);animation:fadeUp .3s cubic-bezier(.16,1,.3,1)}
|
| 182 |
.toast.ok{background:linear-gradient(100deg,#16a34a,#22c55e);color:#fff} .toast.bad{background:linear-gradient(100deg,#e11d48,#f43f5e);color:#fff}
|
| 183 |
.cclist{list-style:none;margin:12px 0 0;padding:0;display:flex;flex-direction:column;gap:8px}
|
| 184 |
+
.ccrow{display:flex;align-items:center;justify-content:space-between;gap:10px;background:var(--surface2);border:1px solid var(--line);border-radius:12px;padding:10px 13px;font-size:13px}
|
| 185 |
.ccrow button{background:none;border:0;color:var(--red);cursor:pointer;font-size:13px;font-weight:700}
|
| 186 |
.ccadd{display:flex;gap:8px;margin-top:14px}
|
| 187 |
+
@media(max-width:860px){.sidebar{transform:translateX(-100%)} .sidebar.open{transform:none} .mainwrap{margin-left:0} .hamb{display:block} .split{grid-template-columns:1fr} main{padding:18px 16px 32px}}
|
| 188 |
|
| 189 |
+
/* ---- Reforço visual: contratos/reajuste, animações staggered ---- */
|
| 190 |
+
.disclaimer{margin-top:14px;padding:11px 14px;border-radius:12px;background:var(--surface2);border:1px dashed var(--line);color:var(--muted);font-size:12px;line-height:1.55}
|
| 191 |
.af-glow{position:relative;overflow:hidden}
|
| 192 |
.af-glow::before{content:"";position:absolute;inset:-2px;border-radius:var(--radius);padding:2px;
|
| 193 |
+
background:linear-gradient(120deg,var(--c1),var(--c3),#7c3aed);background-size:220% 220%;
|
| 194 |
-webkit-mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);-webkit-mask-composite:xor;mask-composite:exclude;
|
| 195 |
+
animation:af-borderflow 5s ease infinite;opacity:.5;pointer-events:none}
|
| 196 |
@keyframes af-borderflow{0%{background-position:0% 50%}50%{background-position:100% 50%}100%{background-position:0% 50%}}
|
| 197 |
+
.kpis .kpi{animation:fadeUp .45s cubic-bezier(.16,1,.3,1) both}
|
| 198 |
.kpis .kpi:nth-child(1){animation-delay:.02s} .kpis .kpi:nth-child(2){animation-delay:.06s}
|
| 199 |
.kpis .kpi:nth-child(3){animation-delay:.10s} .kpis .kpi:nth-child(4){animation-delay:.14s}
|
| 200 |
.kpis .kpi:nth-child(5){animation-delay:.18s} .kpis .kpi:nth-child(6){animation-delay:.22s}
|
| 201 |
.kpis .kpi:nth-child(7){animation-delay:.26s} .kpis .kpi:nth-child(8){animation-delay:.30s}
|
| 202 |
.kpis .kpi:nth-child(9){animation-delay:.34s} .kpis .kpi:nth-child(10){animation-delay:.38s}
|
| 203 |
.kpi:hover .ic{transform:scale(1.08) rotate(-4deg);transition:.2s}
|
|
|
|
|
|
|
|
|
|
|
|
frontend-react/src/views/Dashboard.jsx
CHANGED
|
@@ -5,7 +5,7 @@ import DoughnutChart from "../components/DoughnutChart.jsx";
|
|
| 5 |
import HighlightsCards from "../components/HighlightsCards.jsx";
|
| 6 |
import { moeda, num, pct, pipelineVal, extrairResumo, renderResumoHtml } from "../format.js";
|
| 7 |
|
| 8 |
-
const ACC = "#
|
| 9 |
|
| 10 |
function MetaChips({ dia }) {
|
| 11 |
const altos = (dia.alerts || []).filter((a) => a.severity === "high").length;
|
|
@@ -34,7 +34,6 @@ function MetaChips({ dia }) {
|
|
| 34 |
export default function Dashboard({ dia, onVerAlertas }) {
|
| 35 |
const L = dia.metrics?.leads || {};
|
| 36 |
const O = dia.metrics?.opportunities || {};
|
| 37 |
-
const T = dia.metrics?.tasks || {};
|
| 38 |
const Sa = dia.metrics?.satisfaction || {};
|
| 39 |
const C = dia.metrics?.cancellations || {};
|
| 40 |
const Co = dia.metrics?.contracts || {};
|
|
@@ -48,7 +47,6 @@ export default function Dashboard({ dia, onVerAlertas }) {
|
|
| 48 |
["🏆", "Ganhas", num(O.won_opportunities), "blue"],
|
| 49 |
["📉", "Perdidas", num(O.lost_opportunities), "red"],
|
| 50 |
["🚧", "Paradas", num(O.stalled_opportunities), "amber"],
|
| 51 |
-
["⏰", "Tarefas vencidas", num(T.tasks_overdue), "amber"],
|
| 52 |
];
|
| 53 |
if (Sa.configured && Sa.responses) cards.push(["😊", "Satisfação", num(Sa.avg_score), "teal"]);
|
| 54 |
if (C.configured && C.cancellations_count) cards.push(["❌", "Cancelamentos", num(C.cancellations_count), "red"]);
|
|
|
|
| 5 |
import HighlightsCards from "../components/HighlightsCards.jsx";
|
| 6 |
import { moeda, num, pct, pipelineVal, extrairResumo, renderResumoHtml } from "../format.js";
|
| 7 |
|
| 8 |
+
const ACC = "#0018FF";
|
| 9 |
|
| 10 |
function MetaChips({ dia }) {
|
| 11 |
const altos = (dia.alerts || []).filter((a) => a.severity === "high").length;
|
|
|
|
| 34 |
export default function Dashboard({ dia, onVerAlertas }) {
|
| 35 |
const L = dia.metrics?.leads || {};
|
| 36 |
const O = dia.metrics?.opportunities || {};
|
|
|
|
| 37 |
const Sa = dia.metrics?.satisfaction || {};
|
| 38 |
const C = dia.metrics?.cancellations || {};
|
| 39 |
const Co = dia.metrics?.contracts || {};
|
|
|
|
| 47 |
["🏆", "Ganhas", num(O.won_opportunities), "blue"],
|
| 48 |
["📉", "Perdidas", num(O.lost_opportunities), "red"],
|
| 49 |
["🚧", "Paradas", num(O.stalled_opportunities), "amber"],
|
|
|
|
| 50 |
];
|
| 51 |
if (Sa.configured && Sa.responses) cards.push(["😊", "Satisfação", num(Sa.avg_score), "teal"]);
|
| 52 |
if (C.configured && C.cancellations_count) cards.push(["❌", "Cancelamentos", num(C.cancellations_count), "red"]);
|
src/delivery/email_sender.py
CHANGED
|
@@ -24,9 +24,19 @@ logger = get_logger("delivery.email")
|
|
| 24 |
|
| 25 |
# ----------------------------------------------------------------------
|
| 26 |
# Identidade visual (Penso) — paleta oficial da marca.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
# ----------------------------------------------------------------------
|
| 28 |
-
# Cores "vivas" (uso em fundos/selos) e variantes escurecidas para texto
|
| 29 |
-
# sobre fundo branco (mantém contraste de leitura sem perder a identidade).
|
| 30 |
_PENSO_BLUE = "#0018FF"
|
| 31 |
_BLUE_TECH = "#0004DD"
|
| 32 |
_PENSO_BLACK = "#171717"
|
|
@@ -37,8 +47,10 @@ _ERRO = "#F05454"
|
|
| 37 |
_ERRO_TXT = "#c62f2f"
|
| 38 |
_ALERTA = "#FFC453"
|
| 39 |
_ALERTA_TXT = "#a15c00"
|
| 40 |
-
_DIVISORIA = "#
|
| 41 |
-
_HOVER = "#
|
|
|
|
|
|
|
| 42 |
|
| 43 |
# Logo do Salesforce (fonte de dados) — mesma URL pública já usada no painel React.
|
| 44 |
_LOGO_SALESFORCE_URL = "https://upload.wikimedia.org/wikipedia/commons/f/f9/Salesforce.com_logo.svg"
|
|
@@ -48,6 +60,20 @@ _COR_SEVERIDADE = {"high": _ERRO, "medium": _ALERTA, "low": "#94a3b8"}
|
|
| 48 |
# Texto do selo: escuro sobre o Alerta (amarelo) para manter contraste; branco nos demais.
|
| 49 |
_TEXTO_SEVERIDADE = {"high": "#ffffff", "medium": _PENSO_BLACK, "low": "#ffffff"}
|
| 50 |
_ROTULO_SEVERIDADE = {"high": "ALTA", "medium": "MÉDIA", "low": "BAIXA"}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
|
| 52 |
|
| 53 |
# ----------------------------------------------------------------------
|
|
@@ -114,11 +140,11 @@ def _md_para_html(texto: str) -> str:
|
|
| 114 |
def _flush() -> None:
|
| 115 |
if bullets:
|
| 116 |
itens = "".join(
|
| 117 |
-
f"<li style='margin-bottom:
|
| 118 |
)
|
| 119 |
html.append(
|
| 120 |
-
f"<ul style='margin:
|
| 121 |
-
f"line-height:1.
|
| 122 |
)
|
| 123 |
bullets.clear()
|
| 124 |
|
|
@@ -133,38 +159,99 @@ def _md_para_html(texto: str) -> str:
|
|
| 133 |
if linha.startswith("#"):
|
| 134 |
_flush()
|
| 135 |
html.append(
|
| 136 |
-
f"<div style='font-weight:
|
| 137 |
-
f"font-size:
|
| 138 |
)
|
| 139 |
elif linha[:2] in ("- ", "* ") or linha.startswith("•"):
|
| 140 |
bullets.append(_md_inline(linha.lstrip("-*• ").strip()))
|
| 141 |
else:
|
| 142 |
_flush()
|
| 143 |
html.append(
|
| 144 |
-
f"<p style='margin:0 0
|
| 145 |
f"color:#334155'>{_md_inline(linha)}</p>"
|
| 146 |
)
|
| 147 |
_flush()
|
| 148 |
return "".join(html)
|
| 149 |
|
| 150 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 151 |
# ----------------------------------------------------------------------
|
| 152 |
# Blocos do e-mail HTML
|
| 153 |
# ----------------------------------------------------------------------
|
| 154 |
-
def
|
| 155 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 156 |
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 160 |
"""
|
|
|
|
| 161 |
return (
|
| 162 |
-
'<td class="af-kpi-cell" style="padding:
|
| 163 |
-
'border-radius:
|
|
|
|
| 164 |
f'<div class="af-kpi-label" style="font-size:11px;color:#64748b;text-transform:uppercase;'
|
| 165 |
-
f'letter-spacing:.
|
| 166 |
-
f'<div style="font-size:
|
| 167 |
-
f'margin-top:
|
| 168 |
)
|
| 169 |
|
| 170 |
|
|
@@ -173,6 +260,7 @@ def _grade_kpis(metrics: dict[str, Any]) -> str:
|
|
| 173 |
|
| 174 |
Não inclui mais "Tarefas vencidas" (removido a pedido — volume alto
|
| 175 |
demais para um KPI de topo); em seu lugar entram "Oportunidades criadas".
|
|
|
|
| 176 |
"""
|
| 177 |
leads = metrics.get("leads", {}) or {}
|
| 178 |
opp = metrics.get("opportunities", {}) or {}
|
|
@@ -181,35 +269,39 @@ def _grade_kpis(metrics: dict[str, Any]) -> str:
|
|
| 181 |
contratos = metrics.get("contracts", {}) or {}
|
| 182 |
|
| 183 |
celulas: list[str] = [
|
| 184 |
-
_linha_kpi("Leads novos", _num(leads.get("new_leads"))),
|
| 185 |
-
_linha_kpi("Conversão de leads", _pct(leads.get("conversion_rate"))),
|
| 186 |
_linha_kpi(
|
|
|
|
| 187 |
"Pipeline aberto",
|
| 188 |
_moeda(opp.get("open_pipeline_product_value") or opp.get("open_pipeline_amount")),
|
|
|
|
| 189 |
),
|
| 190 |
-
_linha_kpi("Oportunidades criadas", _num(opp.get("new_opportunities"))),
|
| 191 |
-
_linha_kpi("Oportunidades ganhas", _num(opp.get("won_opportunities")),
|
| 192 |
-
_linha_kpi("Oportunidades perdidas", _num(opp.get("lost_opportunities")),
|
| 193 |
-
_linha_kpi("Oportunidades paradas", _num(opp.get("stalled_opportunities")),
|
| 194 |
]
|
| 195 |
if sat.get("configured") and sat.get("responses"):
|
| 196 |
-
celulas.append(_linha_kpi("Satisfação (nota média)", _num(sat.get("avg_score"))))
|
| 197 |
if canc.get("configured") and canc.get("cancellations_count"):
|
| 198 |
celulas.append(
|
| 199 |
-
_linha_kpi("Cancelamentos", _num(canc.get("cancellations_count")),
|
| 200 |
)
|
| 201 |
if contratos.get("configured"):
|
| 202 |
-
celulas.append(
|
|
|
|
|
|
|
| 203 |
|
| 204 |
# Distribui as células em linhas de 3 colunas.
|
| 205 |
linhas_html: list[str] = []
|
| 206 |
for i in range(0, len(celulas), 3):
|
| 207 |
grupo = celulas[i : i + 3]
|
| 208 |
linhas_html.append(
|
| 209 |
-
|
| 210 |
-
+ ('<td></td>' * (3 - len(grupo)))
|
| 211 |
-
+
|
| 212 |
-
+ '<tr><td colspan="3" style="height:
|
| 213 |
)
|
| 214 |
return (
|
| 215 |
'<table role="presentation" width="100%" cellspacing="0" cellpadding="0" '
|
|
@@ -233,27 +325,26 @@ def _bloco_contratos(metrics: dict[str, Any]) -> str:
|
|
| 233 |
modificados = m.get("modified_today") or []
|
| 234 |
if modificados:
|
| 235 |
itens = "".join(
|
| 236 |
-
f"<li style='margin-bottom:
|
| 237 |
f"<strong>{_escape(c.get('name') or c.get('id') or '—')}</strong>"
|
| 238 |
f" <span style='color:#64748b'>— modificado por "
|
| 239 |
f"{_escape(c.get('modified_by') or 'responsável não identificado')}</span></li>"
|
| 240 |
for c in modificados[:15]
|
| 241 |
)
|
| 242 |
-
|
| 243 |
-
f"<div
|
| 244 |
-
f"<div style='font-size:13px;font-weight:700;color:#0f172a;margin-bottom:6px;'>"
|
| 245 |
f"📄 Contratos modificados hoje "
|
| 246 |
f"<span style='color:#94a3b8;font-weight:400'>({m.get('modified_today_count', 0)})</span></div>"
|
| 247 |
-
f"<ul style='margin:0;padding-left:18px;font-size:13px;color:#334155'>{itens}</ul>"
|
| 248 |
-
f"</div>"
|
| 249 |
)
|
|
|
|
| 250 |
|
| 251 |
if m.get("readjustment_configured"):
|
| 252 |
qtd = m.get("readjustment_month_count") or 0
|
| 253 |
if qtd:
|
| 254 |
inconsist = int(m.get("readjustment_inconsistent_count") or 0)
|
| 255 |
reaj_itens = "".join(
|
| 256 |
-
f"<li style='margin-bottom:
|
| 257 |
f"<strong>{_escape(c.get('name') or c.get('id') or '—')}</strong>: "
|
| 258 |
f"{_moeda(c.get('previous_value'))} → {_moeda(c.get('current_value'))} "
|
| 259 |
f"(<span style='color:{_SUCESSO_TXT if (c.get('delta') or 0) >= 0 else _ERRO_TXT}'>"
|
|
@@ -267,13 +358,12 @@ def _bloco_contratos(metrics: dict[str, Any]) -> str:
|
|
| 267 |
+ ")</li>"
|
| 268 |
for c in (m.get("readjustment_contracts") or [])[:10]
|
| 269 |
)
|
| 270 |
-
|
| 271 |
-
f"<div
|
| 272 |
-
f"<div style='font-size:13px;font-weight:700;color:#0f172a;margin-bottom:6px;'>"
|
| 273 |
f"💹 Reajuste de contratos no mês "
|
| 274 |
f"<span style='color:#94a3b8;font-weight:400'>({qtd} contrato(s) · "
|
| 275 |
f"{_moeda(m.get('readjustment_month_total'))})</span></div>"
|
| 276 |
-
f"<ul style='margin:0 0 8px;padding-left:18px;font-size:13px;color:#334155'>{reaj_itens}</ul>"
|
| 277 |
+ (
|
| 278 |
f"<div style='font-size:12px;color:{_ALERTA_TXT};margin-bottom:6px;'>"
|
| 279 |
f"⚠ {inconsist} contrato(s) com possível inconsistência entre o reajuste "
|
|
@@ -282,8 +372,8 @@ def _bloco_contratos(metrics: dict[str, Any]) -> str:
|
|
| 282 |
else ""
|
| 283 |
)
|
| 284 |
+ f"<div style='font-size:11px;color:#94a3b8;'>⚠ {_escape(m.get('readjustment_disclaimer', ''))}</div>"
|
| 285 |
-
+ "</div>"
|
| 286 |
)
|
|
|
|
| 287 |
else:
|
| 288 |
partes.append(
|
| 289 |
f"<div style='font-size:12px;color:#94a3b8;margin-bottom:10px;'>"
|
|
@@ -292,11 +382,7 @@ def _bloco_contratos(metrics: dict[str, Any]) -> str:
|
|
| 292 |
|
| 293 |
if not partes:
|
| 294 |
return ""
|
| 295 |
-
return (
|
| 296 |
-
"<h3 class='af-h' style=\"font-size:14px;color:#0f172a;margin:22px 0 10px;padding-left:10px;"
|
| 297 |
-
f"border-left:3px solid {_PENSO_BLUE};\">📄 Contratos</h3>"
|
| 298 |
-
+ "".join(partes)
|
| 299 |
-
)
|
| 300 |
|
| 301 |
|
| 302 |
def _bloco_alertas(alerts: list[dict[str, Any]]) -> str:
|
|
@@ -304,9 +390,10 @@ def _bloco_alertas(alerts: list[dict[str, Any]]) -> str:
|
|
| 304 |
altos = [a for a in alerts if a.get("severity") == "high"]
|
| 305 |
selecionados = (altos or alerts)[:6]
|
| 306 |
if not selecionados:
|
| 307 |
-
return (
|
| 308 |
-
|
| 309 |
-
"✅ Nenhum alerta crítico para o período.</p>"
|
|
|
|
| 310 |
)
|
| 311 |
|
| 312 |
cartoes: list[str] = []
|
|
@@ -315,24 +402,38 @@ def _bloco_alertas(alerts: list[dict[str, Any]]) -> str:
|
|
| 315 |
cor = _COR_SEVERIDADE.get(sev, "#94a3b8")
|
| 316 |
cor_texto_selo = _TEXTO_SEVERIDADE.get(sev, "#ffffff")
|
| 317 |
rotulo = _ROTULO_SEVERIDADE.get(sev, "INFO")
|
|
|
|
| 318 |
acao = a.get("recommended_action")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 319 |
acao_html = (
|
| 320 |
-
f'<div style="margin-top:
|
| 321 |
f'<strong>Ação:</strong> {_escape(acao)}</div>'
|
| 322 |
if acao
|
| 323 |
else ""
|
| 324 |
)
|
| 325 |
-
|
| 326 |
-
f'
|
| 327 |
-
f'
|
| 328 |
-
f'<span style="display:inline-block;background:{cor};color:{cor_texto_selo};'
|
| 329 |
-
f'font-size:10px;font-weight:700;padding:2px 8px;border-radius:10px;'
|
| 330 |
-
f'letter-spacing:.04em;">{rotulo}</span> '
|
| 331 |
-
f'<span style="font-weight:700;color:#0f172a;font-size:14px;">'
|
| 332 |
f'{_escape(a.get("title", ""))}</span>'
|
| 333 |
-
f'<div style="margin-top:
|
| 334 |
f'{_escape(a.get("description", ""))}</div>'
|
| 335 |
-
f'{acao_html}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 336 |
)
|
| 337 |
return "".join(cartoes)
|
| 338 |
|
|
@@ -343,13 +444,11 @@ def _bloco_prioridades(alerts: list[dict[str, Any]]) -> str:
|
|
| 343 |
if not acoes:
|
| 344 |
return ""
|
| 345 |
itens = "".join(
|
| 346 |
-
f'<li style="margin-bottom:
|
| 347 |
for a in acoes[:5]
|
| 348 |
)
|
| 349 |
-
return (
|
| 350 |
-
'<
|
| 351 |
-
"🎯 Prioridades para hoje</h3>"
|
| 352 |
-
f'<ol style="margin:0;padding-left:20px;">{itens}</ol>'
|
| 353 |
)
|
| 354 |
|
| 355 |
|
|
@@ -359,8 +458,9 @@ def _estilo_cabecalho() -> str:
|
|
| 359 |
Usa classes com ``!important`` para sobrepor os estilos inline em
|
| 360 |
clientes que suportam ``<style>``/media queries (Apple Mail, Gmail
|
| 361 |
app/web, Outlook.com/iOS). Clientes que ignoram ``<style>`` (ex.: Outlook
|
| 362 |
-
desktop clássico) continuam vendo o layout claro fixo via
|
| 363 |
-
quebram, apenas não ganham o tema escuro/
|
|
|
|
| 364 |
"""
|
| 365 |
return f"""\
|
| 366 |
<meta name="color-scheme" content="light dark">
|
|
@@ -381,9 +481,10 @@ def _estilo_cabecalho() -> str:
|
|
| 381 |
.af-card {{ background:#141a2b !important; border-color:#242c42 !important; }}
|
| 382 |
.af-body h3 {{ color:#e8ecf5 !important; }}
|
| 383 |
.af-body p, .af-body div, .af-body li, .af-body span {{ color:#c3ccdf !important; }}
|
| 384 |
-
.af-kpi-cell {{ background:#1a2138 !important;
|
| 385 |
.af-kpi-label {{ color:#8fa0c2 !important; }}
|
| 386 |
-
.af-altcard {{ background:#171e33 !important; }}
|
|
|
|
| 387 |
.af-footer {{ background:#0e1424 !important; border-color:#242c42 !important; }}
|
| 388 |
.af-muted {{ color:#7d8aa8 !important; }}
|
| 389 |
a.af-link {{ color:#9fb6ff !important; }}
|
|
@@ -400,50 +501,68 @@ def _montar_html(
|
|
| 400 |
) -> str:
|
| 401 |
"""Monta o e-mail executivo completo (documento HTML, pt-BR).
|
| 402 |
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 406 |
"""
|
| 407 |
altos = sum(1 for a in alerts if a.get("severity") == "high")
|
| 408 |
resumo = (
|
| 409 |
f"Foram gerados <strong>{len(alerts)} alerta(s)</strong>, "
|
| 410 |
f"sendo <strong>{altos} de severidade alta</strong>."
|
| 411 |
)
|
| 412 |
-
# Bloco de análise (insights) — converte o Markdown do
|
|
|
|
|
|
|
|
|
|
| 413 |
bloco_ia = ""
|
| 414 |
corpo_ia = _md_para_html(resumo_ia) if resumo_ia.strip() else ""
|
| 415 |
if corpo_ia:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 416 |
bloco_ia = (
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
f
|
| 420 |
-
|
| 421 |
-
f"{corpo_ia}"
|
| 422 |
-
"</
|
| 423 |
)
|
| 424 |
# Cabeçalho: identidade visual da Penso (marca-texto) + Analytical-Force.
|
| 425 |
# TODO: quando o arquivo do logo real da Penso estiver disponível, trocar
|
| 426 |
# este bloco por um <img> com o PNG/SVG oficial (base64 ou hospedado).
|
| 427 |
# Por ora, usa apenas cores da marca (Penso Blue/Black) — sem inventar formas.
|
| 428 |
cabecalho = f"""\
|
| 429 |
-
<td class="af-px af-animate" style="background
|
|
|
|
| 430 |
<table role="presentation" cellspacing="0" cellpadding="0"><tr>
|
| 431 |
-
<td style="width:
|
| 432 |
-
vertical-align:middle;font-weight:
|
| 433 |
-
<td style="padding-left:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 434 |
</tr></table>
|
| 435 |
-
<div style="color:#ffffff;font-size:22px;font-weight:800;letter-spacing:.2px;margin-top:12px;">📊 Analytical-Force</div>
|
| 436 |
-
<div style="color:#dbe4ff;font-size:13px;margin-top:4px;">Relatório diário de performance comercial</div>
|
| 437 |
-
<span style="display:inline-block;margin-top:12px;background:rgba(255,255,255,.18);color:#ffffff;
|
| 438 |
-
font-size:12px;font-weight:600;padding:5px 12px;border-radius:999px;">📅 {_escape(report_date)}</span>
|
| 439 |
</td>"""
|
| 440 |
rodape = f"""\
|
| 441 |
-
<td class="af-footer af-px" style="background:{_HOVER};padding:
|
| 442 |
<table role="presentation" cellspacing="0" cellpadding="0"><tr>
|
| 443 |
-
<td style="font-size:11px;color:#94a3b8;text-transform:uppercase;letter-spacing:.
|
| 444 |
<td><img src="{_LOGO_SALESFORCE_URL}" alt="Salesforce" style="height:14px;vertical-align:middle;" /></td>
|
| 445 |
</tr></table>
|
| 446 |
-
<div class="af-muted" style="font-size:12px;color:#94a3b8;margin-top:
|
| 447 |
Relatório gerado automaticamente pelo agente <strong>Analytical-Force</strong> (Penso).
|
| 448 |
Os números são calculados em Python a partir do Salesforce; a interpretação
|
| 449 |
é gerada por modelo local. O relatório completo está anexado/registrado no sistema.
|
|
@@ -457,27 +576,28 @@ def _montar_html(
|
|
| 457 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 458 |
{_estilo_cabecalho()}
|
| 459 |
</head>
|
| 460 |
-
<body class="af-wrap" style="margin:0;padding:0;background:#
|
| 461 |
-
<div class="af-wrap" style="background:#
|
| 462 |
<table role="presentation" width="640" align="center" cellspacing="0" cellpadding="0" class="af-container"
|
| 463 |
style="max-width:640px;margin:0 auto;">
|
| 464 |
-
<tr><td class="af-card" style="background:#ffffff;border-radius:
|
|
|
|
| 465 |
<table role="presentation" width="100%" cellspacing="0" cellpadding="0">
|
| 466 |
<tr>
|
| 467 |
{cabecalho}
|
| 468 |
</tr>
|
| 469 |
<tr>
|
| 470 |
-
<td class="af-body af-px" style="padding:
|
| 471 |
-
|
| 472 |
-
<p style="font-size:14px;color:#475569;margin:0 0 14px;line-height:1.
|
| 473 |
{bloco_ia}
|
| 474 |
|
| 475 |
-
|
| 476 |
{_grade_kpis(metrics)}
|
| 477 |
|
| 478 |
{_bloco_contratos(metrics)}
|
| 479 |
|
| 480 |
-
|
| 481 |
{_bloco_alertas(alerts)}
|
| 482 |
|
| 483 |
{_bloco_prioridades(alerts)}
|
|
@@ -583,23 +703,22 @@ def _bloco_destaques_html(highlights: dict[str, list[dict[str, Any]]]) -> str:
|
|
| 583 |
info = f" <span style='color:#64748b'>({_escape(r['info'])})</span>" if r.get("info") else ""
|
| 584 |
url = r.get("url")
|
| 585 |
alvo = (
|
| 586 |
-
f"<a href='{_escape(url)}' style='color:
|
| 587 |
if url
|
| 588 |
else nome
|
| 589 |
)
|
| 590 |
-
linhas_li.append(f"<li style='margin-bottom:
|
| 591 |
-
|
| 592 |
-
f"<div style='margin-bottom:
|
| 593 |
-
f"<div style='font-size:13px;font-weight:700;color:#0f172a;margin-bottom:4px'>"
|
| 594 |
f"{rotulo} <span style='color:#94a3b8;font-weight:400'>({len(itens)})</span></div>"
|
| 595 |
-
f"<ul style='margin:0;padding-left:18px;font-size:13px;color:#334155'>{''.join(linhas_li)}</ul>"
|
| 596 |
-
f"</div>"
|
| 597 |
)
|
|
|
|
| 598 |
if not secoes:
|
| 599 |
return ""
|
| 600 |
return (
|
| 601 |
-
"
|
| 602 |
-
"<div style='font-size:12px;color:#94a3b8;margin-bottom:
|
| 603 |
"Links diretos no Salesforce (tarefas vencidas omitidas pelo volume).</div>"
|
| 604 |
+ "".join(secoes)
|
| 605 |
)
|
|
|
|
| 24 |
|
| 25 |
# ----------------------------------------------------------------------
|
| 26 |
# Identidade visual (Penso) — paleta oficial da marca.
|
| 27 |
+
#
|
| 28 |
+
# Direção visual "SaaS moderno claro": fundo claro, cartões com cantos bem
|
| 29 |
+
# arredondados, sombra suave e acentos em gradiente azul Penso — a mesma
|
| 30 |
+
# linguagem usada no painel React (Linear/Notion/Stripe Dashboard-like).
|
| 31 |
+
#
|
| 32 |
+
# IMPORTANTE (compatibilidade de e-mail): clientes como Outlook desktop
|
| 33 |
+
# (motor Word) e até o próprio Gmail em certos contextos IGNORAM cor de
|
| 34 |
+
# fundo aplicada via ``<div style="background:...">``. A técnica segura
|
| 35 |
+
# ("bulletproof") é usar tabelas com o atributo HTML ``bgcolor`` + a
|
| 36 |
+
# propriedade CSS ``background-color`` explícita (nunca só o shorthand
|
| 37 |
+
# ``background``) em toda célula colorida. Por isso todo bloco com fundo
|
| 38 |
+
# sólido abaixo usa ``<table><tr><td bgcolor="...">`` em vez de ``<div>``.
|
| 39 |
# ----------------------------------------------------------------------
|
|
|
|
|
|
|
| 40 |
_PENSO_BLUE = "#0018FF"
|
| 41 |
_BLUE_TECH = "#0004DD"
|
| 42 |
_PENSO_BLACK = "#171717"
|
|
|
|
| 47 |
_ERRO_TXT = "#c62f2f"
|
| 48 |
_ALERTA = "#FFC453"
|
| 49 |
_ALERTA_TXT = "#a15c00"
|
| 50 |
+
_DIVISORIA = "#EBEDF3"
|
| 51 |
+
_HOVER = "#F5F6FA"
|
| 52 |
+
_RADIUS_CARD = "20px"
|
| 53 |
+
_RADIUS_CHIP = "13px"
|
| 54 |
|
| 55 |
# Logo do Salesforce (fonte de dados) — mesma URL pública já usada no painel React.
|
| 56 |
_LOGO_SALESFORCE_URL = "https://upload.wikimedia.org/wikipedia/commons/f/f9/Salesforce.com_logo.svg"
|
|
|
|
| 60 |
# Texto do selo: escuro sobre o Alerta (amarelo) para manter contraste; branco nos demais.
|
| 61 |
_TEXTO_SEVERIDADE = {"high": "#ffffff", "medium": _PENSO_BLACK, "low": "#ffffff"}
|
| 62 |
_ROTULO_SEVERIDADE = {"high": "ALTA", "medium": "MÉDIA", "low": "BAIXA"}
|
| 63 |
+
# Fundo (tint) do cartão de alerta por severidade — sutil, mesma cor do selo a 8%.
|
| 64 |
+
_FUNDO_SEVERIDADE = {"high": "#fff1f2", "medium": "#fffbeb", "low": "#f8fafc"}
|
| 65 |
+
|
| 66 |
+
# Paleta de categorias dos KPIs — espelha exatamente as cores usadas no painel
|
| 67 |
+
# React (StatGrid/Dashboard.jsx: blue/green/red/amber/violet/teal), para que
|
| 68 |
+
# e-mail e front falem a mesma linguagem visual.
|
| 69 |
+
_KPI_ESTILO: dict[str, dict[str, str]] = {
|
| 70 |
+
"blue": {"bg": "#EEF1FF", "icon": _PENSO_BLUE, "text": _PENSO_BLUE},
|
| 71 |
+
"green": {"bg": "#E7FBF4", "icon": "#0d9488", "text": "#0d9488"},
|
| 72 |
+
"red": {"bg": "#FFF1F2", "icon": "#e11d48", "text": "#e11d48"},
|
| 73 |
+
"amber": {"bg": "#FFF8E8", "icon": "#b45309", "text": "#b45309"},
|
| 74 |
+
"violet": {"bg": "#F5F0FF", "icon": "#7c3aed", "text": "#7c3aed"},
|
| 75 |
+
"teal": {"bg": "#E9FBFA", "icon": "#0d9488", "text": "#0d9488"},
|
| 76 |
+
}
|
| 77 |
|
| 78 |
|
| 79 |
# ----------------------------------------------------------------------
|
|
|
|
| 140 |
def _flush() -> None:
|
| 141 |
if bullets:
|
| 142 |
itens = "".join(
|
| 143 |
+
f"<li style='margin-bottom:5px'>{b}</li>" for b in bullets
|
| 144 |
)
|
| 145 |
html.append(
|
| 146 |
+
f"<ul style='margin:6px 0 12px;padding-left:20px;font-size:14.5px;"
|
| 147 |
+
f"line-height:1.7;color:#334155'>{itens}</ul>"
|
| 148 |
)
|
| 149 |
bullets.clear()
|
| 150 |
|
|
|
|
| 159 |
if linha.startswith("#"):
|
| 160 |
_flush()
|
| 161 |
html.append(
|
| 162 |
+
f"<div style='font-weight:800;color:#0f172a;margin:14px 0 5px;"
|
| 163 |
+
f"font-size:14.5px;'>{_md_inline(sem_hash)}</div>"
|
| 164 |
)
|
| 165 |
elif linha[:2] in ("- ", "* ") or linha.startswith("•"):
|
| 166 |
bullets.append(_md_inline(linha.lstrip("-*• ").strip()))
|
| 167 |
else:
|
| 168 |
_flush()
|
| 169 |
html.append(
|
| 170 |
+
f"<p style='margin:0 0 12px;font-size:14.5px;line-height:1.75;"
|
| 171 |
f"color:#334155'>{_md_inline(linha)}</p>"
|
| 172 |
)
|
| 173 |
_flush()
|
| 174 |
return "".join(html)
|
| 175 |
|
| 176 |
|
| 177 |
+
# ----------------------------------------------------------------------
|
| 178 |
+
# Blocos "bulletproof" (tabela + bgcolor) — bloco colorido sólido que
|
| 179 |
+
# renderiza de forma consistente em Gmail, Outlook (desktop/web/iOS/Android),
|
| 180 |
+
# Apple Mail e Yahoo, ao contrário de ``<div style="background:...">`` puro.
|
| 181 |
+
# ----------------------------------------------------------------------
|
| 182 |
+
def _caixa(
|
| 183 |
+
conteudo: str,
|
| 184 |
+
cor_fundo: str,
|
| 185 |
+
*,
|
| 186 |
+
radius: str = _RADIUS_CHIP,
|
| 187 |
+
padding: str = "14px 16px",
|
| 188 |
+
classe: str = "af-altcard",
|
| 189 |
+
borda_esquerda: str | None = None,
|
| 190 |
+
) -> str:
|
| 191 |
+
"""Envolve ``conteudo`` em uma célula de tabela com fundo sólido.
|
| 192 |
+
|
| 193 |
+
Usa ``bgcolor`` (atributo HTML clássico, suportado até por clientes sem
|
| 194 |
+
CSS) + ``background-color`` inline — a combinação mais compatível para
|
| 195 |
+
pintar o fundo de um cartão em e-mail.
|
| 196 |
+
"""
|
| 197 |
+
borda = f"border-left:4px solid {borda_esquerda};" if borda_esquerda else ""
|
| 198 |
+
return (
|
| 199 |
+
'<table role="presentation" width="100%" cellspacing="0" cellpadding="0" '
|
| 200 |
+
'style="margin-bottom:10px;"><tr>'
|
| 201 |
+
f'<td class="{classe}" bgcolor="{cor_fundo}" style="background-color:{cor_fundo};'
|
| 202 |
+
f'border-radius:{radius};{borda}padding:{padding};">{conteudo}</td>'
|
| 203 |
+
"</tr></table>"
|
| 204 |
+
)
|
| 205 |
+
|
| 206 |
+
|
| 207 |
+
def _selo_icone(icone: str, cor: str, *, tamanho: int = 30, raio: int = 10, fonte: int = 14) -> str:
|
| 208 |
+
"""Selo quadrado colorido (ícone) — tabela+bgcolor, nunca ``<div>``."""
|
| 209 |
+
return (
|
| 210 |
+
'<table role="presentation" cellspacing="0" cellpadding="0"><tr>'
|
| 211 |
+
f'<td bgcolor="{cor}" width="{tamanho}" height="{tamanho}" '
|
| 212 |
+
f'style="width:{tamanho}px;height:{tamanho}px;border-radius:{raio}px;background-color:{cor};'
|
| 213 |
+
f'text-align:center;vertical-align:middle;font-size:{fonte}px;line-height:{tamanho}px;">{icone}</td>'
|
| 214 |
+
"</tr></table>"
|
| 215 |
+
)
|
| 216 |
+
|
| 217 |
+
|
| 218 |
# ----------------------------------------------------------------------
|
| 219 |
# Blocos do e-mail HTML
|
| 220 |
# ----------------------------------------------------------------------
|
| 221 |
+
def _secao_titulo(icone: str, titulo: str, cor: str = _PENSO_BLUE) -> str:
|
| 222 |
+
"""Cabeçalho de seção no estilo "chip de ícone" (mesma linguagem do painel).
|
| 223 |
+
|
| 224 |
+
Substitui o antigo título com apenas uma borda lateral por um selo
|
| 225 |
+
colorido arredondado + texto em negrito — mais próximo do design system
|
| 226 |
+
"SaaS moderno claro" usado no front (``.sh-ic`` / ``.sec-h``).
|
| 227 |
+
"""
|
| 228 |
+
return (
|
| 229 |
+
'<table role="presentation" cellspacing="0" cellpadding="0" style="margin:26px 0 13px;"><tr>'
|
| 230 |
+
f'<td>{_selo_icone(icone, cor)}</td>'
|
| 231 |
+
f'<td style="padding-left:11px;font-size:15px;font-weight:800;color:#0f172a;'
|
| 232 |
+
f'letter-spacing:-.2px;">{_escape(titulo)}</td>'
|
| 233 |
+
"</tr></table>"
|
| 234 |
+
)
|
| 235 |
+
|
| 236 |
|
| 237 |
+
def _linha_kpi(icone: str, rotulo: str, valor: str, categoria: str = "blue") -> str:
|
| 238 |
+
"""Célula de KPI (ícone + rótulo + valor) com o cartão tintado por categoria.
|
| 239 |
+
|
| 240 |
+
Cada categoria usa a mesma paleta do painel React (azul/verde/vermelho/
|
| 241 |
+
âmbar/violeta/teal), com fundo suavemente tintado e cantos bem
|
| 242 |
+
arredondados. O quadrado do ícone e o fundo da célula usam a técnica
|
| 243 |
+
tabela+``bgcolor`` para pintar de forma confiável em qualquer cliente de
|
| 244 |
+
e-mail (inclusive Outlook desktop, que ignora fundo de ``<div>``).
|
| 245 |
"""
|
| 246 |
+
estilo = _KPI_ESTILO.get(categoria, _KPI_ESTILO["blue"])
|
| 247 |
return (
|
| 248 |
+
f'<td class="af-kpi-cell" bgcolor="{estilo["bg"]}" style="padding:15px 14px 14px;'
|
| 249 |
+
f'border-radius:14px;background-color:{estilo["bg"]};vertical-align:top;">'
|
| 250 |
+
f'<div style="margin-bottom:9px;">{_selo_icone(icone, estilo["icon"], tamanho=26, raio=8, fonte=13)}</div>'
|
| 251 |
f'<div class="af-kpi-label" style="font-size:11px;color:#64748b;text-transform:uppercase;'
|
| 252 |
+
f'letter-spacing:.05em;font-weight:700;">{_escape(rotulo)}</div>'
|
| 253 |
+
f'<div style="font-size:19px;font-weight:800;color:{estilo["text"]};'
|
| 254 |
+
f'margin-top:3px;letter-spacing:-.3px;">{_escape(valor)}</div></td>'
|
| 255 |
)
|
| 256 |
|
| 257 |
|
|
|
|
| 260 |
|
| 261 |
Não inclui mais "Tarefas vencidas" (removido a pedido — volume alto
|
| 262 |
demais para um KPI de topo); em seu lugar entram "Oportunidades criadas".
|
| 263 |
+
As categorias de cor espelham exatamente as usadas no painel React.
|
| 264 |
"""
|
| 265 |
leads = metrics.get("leads", {}) or {}
|
| 266 |
opp = metrics.get("opportunities", {}) or {}
|
|
|
|
| 269 |
contratos = metrics.get("contracts", {}) or {}
|
| 270 |
|
| 271 |
celulas: list[str] = [
|
| 272 |
+
_linha_kpi("🌱", "Leads novos", _num(leads.get("new_leads")), "green"),
|
| 273 |
+
_linha_kpi("🎯", "Conversão de leads", _pct(leads.get("conversion_rate")), "blue"),
|
| 274 |
_linha_kpi(
|
| 275 |
+
"💰",
|
| 276 |
"Pipeline aberto",
|
| 277 |
_moeda(opp.get("open_pipeline_product_value") or opp.get("open_pipeline_amount")),
|
| 278 |
+
"violet",
|
| 279 |
),
|
| 280 |
+
_linha_kpi("✨", "Oportunidades criadas", _num(opp.get("new_opportunities")), "blue"),
|
| 281 |
+
_linha_kpi("🏆", "Oportunidades ganhas", _num(opp.get("won_opportunities")), "blue"),
|
| 282 |
+
_linha_kpi("📉", "Oportunidades perdidas", _num(opp.get("lost_opportunities")), "red"),
|
| 283 |
+
_linha_kpi("🚧", "Oportunidades paradas", _num(opp.get("stalled_opportunities")), "amber"),
|
| 284 |
]
|
| 285 |
if sat.get("configured") and sat.get("responses"):
|
| 286 |
+
celulas.append(_linha_kpi("😊", "Satisfação (nota média)", _num(sat.get("avg_score")), "teal"))
|
| 287 |
if canc.get("configured") and canc.get("cancellations_count"):
|
| 288 |
celulas.append(
|
| 289 |
+
_linha_kpi("❌", "Cancelamentos", _num(canc.get("cancellations_count")), "red")
|
| 290 |
)
|
| 291 |
if contratos.get("configured"):
|
| 292 |
+
celulas.append(
|
| 293 |
+
_linha_kpi("📄", "Contratos modificados hoje", _num(contratos.get("modified_today_count")), "violet")
|
| 294 |
+
)
|
| 295 |
|
| 296 |
# Distribui as células em linhas de 3 colunas.
|
| 297 |
linhas_html: list[str] = []
|
| 298 |
for i in range(0, len(celulas), 3):
|
| 299 |
grupo = celulas[i : i + 3]
|
| 300 |
linhas_html.append(
|
| 301 |
+
"<tr>" + "".join(grupo)
|
| 302 |
+
+ ('<td style="border-radius:14px;"></td>' * (3 - len(grupo)))
|
| 303 |
+
+ "</tr>"
|
| 304 |
+
+ '<tr><td colspan="3" style="height:10px;"></td></tr>'
|
| 305 |
)
|
| 306 |
return (
|
| 307 |
'<table role="presentation" width="100%" cellspacing="0" cellpadding="0" '
|
|
|
|
| 325 |
modificados = m.get("modified_today") or []
|
| 326 |
if modificados:
|
| 327 |
itens = "".join(
|
| 328 |
+
f"<li style='margin-bottom:4px'>"
|
| 329 |
f"<strong>{_escape(c.get('name') or c.get('id') or '—')}</strong>"
|
| 330 |
f" <span style='color:#64748b'>— modificado por "
|
| 331 |
f"{_escape(c.get('modified_by') or 'responsável não identificado')}</span></li>"
|
| 332 |
for c in modificados[:15]
|
| 333 |
)
|
| 334 |
+
conteudo = (
|
| 335 |
+
f"<div style='font-size:13px;font-weight:700;color:#0f172a;margin-bottom:8px;'>"
|
|
|
|
| 336 |
f"📄 Contratos modificados hoje "
|
| 337 |
f"<span style='color:#94a3b8;font-weight:400'>({m.get('modified_today_count', 0)})</span></div>"
|
| 338 |
+
f"<ul style='margin:0;padding-left:18px;font-size:13px;color:#334155;line-height:1.6'>{itens}</ul>"
|
|
|
|
| 339 |
)
|
| 340 |
+
partes.append(_caixa(conteudo, _HOVER))
|
| 341 |
|
| 342 |
if m.get("readjustment_configured"):
|
| 343 |
qtd = m.get("readjustment_month_count") or 0
|
| 344 |
if qtd:
|
| 345 |
inconsist = int(m.get("readjustment_inconsistent_count") or 0)
|
| 346 |
reaj_itens = "".join(
|
| 347 |
+
f"<li style='margin-bottom:4px'>"
|
| 348 |
f"<strong>{_escape(c.get('name') or c.get('id') or '—')}</strong>: "
|
| 349 |
f"{_moeda(c.get('previous_value'))} → {_moeda(c.get('current_value'))} "
|
| 350 |
f"(<span style='color:{_SUCESSO_TXT if (c.get('delta') or 0) >= 0 else _ERRO_TXT}'>"
|
|
|
|
| 358 |
+ ")</li>"
|
| 359 |
for c in (m.get("readjustment_contracts") or [])[:10]
|
| 360 |
)
|
| 361 |
+
conteudo = (
|
| 362 |
+
f"<div style='font-size:13px;font-weight:700;color:#0f172a;margin-bottom:8px;'>"
|
|
|
|
| 363 |
f"💹 Reajuste de contratos no mês "
|
| 364 |
f"<span style='color:#94a3b8;font-weight:400'>({qtd} contrato(s) · "
|
| 365 |
f"{_moeda(m.get('readjustment_month_total'))})</span></div>"
|
| 366 |
+
f"<ul style='margin:0 0 8px;padding-left:18px;font-size:13px;color:#334155;line-height:1.6'>{reaj_itens}</ul>"
|
| 367 |
+ (
|
| 368 |
f"<div style='font-size:12px;color:{_ALERTA_TXT};margin-bottom:6px;'>"
|
| 369 |
f"⚠ {inconsist} contrato(s) com possível inconsistência entre o reajuste "
|
|
|
|
| 372 |
else ""
|
| 373 |
)
|
| 374 |
+ f"<div style='font-size:11px;color:#94a3b8;'>⚠ {_escape(m.get('readjustment_disclaimer', ''))}</div>"
|
|
|
|
| 375 |
)
|
| 376 |
+
partes.append(_caixa(conteudo, _HOVER))
|
| 377 |
else:
|
| 378 |
partes.append(
|
| 379 |
f"<div style='font-size:12px;color:#94a3b8;margin-bottom:10px;'>"
|
|
|
|
| 382 |
|
| 383 |
if not partes:
|
| 384 |
return ""
|
| 385 |
+
return _secao_titulo("📄", "Contratos", _PENSO_BLUE) + "".join(partes)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 386 |
|
| 387 |
|
| 388 |
def _bloco_alertas(alerts: list[dict[str, Any]]) -> str:
|
|
|
|
| 390 |
altos = [a for a in alerts if a.get("severity") == "high"]
|
| 391 |
selecionados = (altos or alerts)[:6]
|
| 392 |
if not selecionados:
|
| 393 |
+
return _caixa(
|
| 394 |
+
"<p style='color:#15803d;font-size:14px;margin:0;font-weight:600;'>"
|
| 395 |
+
"✅ Nenhum alerta crítico para o período.</p>",
|
| 396 |
+
"#ecfdf5",
|
| 397 |
)
|
| 398 |
|
| 399 |
cartoes: list[str] = []
|
|
|
|
| 402 |
cor = _COR_SEVERIDADE.get(sev, "#94a3b8")
|
| 403 |
cor_texto_selo = _TEXTO_SEVERIDADE.get(sev, "#ffffff")
|
| 404 |
rotulo = _ROTULO_SEVERIDADE.get(sev, "INFO")
|
| 405 |
+
fundo = _FUNDO_SEVERIDADE.get(sev, "#f8fafc")
|
| 406 |
acao = a.get("recommended_action")
|
| 407 |
+
selo = (
|
| 408 |
+
'<table role="presentation" cellspacing="0" cellpadding="0" style="display:inline-table;'
|
| 409 |
+
'vertical-align:middle;margin-right:7px;"><tr>'
|
| 410 |
+
f'<td bgcolor="{cor}" style="background-color:{cor};color:{cor_texto_selo};'
|
| 411 |
+
f'font-size:10px;font-weight:800;padding:3px 9px;border-radius:999px;'
|
| 412 |
+
f'letter-spacing:.05em;">{rotulo}</td></tr></table>'
|
| 413 |
+
)
|
| 414 |
acao_html = (
|
| 415 |
+
f'<div style="margin-top:8px;font-size:13px;color:#0f172a;">'
|
| 416 |
f'<strong>Ação:</strong> {_escape(acao)}</div>'
|
| 417 |
if acao
|
| 418 |
else ""
|
| 419 |
)
|
| 420 |
+
conteudo = (
|
| 421 |
+
f'{selo}'
|
| 422 |
+
f'<span style="font-weight:800;color:#0f172a;font-size:14px;letter-spacing:-.1px;">'
|
|
|
|
|
|
|
|
|
|
|
|
|
| 423 |
f'{_escape(a.get("title", ""))}</span>'
|
| 424 |
+
f'<div style="margin-top:6px;font-size:13px;color:#475569;line-height:1.55;">'
|
| 425 |
f'{_escape(a.get("description", ""))}</div>'
|
| 426 |
+
f'{acao_html}'
|
| 427 |
+
)
|
| 428 |
+
cartoes.append(
|
| 429 |
+
_caixa(
|
| 430 |
+
conteudo,
|
| 431 |
+
fundo,
|
| 432 |
+
radius=f"0 {_RADIUS_CHIP} {_RADIUS_CHIP} 0",
|
| 433 |
+
padding="13px 16px",
|
| 434 |
+
classe="af-altcard",
|
| 435 |
+
borda_esquerda=cor,
|
| 436 |
+
)
|
| 437 |
)
|
| 438 |
return "".join(cartoes)
|
| 439 |
|
|
|
|
| 444 |
if not acoes:
|
| 445 |
return ""
|
| 446 |
itens = "".join(
|
| 447 |
+
f'<li style="margin-bottom:8px;font-size:13.5px;color:#0f172a;line-height:1.55;">{_escape(a)}</li>'
|
| 448 |
for a in acoes[:5]
|
| 449 |
)
|
| 450 |
+
return _secao_titulo("🎯", "Prioridades para hoje", "#7c3aed") + _caixa(
|
| 451 |
+
f'<ol style="margin:0;padding-left:20px;">{itens}</ol>', _HOVER, padding="15px 17px 15px 24px"
|
|
|
|
|
|
|
| 452 |
)
|
| 453 |
|
| 454 |
|
|
|
|
| 458 |
Usa classes com ``!important`` para sobrepor os estilos inline em
|
| 459 |
clientes que suportam ``<style>``/media queries (Apple Mail, Gmail
|
| 460 |
app/web, Outlook.com/iOS). Clientes que ignoram ``<style>`` (ex.: Outlook
|
| 461 |
+
desktop clássico) continuam vendo o layout claro fixo via ``bgcolor`` +
|
| 462 |
+
estilo inline — nunca quebram, apenas não ganham o tema escuro/o
|
| 463 |
+
empilhamento em telas pequenas.
|
| 464 |
"""
|
| 465 |
return f"""\
|
| 466 |
<meta name="color-scheme" content="light dark">
|
|
|
|
| 481 |
.af-card {{ background:#141a2b !important; border-color:#242c42 !important; }}
|
| 482 |
.af-body h3 {{ color:#e8ecf5 !important; }}
|
| 483 |
.af-body p, .af-body div, .af-body li, .af-body span {{ color:#c3ccdf !important; }}
|
| 484 |
+
.af-kpi-cell {{ background:#1a2138 !important; background-color:#1a2138 !important; }}
|
| 485 |
.af-kpi-label {{ color:#8fa0c2 !important; }}
|
| 486 |
+
.af-altcard {{ background:#171e33 !important; background-color:#171e33 !important; }}
|
| 487 |
+
.af-hero {{ background:#171e33 !important; background-color:#171e33 !important; }}
|
| 488 |
.af-footer {{ background:#0e1424 !important; border-color:#242c42 !important; }}
|
| 489 |
.af-muted {{ color:#7d8aa8 !important; }}
|
| 490 |
a.af-link {{ color:#9fb6ff !important; }}
|
|
|
|
| 501 |
) -> str:
|
| 502 |
"""Monta o e-mail executivo completo (documento HTML, pt-BR).
|
| 503 |
|
| 504 |
+
Visual "SaaS moderno claro": fundo cinza-claro fora do cartão, cartão
|
| 505 |
+
branco com cantos bem arredondados e sombra suave, cabeçalho em
|
| 506 |
+
gradiente azul Penso, KPIs tintados por categoria e selos de seção com
|
| 507 |
+
ícone — a mesma linguagem visual do painel React. Todo bloco com fundo
|
| 508 |
+
sólido usa tabela+``bgcolor`` (nunca ``<div>`` puro), para renderizar de
|
| 509 |
+
forma consistente em Gmail, Outlook, Apple Mail e Yahoo — inclusive
|
| 510 |
+
versões que ignoram fundo de ``<div>``. Responsivo (empilha em telas
|
| 511 |
+
pequenas) e com suporte a tema escuro (``prefers-color-scheme``),
|
| 512 |
+
preservando o layout claro como fallback via ``bgcolor``/inline.
|
| 513 |
"""
|
| 514 |
altos = sum(1 for a in alerts if a.get("severity") == "high")
|
| 515 |
resumo = (
|
| 516 |
f"Foram gerados <strong>{len(alerts)} alerta(s)</strong>, "
|
| 517 |
f"sendo <strong>{altos} de severidade alta</strong>."
|
| 518 |
)
|
| 519 |
+
# Bloco de análise (insights/storytelling) — converte o Markdown do
|
| 520 |
+
# resumo gerado pela IA (ou template) em HTML. É o coração narrativo do
|
| 521 |
+
# e-mail: por isso ganha um cartão maior, com selo de destaque e mais
|
| 522 |
+
# respiro, em vez de só um parágrafo solto.
|
| 523 |
bloco_ia = ""
|
| 524 |
corpo_ia = _md_para_html(resumo_ia) if resumo_ia.strip() else ""
|
| 525 |
if corpo_ia:
|
| 526 |
+
selo_ia = (
|
| 527 |
+
'<table role="presentation" cellspacing="0" cellpadding="0" style="margin-bottom:12px;"><tr>'
|
| 528 |
+
f'<td bgcolor="{_PENSO_BLUE}" style="background-color:{_PENSO_BLUE};color:#ffffff;'
|
| 529 |
+
'font-size:11px;font-weight:800;text-transform:uppercase;letter-spacing:.08em;'
|
| 530 |
+
'padding:5px 12px;border-radius:999px;">🧠 Análise do dia</td></tr></table>'
|
| 531 |
+
)
|
| 532 |
bloco_ia = (
|
| 533 |
+
'<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="margin:0 0 20px;"><tr>'
|
| 534 |
+
f'<td class="af-hero" bgcolor="#F5F6FE" style="background-color:#F5F6FE;'
|
| 535 |
+
f'background-image:linear-gradient(135deg,rgba(0,24,255,.07),rgba(124,58,237,.05) 55%,rgba(255,196,83,.06));'
|
| 536 |
+
f'border:1px solid rgba(0,24,255,.10);border-radius:16px;padding:18px 20px;">'
|
| 537 |
+
f"{selo_ia}{corpo_ia}"
|
| 538 |
+
"</td></tr></table>"
|
| 539 |
)
|
| 540 |
# Cabeçalho: identidade visual da Penso (marca-texto) + Analytical-Force.
|
| 541 |
# TODO: quando o arquivo do logo real da Penso estiver disponível, trocar
|
| 542 |
# este bloco por um <img> com o PNG/SVG oficial (base64 ou hospedado).
|
| 543 |
# Por ora, usa apenas cores da marca (Penso Blue/Black) — sem inventar formas.
|
| 544 |
cabecalho = f"""\
|
| 545 |
+
<td class="af-px af-animate" bgcolor="{_PENSO_BLUE}" style="background-color:{_PENSO_BLUE};
|
| 546 |
+
background-image:linear-gradient(120deg,{_BLACK_INTENSE},{_BLUE_TECH} 55%,{_PENSO_BLUE});padding:28px 30px;">
|
| 547 |
<table role="presentation" cellspacing="0" cellpadding="0"><tr>
|
| 548 |
+
<td bgcolor="#ffffff" style="width:36px;height:36px;background-color:#ffffff;border-radius:11px;text-align:center;
|
| 549 |
+
vertical-align:middle;font-weight:900;font-size:17px;color:{_PENSO_BLUE};">P</td>
|
| 550 |
+
<td style="padding-left:11px;color:#ffffff;font-size:13px;font-weight:700;letter-spacing:.04em;">PENSO</td>
|
| 551 |
+
</tr></table>
|
| 552 |
+
<div style="color:#ffffff;font-size:23px;font-weight:800;letter-spacing:-.3px;margin-top:14px;">📊 Analytical-Force</div>
|
| 553 |
+
<div style="color:#dbe4ff;font-size:13px;margin-top:5px;">Relatório diário de performance comercial</div>
|
| 554 |
+
<table role="presentation" cellspacing="0" cellpadding="0" style="margin-top:13px;"><tr>
|
| 555 |
+
<td bgcolor="rgba(255,255,255,.16)" style="background-color:rgba(255,255,255,.18);color:#ffffff;
|
| 556 |
+
font-size:12px;font-weight:600;padding:6px 13px;border-radius:999px;">📅 {_escape(report_date)}</td>
|
| 557 |
</tr></table>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 558 |
</td>"""
|
| 559 |
rodape = f"""\
|
| 560 |
+
<td class="af-footer af-px" bgcolor="{_HOVER}" style="background-color:{_HOVER};padding:18px 30px;border-top:1px solid {_DIVISORIA};">
|
| 561 |
<table role="presentation" cellspacing="0" cellpadding="0"><tr>
|
| 562 |
+
<td style="font-size:11px;color:#94a3b8;text-transform:uppercase;letter-spacing:.06em;padding-right:8px;">Fonte de dados</td>
|
| 563 |
<td><img src="{_LOGO_SALESFORCE_URL}" alt="Salesforce" style="height:14px;vertical-align:middle;" /></td>
|
| 564 |
</tr></table>
|
| 565 |
+
<div class="af-muted" style="font-size:12px;color:#94a3b8;margin-top:11px;line-height:1.5;">
|
| 566 |
Relatório gerado automaticamente pelo agente <strong>Analytical-Force</strong> (Penso).
|
| 567 |
Os números são calculados em Python a partir do Salesforce; a interpretação
|
| 568 |
é gerada por modelo local. O relatório completo está anexado/registrado no sistema.
|
|
|
|
| 576 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 577 |
{_estilo_cabecalho()}
|
| 578 |
</head>
|
| 579 |
+
<body class="af-wrap" bgcolor="#F6F7FB" style="margin:0;padding:0;background-color:#F6F7FB;font-family:-apple-system,'Segoe UI',Helvetica,Arial,sans-serif;">
|
| 580 |
+
<div class="af-wrap" style="background-color:#F6F7FB;padding:28px 0;font-family:-apple-system,'Segoe UI',Helvetica,Arial,sans-serif;">
|
| 581 |
<table role="presentation" width="640" align="center" cellspacing="0" cellpadding="0" class="af-container"
|
| 582 |
style="max-width:640px;margin:0 auto;">
|
| 583 |
+
<tr><td class="af-card" bgcolor="#ffffff" style="background-color:#ffffff;border-radius:{_RADIUS_CARD};overflow:hidden;
|
| 584 |
+
border:1px solid {_DIVISORIA};box-shadow:0 20px 50px -20px rgba(6,6,20,.18);">
|
| 585 |
<table role="presentation" width="100%" cellspacing="0" cellpadding="0">
|
| 586 |
<tr>
|
| 587 |
{cabecalho}
|
| 588 |
</tr>
|
| 589 |
<tr>
|
| 590 |
+
<td class="af-body af-px" style="padding:26px 30px;">
|
| 591 |
+
{_secao_titulo("📋", "Resumo executivo", _PENSO_BLUE)}
|
| 592 |
+
<p style="font-size:14px;color:#475569;margin:0 0 14px;line-height:1.6;">{resumo}</p>
|
| 593 |
{bloco_ia}
|
| 594 |
|
| 595 |
+
{_secao_titulo("📊", "Números-chave", "#0d9488")}
|
| 596 |
{_grade_kpis(metrics)}
|
| 597 |
|
| 598 |
{_bloco_contratos(metrics)}
|
| 599 |
|
| 600 |
+
{_secao_titulo("🚨", "Principais alertas", _ERRO)}
|
| 601 |
{_bloco_alertas(alerts)}
|
| 602 |
|
| 603 |
{_bloco_prioridades(alerts)}
|
|
|
|
| 703 |
info = f" <span style='color:#64748b'>({_escape(r['info'])})</span>" if r.get("info") else ""
|
| 704 |
url = r.get("url")
|
| 705 |
alvo = (
|
| 706 |
+
f"<a class='af-link' href='{_escape(url)}' style='color:{_PENSO_BLUE};text-decoration:none'>{nome}</a>"
|
| 707 |
if url
|
| 708 |
else nome
|
| 709 |
)
|
| 710 |
+
linhas_li.append(f"<li style='margin-bottom:4px'>{alvo}{info}</li>")
|
| 711 |
+
conteudo = (
|
| 712 |
+
f"<div style='font-size:13px;font-weight:700;color:#0f172a;margin-bottom:6px'>"
|
|
|
|
| 713 |
f"{rotulo} <span style='color:#94a3b8;font-weight:400'>({len(itens)})</span></div>"
|
| 714 |
+
f"<ul style='margin:0;padding-left:18px;font-size:13px;color:#334155;line-height:1.6'>{''.join(linhas_li)}</ul>"
|
|
|
|
| 715 |
)
|
| 716 |
+
secoes.append(_caixa(conteudo, _HOVER))
|
| 717 |
if not secoes:
|
| 718 |
return ""
|
| 719 |
return (
|
| 720 |
+
_secao_titulo("🔗", "Registros do dia", "#7c3aed")
|
| 721 |
+
+ "<div style='font-size:12px;color:#94a3b8;margin-bottom:10px'>"
|
| 722 |
"Links diretos no Salesforce (tarefas vencidas omitidas pelo volume).</div>"
|
| 723 |
+ "".join(secoes)
|
| 724 |
)
|