ViniciusKhan commited on
Commit
01cf033
·
1 Parent(s): 373caa7

Corrige unidade do campo de reajuste (percent vs valor)

Browse files
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:#f5f6fb;--surface:#ffffff;--surface2:#F7F8FA;--line:#E6EAEE;
5
- --txt:#171717;--muted:#6b7280;--faint:#9aa3b2;
6
  --ok:#16C79A;--high:#F05454;--med:#FFC453;--low:#eab308;
7
- --blue:#0018FF;--green:#16C79A;--red:#F05454;--amber:#FFC453;--violet:#0004DD;--teal:#16C79A;
8
  --penso-blue:#0018FF;--penso-bluetech:#0004DD;--penso-black:#171717;--penso-blackx:#060606;
9
- --radius:18px;--shadow:0 10px 28px rgba(6,6,6,.08);--shadow-lg:0 26px 60px rgba(6,6,6,.22);--sb:248px;
 
 
 
 
 
10
  }
11
- [data-theme="dark"]{--bg:#0b0b0f;--surface:#141520;--surface2:#191b28;--line:#272c47;--txt:#e8eaf5;--muted:#9aa3c0;--faint:#6c7595;
12
- --shadow:0 10px 28px rgba(0,0,0,.45);--shadow-lg:0 26px 60px rgba(0,0,0,.6)}
 
 
13
  *{box-sizing:border-box}
14
- body{margin:0;font-family:'Segoe UI',system-ui,-apple-system,Arial,sans-serif;color:var(--txt);
15
- background:radial-gradient(1200px 500px at 100% -10%,rgba(0,24,255,.08),transparent),
16
- radial-gradient(900px 400px at -10% 0%,rgba(255,196,83,.07),transparent),var(--bg);
17
- min-height:100vh}
 
 
 
 
18
  a{color:var(--c1)}
19
  .app{display:flex;min-height:100vh}
20
- .sidebar{width:var(--sb);background:linear-gradient(170deg,#060606 0%,#0004DD 55%,#0018FF 120%);color:#fff;position:fixed;inset:0 auto 0 0;
21
- display:flex;flex-direction:column;padding:16px 12px;z-index:40;transition:transform .25s}
22
- .sb-brand{display:flex;align-items:center;gap:11px;padding:6px 10px 16px;margin-bottom:6px;border-bottom:1px solid rgba(255,255,255,.18)}
23
- .sb-brand .logo{width:40px;height:40px;border-radius:13px;background:rgba(255,255,255,.22);display:grid;place-items:center;font-size:20px}
24
- .sb-brand .penso-logo{background:#fff;color:#0018FF;font-weight:900;font-size:20px;font-family:Arial,sans-serif}
25
- .sb-brand h1{font-size:15px;margin:0;font-weight:800;letter-spacing:.2px} .sb-brand .s{font-size:11px;color:#CEE2FF}
26
- nav{display:flex;flex-direction:column;gap:3px;overflow:auto;margin-top:6px}
27
- nav a{display:flex;align-items:center;gap:12px;color:#EAF0FF;text-decoration:none;padding:10px 12px;border-radius:12px;
28
- font-size:13.5px;font-weight:600;cursor:pointer;transition:.18s}
29
- nav a .i{width:22px;text-align:center;font-size:16px}
30
- nav a:hover{background:rgba(255,255,255,.14);color:#fff;transform:translateX(2px)}
31
- nav a.active{background:#fff;color:#0018FF;box-shadow:0 6px 16px rgba(0,0,0,.18)}
32
- .sb-foot{margin-top:auto;padding-top:14px;display:flex;align-items:center;gap:10px;border-top:1px solid rgba(255,255,255,.18)}
33
- .sb-foot img{width:38px;height:38px;border-radius:50%;border:2px solid rgba(255,255,255,.65)}
34
- .sb-foot .n{font-size:12.5px;font-weight:700} .sb-foot .r{font-size:11px;color:#CEE2FF}
 
 
 
 
 
 
 
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,.82);backdrop-filter:blur(10px);
37
- border-bottom:1px solid var(--line);padding:12px 22px;display:flex;align-items:center;gap:12px;flex-wrap:wrap}
38
- [data-theme="dark"] .topbar{background:rgba(23,26,46,.82)}
39
- .hamb{display:none;background:var(--surface);border:1px solid var(--line);color:var(--txt);border-radius:11px;width:38px;height:38px;font-size:18px;cursor:pointer}
40
- .vtitle{font-size:20px;font-weight:800;margin:0;letter-spacing:-.3px;
41
- background:linear-gradient(90deg,var(--c1),var(--c2));-webkit-background-clip:text;background-clip:text;color:transparent}
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 12px;border-radius:999px;font-weight:600}
44
- .dot{width:9px;height:9px;border-radius:50%;background:#9aa3b2}
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:11px;
48
- padding:9px 30px 9px 12px;font-size:13px;font-weight:600;cursor:pointer;
49
- background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='%236b7280'%3E%3Cpath d='M6 9 1 4h10z'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 10px center}
50
  .sel:focus{outline:none;border-color:var(--c1)}
51
- .iconbtn{width:38px;height:38px;border-radius:11px;border:1px solid var(--line);background:var(--surface);color:var(--txt);cursor:pointer;font-size:16px}
52
- .iconbtn:hover{border-color:var(--c1)}
53
- .btn{border:0;border-radius:12px;padding:10px 18px;font-size:14px;font-weight:700;cursor:pointer;transition:.15s;display:inline-flex;align-items:center;gap:8px}
54
- .btn:hover{filter:brightness(1.05)} .btn:active{transform:translateY(1px)} .btn:disabled{opacity:.55;cursor:not-allowed}
55
- .btn-primary{background:linear-gradient(100deg,var(--c1),var(--c2));color:#fff;box-shadow:0 8px 18px rgba(124,58,237,.32)}
56
  .btn-ghost{background:var(--surface);color:var(--c1);border:1px solid var(--line)}
57
- .btn-sm{padding:8px 13px;font-size:12.5px;border-radius:10px}
58
- main{padding:22px;flex:1}
59
- .card{background:var(--surface);border:1px solid var(--line);border-radius:var(--radius);box-shadow:var(--shadow);padding:20px;margin-bottom:18px;animation:fadeUp .45s ease both}
60
- @keyframes fadeUp{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:none}}
61
- .sec-h{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:16px;flex-wrap:wrap}
62
- .sh-left{display:flex;align-items:center;gap:11px}
63
- .sh-ic{width:38px;height:38px;border-radius:11px;display:grid;place-items:center;font-size:19px;color:#fff;flex:none}
64
- .sec-h h2{font-size:15px;margin:0;color:var(--txt);font-weight:800;letter-spacing:-.2px}
 
 
 
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 12px;font-size:12.5px;font-weight:600}
67
  .chip.good{background:#dcfce7;color:#15803d;border-color:#bbf7d0} .chip.bad{background:#ffe4e6;color:#be123c;border-color:#fecdd3}
68
- .hero{border-radius:14px;padding:16px 18px;background:linear-gradient(120deg,rgba(0,24,255,.08),rgba(255,196,83,.08));border:1px solid rgba(0,4,221,.18);border-left:4px solid var(--c2);margin-bottom:16px}
69
- .hero .h{font-size:12px;font-weight:800;color:var(--c2);text-transform:uppercase;letter-spacing:.05em;margin-bottom:6px}
70
- .hero p{margin:0 0 10px;font-size:14px;line-height:1.6} .hero p:last-child{margin-bottom:0}
71
- .hero strong{color:var(--txt)}
72
- .skeleton{border-radius:10px;background:linear-gradient(100deg,var(--surface2) 30%,var(--line) 50%,var(--surface2) 70%);
 
 
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 .3s ease both}
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
- .kpis{display:grid;grid-template-columns:repeat(auto-fit,minmax(170px,1fr));gap:14px}
83
- .kpi{border:1px solid var(--line);border-radius:16px;padding:16px;background:var(--surface);transition:.18s;position:relative;overflow:hidden}
84
- .kpi:hover{transform:translateY(-3px);box-shadow:var(--shadow)}
85
- .kpi .ic{width:44px;height:44px;border-radius:13px;display:grid;place-items:center;font-size:22px;margin-bottom:10px}
86
- .kpi .lab{font-size:12px;color:var(--muted);font-weight:600}
87
- .kpi .val{font-size:25px;font-weight:800;margin-top:2px;letter-spacing:-.6px;color:var(--txt)}
88
- .kpi.blue .ic{background:#e0ecff;color:#2563eb} .kpi.blue .val{color:#2563eb}
89
- .kpi.green .ic{background:#dcfce7;color:#16a34a} .kpi.green .val{color:#16a34a}
90
- .kpi.red .ic{background:#ffe4e6;color:#e11d48} .kpi.red .val{color:#e11d48}
91
- .kpi.amber .ic{background:#fef3c7;color:#d97706} .kpi.amber .val{color:#d97706}
92
- .kpi.violet .ic{background:#ede9fe;color:#7c3aed} .kpi.violet .val{color:#7c3aed}
93
- .kpi.teal .ic{background:#ccfbf1;color:#0d9488} .kpi.teal .val{color:#0d9488}
 
 
 
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 13px;border-radius:999px;border:1px solid var(--line);background:var(--surface2);color:var(--muted);cursor:pointer;font-weight:700}
97
- .fchip.active{background:linear-gradient(100deg,var(--c1),var(--c2));color:#fff;border-color:transparent}
98
- .alert{border:1px solid var(--line);border-left:5px solid #9aa3b2;background:var(--surface);padding:13px 15px;margin-bottom:11px;border-radius:0 14px 14px 0;animation:fadeUp .35s ease both}
99
- .alert .badge{display:inline-block;color:#fff;font-size:10px;font-weight:800;padding:3px 10px;border-radius:999px;margin-right:7px}
100
- .alert .t{font-weight:800;font-size:14.5px} .alert .d{color:var(--muted);font-size:13px;margin-top:5px} .alert .a{font-size:13px;margin-top:7px}
 
 
 
 
 
 
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:10px;padding:10px 12px;font-size:12.5px;margin-top:9px}
104
- .dgrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(250px,1fr));gap:14px}
105
- .dcard{border:1px solid var(--line);border-radius:15px;background:var(--surface);overflow:hidden;box-shadow:var(--shadow)}
106
- .dcard .dh{display:flex;justify-content:space-between;align-items:center;padding:12px 14px;font-size:13px;font-weight:800;color:#fff}
 
 
 
 
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:10px 14px 12px 28px;max-height:280px;overflow:auto} .dcard li{font-size:12.5px;margin-bottom:5px}
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 14px;border-radius:999px;background:var(--surface2);border:1px solid var(--line);color:var(--muted);font-size:12.5px;font-weight:700;cursor:pointer}
112
- .tab.active{background:linear-gradient(100deg,var(--c1),var(--c2));color:#fff;border-color:transparent}
113
- .panel{display:none;border:1px solid var(--line);border-radius:14px;padding:18px 22px;background:var(--surface);max-height:600px;overflow:auto}
 
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:9px 10px;font-size:13px;text-align:left}
117
- .owners th{color:var(--muted);font-size:11px;text-transform:uppercase;letter-spacing:.04em}
118
- .owners tbody tr:hover{background:var(--surface2)}
119
- .empty{text-align:center;color:var(--faint);padding:40px 20px;font-size:14px}
120
- .err{background:#ffe4e6;border:1px solid #fecdd3;color:#be123c;padding:13px 15px;border-radius:13px;font-size:14px;white-space:pre-wrap}
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 13px;border:1px solid var(--line);border-radius:12px;font-size:14px;background:var(--surface2);color:var(--txt)}
123
- input:focus{outline:none;border-color:var(--c1);box-shadow:0 0 0 3px rgba(99,102,241,.16)}
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),var(--c2))} .switch input:checked + .track::after{transform:translateX(18px)}
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:16px;flex-wrap:wrap;justify-content:center;padding:18px;border-top:1px solid var(--line);margin-top:8px}
133
  .integr span{font-size:11px;text-transform:uppercase;letter-spacing:.08em;color:var(--faint);font-weight:700}
134
- .integr img{height:20px;opacity:.6;transition:.2s} .integr img:hover{opacity:1;transform:translateY(-2px)}
135
- .backdrop{position:fixed;inset:0;background:rgba(20,16,40,.5);backdrop-filter:blur(4px);display:none;align-items:flex-start;justify-content:center;padding:60px 16px;z-index:60}
136
- .backdrop.open{display:flex} .modal{background:var(--surface);border:1px solid var(--line);border-radius:18px;max-width:520px;width:100%;padding:24px;box-shadow:var(--shadow-lg)}
137
- .modal .mh{display:flex;justify-content:space-between;align-items:center;margin-bottom:16px}.modal h3{margin:0}
138
- .modal .x{background:var(--surface2);border:1px solid var(--line);border-radius:10px;cursor:pointer;font-size:15px;padding:5px 11px;color:var(--txt)}
139
- #toasts{position:fixed;right:18px;bottom:18px;display:flex;flex-direction:column;gap:9px;z-index:70}
140
- .toast{background:var(--txt);color:var(--surface);padding:12px 16px;border-radius:12px;font-size:13px;box-shadow:var(--shadow-lg);animation:fadeUp .3s ease}
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:10px;padding:9px 12px;font-size:13px}
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: mais dinâmico/animado (reajuste de contrato, KPIs) ---- */
149
- .disclaimer{margin-top:12px;padding:10px 13px;border-radius:10px;background:var(--surface2);border:1px dashed var(--line);color:var(--muted);font-size:12px;line-height:1.5}
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),var(--c2));background-size:220% 220%;
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:.55;pointer-events:none}
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 ease both}
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 = "#6366f1";
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 = "#E6EAEE"
41
- _HOVER = "#F7F8FA"
 
 
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:4px'>{b}</li>" for b in bullets
118
  )
119
  html.append(
120
- f"<ul style='margin:4px 0 10px;padding-left:18px;font-size:14px;"
121
- f"line-height:1.55;color:#334155'>{itens}</ul>"
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:700;color:#0f172a;margin:10px 0 4px;"
137
- f"font-size:14px'>{_md_inline(sem_hash)}</div>"
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 10px;font-size:14px;line-height:1.6;"
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 _linha_kpi(rotulo: str, valor: str, destaque: str = _PENSO_BLUE) -> str:
155
- """Célula de KPI (rótulo + valor) para a grade de números-chave.
 
 
 
 
 
 
 
 
 
 
 
 
 
156
 
157
- Recebe a classe ``af-kpi-cell`` (dark mode + empilhamento em telas
158
- pequenas via CSS em ``<head>``) além do estilo inline (fallback para
159
- clientes que ignoram ``<style>``, ex.: Outlook desktop).
 
 
 
 
 
160
  """
 
161
  return (
162
- '<td class="af-kpi-cell" style="padding:10px 14px;border:1px solid #e6e8eb;'
163
- 'border-radius:8px;background:#f8fafc;vertical-align:top;">'
 
164
  f'<div class="af-kpi-label" style="font-size:11px;color:#64748b;text-transform:uppercase;'
165
- f'letter-spacing:.04em;">{_escape(rotulo)}</div>'
166
- f'<div style="font-size:20px;font-weight:700;color:{destaque};'
167
- f'margin-top:2px;">{_escape(valor)}</div></td>'
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")), _SUCESSO_TXT),
192
- _linha_kpi("Oportunidades perdidas", _num(opp.get("lost_opportunities")), _ERRO_TXT),
193
- _linha_kpi("Oportunidades paradas", _num(opp.get("stalled_opportunities")), _ALERTA_TXT),
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")), _ERRO_TXT)
200
  )
201
  if contratos.get("configured"):
202
- celulas.append(_linha_kpi("Contratos modificados hoje", _num(contratos.get("modified_today_count"))))
 
 
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
- '<tr>' + "".join(grupo)
210
- + ('<td></td>' * (3 - len(grupo)))
211
- + '</tr>'
212
- + '<tr><td colspan="3" style="height:8px;"></td></tr>'
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:3px'>"
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
- partes.append(
243
- f"<div class='af-altcard' style='background:{_HOVER};border-radius:10px;padding:12px 14px;margin-bottom:10px;'>"
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:3px'>"
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
- partes.append(
271
- f"<div class='af-altcard' style='background:{_HOVER};border-radius:10px;padding:12px 14px;margin-bottom:10px;'>"
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
- '<p style="color:#15803d;font-size:14px;">'
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:6px;font-size:13px;color:#0f172a;">'
321
  f'<strong>Ação:</strong> {_escape(acao)}</div>'
322
  if acao
323
  else ""
324
  )
325
- cartoes.append(
326
- f'<div style="border-left:4px solid {cor};background:{_HOVER};'
327
- f'padding:10px 14px;margin-bottom:10px;border-radius:0 8px 8px 0;">'
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:4px;font-size:13px;color:#475569;">'
334
  f'{_escape(a.get("description", ""))}</div>'
335
- f'{acao_html}</div>'
 
 
 
 
 
 
 
 
 
 
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:6px;font-size:13px;color:#0f172a;">{_escape(a)}</li>'
347
  for a in acoes[:5]
348
  )
349
- return (
350
- '<h3 style="font-size:15px;color:#0f172a;margin:22px 0 8px;">'
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 inline — nunca
363
- quebram, apenas não ganham o tema escuro/empilhamento.
 
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; border-color:#2a3350 !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
- Responsivo (empilha em telas pequenas) e com suporte a tema escuro dos
404
- clientes de e-mail (``prefers-color-scheme``), preservando o layout claro
405
- como fallback via estilos inline.
 
 
 
 
 
 
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 resumo em HTML.
 
 
 
413
  bloco_ia = ""
414
  corpo_ia = _md_para_html(resumo_ia) if resumo_ia.strip() else ""
415
  if corpo_ia:
 
 
 
 
 
 
416
  bloco_ia = (
417
- f"<div style='background:{_HOVER};border:1px solid {_DIVISORIA};border-left:4px solid {_PENSO_BLUE};"
418
- "border-radius:10px;padding:14px 16px;margin:0 0 18px;' class='af-altcard'>"
419
- f"<div style='font-size:12px;color:{_PENSO_BLUE};font-weight:700;text-transform:uppercase;"
420
- "letter-spacing:.04em;margin-bottom:8px;'>🧠 Análise do dia</div>"
421
- f"{corpo_ia}"
422
- "</div>"
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:linear-gradient(120deg,{_BLACK_INTENSE},{_BLUE_TECH} 55%,{_PENSO_BLUE});padding:26px 28px;">
 
430
  <table role="presentation" cellspacing="0" cellpadding="0"><tr>
431
- <td style="width:34px;height:34px;background:#ffffff;border-radius:9px;text-align:center;
432
- vertical-align:middle;font-weight:800;font-size:16px;color:{_PENSO_BLUE};">P</td>
433
- <td style="padding-left:10px;color:#ffffff;font-size:13px;font-weight:700;letter-spacing:.03em;">PENSO</td>
 
 
 
 
 
 
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:16px 28px;border-top:1px solid {_DIVISORIA};">
442
  <table role="presentation" cellspacing="0" cellpadding="0"><tr>
443
- <td style="font-size:11px;color:#94a3b8;text-transform:uppercase;letter-spacing:.05em;padding-right:8px;">Fonte de dados</td>
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:10px;">
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:#f1f5f9;">
461
- <div class="af-wrap" style="background:#f1f5f9;padding:24px 0;font-family:Arial,Helvetica,sans-serif;">
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:12px;overflow:hidden;border:1px solid {_DIVISORIA};">
 
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:24px 28px;">
471
- <h3 style="font-size:14px;color:#0f172a;margin:0 0 10px;padding-left:10px;border-left:3px solid {_PENSO_BLUE};">Resumo executivo</h3>
472
- <p style="font-size:14px;color:#475569;margin:0 0 14px;line-height:1.55;">{resumo}</p>
473
  {bloco_ia}
474
 
475
- <h3 style="font-size:14px;color:#0f172a;margin:22px 0 10px;padding-left:10px;border-left:3px solid {_PENSO_BLUE};">Números-chave</h3>
476
  {_grade_kpis(metrics)}
477
 
478
  {_bloco_contratos(metrics)}
479
 
480
- <h3 style="font-size:14px;color:#0f172a;margin:22px 0 10px;padding-left:10px;border-left:3px solid {_ERRO};">Principais alertas</h3>
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:#1f4fb2;text-decoration:none'>{nome}</a>"
587
  if url
588
  else nome
589
  )
590
- linhas_li.append(f"<li style='margin-bottom:3px'>{alvo}{info}</li>")
591
- secoes.append(
592
- f"<div style='margin-bottom:12px'>"
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
- "<h3 style='font-size:15px;color:#0f172a;margin:22px 0 8px;'>🔗 Registros do dia</h3>"
602
- "<div style='font-size:12px;color:#94a3b8;margin-bottom:8px'>"
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
  )