Commit ·
eeeab18
1
Parent(s): 79d1357
feat: Add new dashboard pages for full, receipt, actual, and expedition data, incorporating session management, a loading UI, and data processing.
Browse files- app.py +100 -12
- pages/atual.py +217 -183
- pages/exp.py +580 -495
- pages/full.py +524 -474
- pages/rec.py +484 -603
app.py
CHANGED
|
@@ -7,7 +7,6 @@ from streamlit.components.v1 import html as _html
|
|
| 7 |
from dotenv import load_dotenv
|
| 8 |
load_dotenv()
|
| 9 |
|
| 10 |
-
# ← substitui GSheetsConnection
|
| 11 |
from gs_client import read_ws, write_ws
|
| 12 |
|
| 13 |
st.set_page_config(
|
|
@@ -17,8 +16,15 @@ st.set_page_config(
|
|
| 17 |
initial_sidebar_state="collapsed"
|
| 18 |
)
|
| 19 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
# ── COOKIE SESSION ────────────────────────────────────────────────────
|
| 21 |
-
_cc
|
| 22 |
COOKIE_NAME = "pmja_session"
|
| 23 |
SESSION_EXPIRY_HOURS = 8
|
| 24 |
|
|
@@ -103,7 +109,7 @@ COLS = [
|
|
| 103 |
"last_login", "email_verified", "verification_code", "code_expiry", "image_url",
|
| 104 |
]
|
| 105 |
|
| 106 |
-
# ── DB
|
| 107 |
def get_users() -> pd.DataFrame:
|
| 108 |
df = read_ws("users_auth", spreadsheet="main")
|
| 109 |
if df.empty: return pd.DataFrame(columns=COLS)
|
|
@@ -182,11 +188,12 @@ def verify_email_code(username, code):
|
|
| 182 |
time.sleep(1)
|
| 183 |
return True, "Email verificado!"
|
| 184 |
|
| 185 |
-
# ──
|
| 186 |
for k, v in [
|
| 187 |
("logged_in", False), ("user_data", None), ("page", "login"),
|
| 188 |
("temp_username", None), ("msg", ""), ("msg_type", ""),
|
| 189 |
("_action", None), ("session_uid", None), ("session_usr", None), ("session_exp", None),
|
|
|
|
| 190 |
]:
|
| 191 |
if k not in st.session_state:
|
| 192 |
st.session_state[k] = v
|
|
@@ -214,6 +221,71 @@ elif action == "extend":
|
|
| 214 |
save_session(user["id"], user["username"], SESSION_EXPIRY_HOURS)
|
| 215 |
st.session_state.msg = "Sessão estendida por 2 horas"; st.session_state.msg_type = "success"
|
| 216 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 217 |
# ── CSS ───────────────────────────────────────────────────────────────
|
| 218 |
_CSS = """\
|
| 219 |
<style>
|
|
@@ -264,7 +336,7 @@ div[data-testid="stAlert"]{font-size:13px!important;border-radius:12px!important
|
|
| 264 |
"""
|
| 265 |
_html(_CSS, height=0)
|
| 266 |
|
| 267 |
-
# ──
|
| 268 |
pg = "dashboard" if st.session_state.logged_in else st.session_state.page
|
| 269 |
user = st.session_state.user_data
|
| 270 |
|
|
@@ -328,22 +400,39 @@ with card:
|
|
| 328 |
st.text_input("Usuário", placeholder="nome_usuario", key="li_u")
|
| 329 |
st.text_input("Senha", placeholder="••••••••", type="password", key="li_p")
|
| 330 |
sub = st.form_submit_button("Entrar →", type="primary", use_container_width=True)
|
|
|
|
| 331 |
if sub:
|
| 332 |
u = st.session_state.li_u.strip(); p = st.session_state.li_p
|
| 333 |
if not (u and p):
|
| 334 |
-
st.session_state.msg = "Preencha todos os campos"
|
|
|
|
|
|
|
| 335 |
else:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 336 |
ok, result = login_user(u, p)
|
|
|
|
| 337 |
if ok:
|
| 338 |
-
st.session_state.logged_in = True
|
|
|
|
| 339 |
save_session(result["id"], u, SESSION_EXPIRY_HOURS)
|
| 340 |
st.switch_page("pages/rec.py")
|
| 341 |
elif result == "EMAIL_NOT_VERIFIED":
|
|
|
|
| 342 |
resend_verification_code(u)
|
| 343 |
-
st.session_state.temp_username = u
|
| 344 |
-
st.session_state.
|
|
|
|
|
|
|
|
|
|
| 345 |
else:
|
| 346 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 347 |
divider()
|
| 348 |
if st.button("Criar nova conta →", use_container_width=True, key="go_reg"):
|
| 349 |
st.session_state.page = "register"; st.session_state.msg = ""; st.rerun()
|
|
@@ -447,5 +536,4 @@ with card:
|
|
| 447 |
"<div style='text-align:center;padding-top:12px;font-size:10px;color:#94a3b8;letter-spacing:.1em;'>"
|
| 448 |
"PMJA · UHE Jaguara · Rifaina SP</div>",
|
| 449 |
unsafe_allow_html=True,
|
| 450 |
-
)
|
| 451 |
-
#rec.py
|
|
|
|
| 7 |
from dotenv import load_dotenv
|
| 8 |
load_dotenv()
|
| 9 |
|
|
|
|
| 10 |
from gs_client import read_ws, write_ws
|
| 11 |
|
| 12 |
st.set_page_config(
|
|
|
|
| 16 |
initial_sidebar_state="collapsed"
|
| 17 |
)
|
| 18 |
|
| 19 |
+
# ── ESCONDE UI DO STREAMLIT ───────────────────────────────────────────
|
| 20 |
+
st.markdown("""<style>
|
| 21 |
+
#MainMenu,footer,header,.stDeployButton,[data-testid="stToolbar"],
|
| 22 |
+
[data-testid="stDecoration"],[data-testid="stStatusWidget"]{display:none!important}
|
| 23 |
+
footer{visibility:hidden!important;height:0!important}
|
| 24 |
+
</style>""", unsafe_allow_html=True)
|
| 25 |
+
|
| 26 |
# ── COOKIE SESSION ────────────────────────────────────────────────────
|
| 27 |
+
_cc = CookieController()
|
| 28 |
COOKIE_NAME = "pmja_session"
|
| 29 |
SESSION_EXPIRY_HOURS = 8
|
| 30 |
|
|
|
|
| 109 |
"last_login", "email_verified", "verification_code", "code_expiry", "image_url",
|
| 110 |
]
|
| 111 |
|
| 112 |
+
# ── DB ────────────────────────────────────────────────────────────────
|
| 113 |
def get_users() -> pd.DataFrame:
|
| 114 |
df = read_ws("users_auth", spreadsheet="main")
|
| 115 |
if df.empty: return pd.DataFrame(columns=COLS)
|
|
|
|
| 188 |
time.sleep(1)
|
| 189 |
return True, "Email verificado!"
|
| 190 |
|
| 191 |
+
# ── SESSION STATE ─────────────────────────────────────────────────────
|
| 192 |
for k, v in [
|
| 193 |
("logged_in", False), ("user_data", None), ("page", "login"),
|
| 194 |
("temp_username", None), ("msg", ""), ("msg_type", ""),
|
| 195 |
("_action", None), ("session_uid", None), ("session_usr", None), ("session_exp", None),
|
| 196 |
+
("show_loading", False),
|
| 197 |
]:
|
| 198 |
if k not in st.session_state:
|
| 199 |
st.session_state[k] = v
|
|
|
|
| 221 |
save_session(user["id"], user["username"], SESSION_EXPIRY_HOURS)
|
| 222 |
st.session_state.msg = "Sessão estendida por 2 horas"; st.session_state.msg_type = "success"
|
| 223 |
|
| 224 |
+
# ── LOADING SCREEN MD ────────────────────────────────────────────────
|
| 225 |
+
LOADING_MD = """
|
| 226 |
+
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;600;700;800&family=Sora:wght@700;800&display=swap" rel="stylesheet">
|
| 227 |
+
<style>
|
| 228 |
+
#pmja-loading{position:fixed;inset:0;z-index:999999;
|
| 229 |
+
background:linear-gradient(135deg,#003a70 0%,#0056A3 100%);
|
| 230 |
+
display:flex;flex-direction:column;align-items:center;justify-content:center;
|
| 231 |
+
font-family:'DM Sans',sans-serif;}
|
| 232 |
+
#pmja-loading .ld-logos{display:flex;align-items:center;gap:28px;margin-bottom:40px}
|
| 233 |
+
#pmja-loading .ld-logo{height:36px;filter:brightness(0) invert(1);opacity:.9}
|
| 234 |
+
#pmja-loading .ld-div{width:1px;height:36px;background:rgba(255,255,255,.25)}
|
| 235 |
+
#pmja-loading .ld-title{font-family:'Sora',sans-serif;font-size:clamp(16px,4vw,22px);
|
| 236 |
+
color:#fff;font-weight:800;letter-spacing:-.5px;margin-bottom:4px;text-align:center}
|
| 237 |
+
#pmja-loading .ld-sub{font-size:clamp(11px,3vw,13px);color:rgba(255,255,255,.7);
|
| 238 |
+
font-weight:600;margin-bottom:44px;text-align:center}
|
| 239 |
+
#pmja-loading .ld-spinner{width:34px;height:34px;border:3px solid rgba(255,255,255,.2);
|
| 240 |
+
border-top-color:#fff;border-radius:50%;animation:ldspin .8s linear infinite;margin-bottom:28px}
|
| 241 |
+
@keyframes ldspin{to{transform:rotate(360deg)}}
|
| 242 |
+
#pmja-loading .ld-bar-wrap{width:min(280px,70vw);height:4px;
|
| 243 |
+
background:rgba(255,255,255,.15);border-radius:99px;overflow:hidden;margin-bottom:16px}
|
| 244 |
+
#pmja-loading .ld-bar{height:100%;width:0%;background:#fff;border-radius:99px;transition:width .4s ease}
|
| 245 |
+
#pmja-loading .ld-status{font-size:12px;color:rgba(255,255,255,.65);font-weight:600;
|
| 246 |
+
letter-spacing:.3px;text-align:center}
|
| 247 |
+
#pmja-loading .ld-dots span{animation:ldblink 1.2s infinite;opacity:0}
|
| 248 |
+
#pmja-loading .ld-dots span:nth-child(2){animation-delay:.2s}
|
| 249 |
+
#pmja-loading .ld-dots span:nth-child(3){animation-delay:.4s}
|
| 250 |
+
@keyframes ldblink{0%,100%{opacity:0}50%{opacity:1}}
|
| 251 |
+
</style>
|
| 252 |
+
<div id="pmja-loading">
|
| 253 |
+
<div class="ld-logos">
|
| 254 |
+
<img class="ld-logo" src="https://companieslogo.com/img/orig/AZ2.F-d26946db.png?t=1720244490">
|
| 255 |
+
<div class="ld-div"></div>
|
| 256 |
+
<img class="ld-logo" src="https://upload.wikimedia.org/wikipedia/commons/8/8f/Logo-engie.svg">
|
| 257 |
+
</div>
|
| 258 |
+
<div class="ld-spinner"></div>
|
| 259 |
+
<div class="ld-title">PMJA — Gestão de Materiais</div>
|
| 260 |
+
<div class="ld-sub">UHE Jaguara · Rifaina SP</div>
|
| 261 |
+
<div class="ld-bar-wrap"><div class="ld-bar" id="pmja-bar"></div></div>
|
| 262 |
+
<div class="ld-status" id="pmja-status">Autenticando
|
| 263 |
+
<span class="ld-dots"><span>.</span><span>.</span><span>.</span></span>
|
| 264 |
+
</div>
|
| 265 |
+
</div>
|
| 266 |
+
<script>
|
| 267 |
+
(function(){
|
| 268 |
+
var steps=[
|
| 269 |
+
{p:25,msg:'Autenticando'},
|
| 270 |
+
{p:55,msg:'Verificando credenciais'},
|
| 271 |
+
{p:80,msg:'Carregando dados'},
|
| 272 |
+
{p:96,msg:'Redirecionando'}
|
| 273 |
+
];
|
| 274 |
+
var i=0;
|
| 275 |
+
function next(){
|
| 276 |
+
if(i>=steps.length)return;
|
| 277 |
+
var s=steps[i++];
|
| 278 |
+
var bar=document.getElementById('pmja-bar');
|
| 279 |
+
var st=document.getElementById('pmja-status');
|
| 280 |
+
if(bar)bar.style.width=s.p+'%';
|
| 281 |
+
if(st)st.innerHTML=s.msg+'<span class="ld-dots"><span>.</span><span>.</span><span>.</span></span>';
|
| 282 |
+
setTimeout(next,700+Math.random()*400);
|
| 283 |
+
}
|
| 284 |
+
setTimeout(next,200);
|
| 285 |
+
})();
|
| 286 |
+
</script>
|
| 287 |
+
"""
|
| 288 |
+
|
| 289 |
# ── CSS ───────────────────────────────────────────────────────────────
|
| 290 |
_CSS = """\
|
| 291 |
<style>
|
|
|
|
| 336 |
"""
|
| 337 |
_html(_CSS, height=0)
|
| 338 |
|
| 339 |
+
# ── PAGE STATE ────────────────────────────────────────────────────────
|
| 340 |
pg = "dashboard" if st.session_state.logged_in else st.session_state.page
|
| 341 |
user = st.session_state.user_data
|
| 342 |
|
|
|
|
| 400 |
st.text_input("Usuário", placeholder="nome_usuario", key="li_u")
|
| 401 |
st.text_input("Senha", placeholder="••••••••", type="password", key="li_p")
|
| 402 |
sub = st.form_submit_button("Entrar →", type="primary", use_container_width=True)
|
| 403 |
+
|
| 404 |
if sub:
|
| 405 |
u = st.session_state.li_u.strip(); p = st.session_state.li_p
|
| 406 |
if not (u and p):
|
| 407 |
+
st.session_state.msg = "Preencha todos os campos"
|
| 408 |
+
st.session_state.msg_type = "error"
|
| 409 |
+
st.rerun()
|
| 410 |
else:
|
| 411 |
+
# Mostra loading antes de autenticar
|
| 412 |
+
loading_ph = st.empty()
|
| 413 |
+
loading_ph.markdown(LOADING_MD, unsafe_allow_html=True)
|
| 414 |
+
|
| 415 |
ok, result = login_user(u, p)
|
| 416 |
+
|
| 417 |
if ok:
|
| 418 |
+
st.session_state.logged_in = True
|
| 419 |
+
st.session_state.user_data = result
|
| 420 |
save_session(result["id"], u, SESSION_EXPIRY_HOURS)
|
| 421 |
st.switch_page("pages/rec.py")
|
| 422 |
elif result == "EMAIL_NOT_VERIFIED":
|
| 423 |
+
loading_ph.empty()
|
| 424 |
resend_verification_code(u)
|
| 425 |
+
st.session_state.temp_username = u
|
| 426 |
+
st.session_state.page = "verify"
|
| 427 |
+
st.session_state.msg = "📧 Código reenviado para seu email"
|
| 428 |
+
st.session_state.msg_type = "info"
|
| 429 |
+
st.rerun()
|
| 430 |
else:
|
| 431 |
+
loading_ph.empty()
|
| 432 |
+
st.session_state.msg = result
|
| 433 |
+
st.session_state.msg_type = "error"
|
| 434 |
+
st.rerun()
|
| 435 |
+
|
| 436 |
divider()
|
| 437 |
if st.button("Criar nova conta →", use_container_width=True, key="go_reg"):
|
| 438 |
st.session_state.page = "register"; st.session_state.msg = ""; st.rerun()
|
|
|
|
| 536 |
"<div style='text-align:center;padding-top:12px;font-size:10px;color:#94a3b8;letter-spacing:.1em;'>"
|
| 537 |
"PMJA · UHE Jaguara · Rifaina SP</div>",
|
| 538 |
unsafe_allow_html=True,
|
| 539 |
+
)
|
|
|
pages/atual.py
CHANGED
|
@@ -8,7 +8,39 @@ from gs_client import read_ws, write_ws
|
|
| 8 |
from dotenv import load_dotenv
|
| 9 |
load_dotenv()
|
| 10 |
|
| 11 |
-
# ──
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
def now_brt():
|
| 13 |
return datetime.now(ZoneInfo("America/Sao_Paulo"))
|
| 14 |
|
|
@@ -71,6 +103,181 @@ def session_mins():
|
|
| 71 |
return max(0, int((exp - datetime.now()).total_seconds() // 60))
|
| 72 |
return 0
|
| 73 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
# ── EMAIL: TAREFA CRIADA ──────────────────────────────────────────────
|
| 75 |
def send_task_created_email(task_row):
|
| 76 |
def clean(v, fb=""):
|
|
@@ -128,7 +335,6 @@ def send_task_created_email(task_row):
|
|
| 128 |
except Exception as e:
|
| 129 |
st.warning(f"Falha ao enviar email: {e}")
|
| 130 |
|
| 131 |
-
|
| 132 |
# ── EMAIL: TAREFA FINALIZADA ──────────────────────────────────────────
|
| 133 |
def send_task_done_email(task_row):
|
| 134 |
def clean(v, fb=""):
|
|
@@ -184,124 +390,6 @@ def send_task_done_email(task_row):
|
|
| 184 |
except Exception as e:
|
| 185 |
st.warning(f"Falha ao enviar email: {e}")
|
| 186 |
|
| 187 |
-
# ── AUTH ──────────────────────────────────────────────────────────────
|
| 188 |
-
if not st.session_state.get("logged_in"):
|
| 189 |
-
s = load_session()
|
| 190 |
-
if not s:
|
| 191 |
-
st.switch_page("app.py")
|
| 192 |
-
|
| 193 |
-
# ── AUTO SWITCH ───────────────────────────────────────────────────────
|
| 194 |
-
if 'inicio_exibicao' not in st.session_state:
|
| 195 |
-
st.session_state.inicio_exibicao = time.time()
|
| 196 |
-
if time.time() - st.session_state.inicio_exibicao >= 180:
|
| 197 |
-
st.session_state.inicio_exibicao = time.time()
|
| 198 |
-
st.switch_page("pages/rec.py")
|
| 199 |
-
|
| 200 |
-
st.set_page_config(
|
| 201 |
-
layout="wide",
|
| 202 |
-
initial_sidebar_state="collapsed",
|
| 203 |
-
page_title="TaskFlow | Kanban",
|
| 204 |
-
page_icon="📊"
|
| 205 |
-
)
|
| 206 |
-
|
| 207 |
-
# ── ACESSO A DADOS via gs_client ──────────────────────────────────────
|
| 208 |
-
def _read(planilha, ws, ttl=600):
|
| 209 |
-
"""Lê worksheet; planilha='main' ou 'users'."""
|
| 210 |
-
# ttl=0 significa forçar leitura fresca (ignora cache)
|
| 211 |
-
if ttl == 0:
|
| 212 |
-
read_ws.clear()
|
| 213 |
-
df = read_ws(ws, spreadsheet=planilha)
|
| 214 |
-
if df is None or df.empty:
|
| 215 |
-
return pd.DataFrame()
|
| 216 |
-
for col in df.columns:
|
| 217 |
-
if df[col].dtype == object:
|
| 218 |
-
df[col] = df[col].fillna("").astype(str)
|
| 219 |
-
df[col] = df[col].replace("nan", "").replace("None", "")
|
| 220 |
-
return df
|
| 221 |
-
|
| 222 |
-
def _update(planilha, ws, df):
|
| 223 |
-
write_ws(ws, df, spreadsheet=planilha)
|
| 224 |
-
|
| 225 |
-
# ── DB ────────────────────────────────────────────────────────────────
|
| 226 |
-
def calc_status(d):
|
| 227 |
-
try:
|
| 228 |
-
dl = datetime.strptime(str(d).strip(), '%d/%m/%Y').date()
|
| 229 |
-
t = now_brt().date()
|
| 230 |
-
return "Atrasada" if dl < t else "Curto Prazo" if dl <= t + timedelta(days=3) else "Em dia"
|
| 231 |
-
except:
|
| 232 |
-
return "Em dia"
|
| 233 |
-
|
| 234 |
-
def make_formulas(row_num, responsible_id, user_id):
|
| 235 |
-
return {
|
| 236 |
-
'responsible': f'=SEERRO(PROCX(D{row_num};users_auth!A:A;users_auth!E:E;"");"")' ,
|
| 237 |
-
'url_responsible': f'=SEERRO(PROCX(D{row_num};users_auth!A:A;users_auth!K:K;"");"")' ,
|
| 238 |
-
'email_responsible': f'=SEERRO(PROCX(D{row_num};users_auth!A:A;users_auth!C:C;"");"")' ,
|
| 239 |
-
'user_full_name': f'=SEERRO(PROCX(N{row_num};users_auth!A:A;users_auth!E:E;"");"")' ,
|
| 240 |
-
'user_email': f'=SEERRO(PROCX(N{row_num};users_auth!A:A;users_auth!C:C;"");"")' ,
|
| 241 |
-
'user_image': f'=SEERRO(PROCX(N{row_num};users_auth!A:A;users_auth!K:K;"");"")' ,
|
| 242 |
-
'status': f'=SE(G{row_num}="";"";SE(G{row_num}<HOJE();"Atrasada";SE(G{row_num}<=HOJE()+3;"Curto Prazo";"Em dia")))',
|
| 243 |
-
}
|
| 244 |
-
|
| 245 |
-
def recalc():
|
| 246 |
-
try:
|
| 247 |
-
df = _read("users", "tasks", ttl=0)
|
| 248 |
-
if df.empty: return True
|
| 249 |
-
for i in df.index:
|
| 250 |
-
row_num = i + 2
|
| 251 |
-
resp_id = df.loc[i, 'responsible_id'] if 'responsible_id' in df.columns else ''
|
| 252 |
-
user_id = df.loc[i, 'user_id'] if 'user_id' in df.columns else ''
|
| 253 |
-
formulas = make_formulas(row_num, resp_id, user_id)
|
| 254 |
-
for k, val in formulas.items():
|
| 255 |
-
df.loc[i, k] = val
|
| 256 |
-
_update("users", "tasks", df)
|
| 257 |
-
load_data.clear()
|
| 258 |
-
return True
|
| 259 |
-
except Exception as e:
|
| 260 |
-
st.error(f"recalc: {e}")
|
| 261 |
-
return False
|
| 262 |
-
|
| 263 |
-
@st.cache_data(ttl=600)
|
| 264 |
-
def load_data():
|
| 265 |
-
try:
|
| 266 |
-
u = _read("users", "users_auth")
|
| 267 |
-
c = _read("users", "config")
|
| 268 |
-
t = _read("users", "tasks")
|
| 269 |
-
|
| 270 |
-
if not t.empty:
|
| 271 |
-
# Normaliza my_task
|
| 272 |
-
_map = {
|
| 273 |
-
'a fazer': 'A Fazer', 'em andamento': 'Em Andamento',
|
| 274 |
-
'paralizada': 'Paralizada', 'finalizada': 'Finalizada',
|
| 275 |
-
'finalizado': 'Finalizada',
|
| 276 |
-
}
|
| 277 |
-
if 'my_task' not in t.columns:
|
| 278 |
-
t['my_task'] = 'A Fazer'
|
| 279 |
-
else:
|
| 280 |
-
t['my_task'] = t['my_task'].apply(
|
| 281 |
-
lambda v: _map.get(str(v).strip().lower(), str(v).strip()) or 'A Fazer'
|
| 282 |
-
)
|
| 283 |
-
|
| 284 |
-
# Recalcula status localmente (ignora fórmula do Sheets)
|
| 285 |
-
if 'deadline' in t.columns:
|
| 286 |
-
t['status'] = t['deadline'].apply(calc_status)
|
| 287 |
-
|
| 288 |
-
# ID numérico para comparações seguras
|
| 289 |
-
if 'id' in t.columns:
|
| 290 |
-
t['id'] = pd.to_numeric(t['id'], errors='coerce').fillna(0).astype(int)
|
| 291 |
-
# Remove linhas com id=0 (linhas vazias vindas do Google Sheets)
|
| 292 |
-
t = t[t['id'] != 0].reset_index(drop=True)
|
| 293 |
-
|
| 294 |
-
return (
|
| 295 |
-
u, c, t,
|
| 296 |
-
u['full_name'].tolist() if not u.empty and 'full_name' in u.columns else [],
|
| 297 |
-
c['priority'].tolist() if not c.empty and 'priority' in c.columns else [],
|
| 298 |
-
)
|
| 299 |
-
except Exception as e:
|
| 300 |
-
st.error(f"Erro ao carregar dados: {e}")
|
| 301 |
-
return pd.DataFrame(), pd.DataFrame(), pd.DataFrame(), [], []
|
| 302 |
-
|
| 303 |
-
users_df, config_df, tasks_df, users_list, prio_list = load_data()
|
| 304 |
-
|
| 305 |
def update_sheet(td, action):
|
| 306 |
try:
|
| 307 |
df = _read("users", "tasks", ttl=0)
|
|
@@ -352,34 +440,7 @@ def update_sheet(td, action):
|
|
| 352 |
st.error(f"update_sheet: {e}")
|
| 353 |
return False
|
| 354 |
|
| 355 |
-
|
| 356 |
-
# ── CSS ───────────────────────────────────────────────────────────────
|
| 357 |
-
st.markdown("""<style>
|
| 358 |
-
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
|
| 359 |
-
*{font-family:'Inter',sans-serif!important}
|
| 360 |
-
#MainMenu,footer,header,.stDeployButton,[data-testid="stToolbar"],[data-testid="stToolbarActions"],
|
| 361 |
-
[data-testid="stDecoration"],[data-testid="stStatusWidget"],[data-testid="collapsedControl"],
|
| 362 |
-
[data-testid="stSidebarCollapsedControl"],[data-testid="manage-app-button"],[data-testid="stBaseButton-header"],
|
| 363 |
-
button[title="Deploy"],button[aria-label="Deploy"],button[aria-label="Share"],button[title="Share"],
|
| 364 |
-
.stAppDeployButton,section[data-testid="stSidebar"],div[class*="deployButton"],div[class*="viewerBadge"],
|
| 365 |
-
div[class*="StatusWidget"],iframe[title="streamlit_analytics"]{display:none!important}
|
| 366 |
-
footer{visibility:hidden!important;height:0!important}
|
| 367 |
-
.block-container,.element-container,.stMarkdown{padding:0!important;margin:0!important;max-width:100%!important}
|
| 368 |
-
.stButton>button{position:fixed!important;left:-9999px!important;opacity:0!important;pointer-events:all!important;width:1px!important;height:1px!important}
|
| 369 |
-
[data-testid="stHorizontalBlock"]{gap:0!important;margin:0!important;padding:0!important;height:0!important;overflow:visible!important;position:relative!important}
|
| 370 |
-
iframe{position:fixed!important;top:0!important;left:0!important;width:100vw!important;height:100dvh!important;border:none!important;z-index:9999!important}
|
| 371 |
-
@media(max-width:900px){
|
| 372 |
-
iframe{position:relative!important;width:100%!important;height:100dvh!important;overflow:auto!important}
|
| 373 |
-
[data-testid="stAppViewContainer"],[data-testid="stMain"],.main,.block-container{height:100dvh!important;overflow:visible!important;padding:0!important}
|
| 374 |
-
}
|
| 375 |
-
.main,[data-testid="stAppViewContainer"]{background:#fff!important}
|
| 376 |
-
div[data-testid="stDialog"]{z-index:99999!important;position:fixed!important}
|
| 377 |
-
div[data-testid="stDialog"] .stButton>button{font-size:13px!important;color:#374151!important;background:#fff!important;border:1px solid rgba(0,0,0,.12)!important;padding:6px 14px!important;height:34px!important;width:auto!important;border-radius:8px!important;cursor:pointer!important;position:relative!important;left:auto!important;top:auto!important;opacity:1!important}
|
| 378 |
-
div[data-testid="stDialog"] .stButton>button[kind="primary"]{background:#1d4ed8!important;border-color:#1d4ed8!important;color:#fff!important}
|
| 379 |
-
div[data-testid="stDialog"] [data-testid="stHorizontalBlock"]{height:auto!important;overflow:visible!important;gap:8px!important}
|
| 380 |
-
</style>""", unsafe_allow_html=True)
|
| 381 |
-
|
| 382 |
-
# ── STATE ──
|
| 383 |
for k, v in [('dialog_action', None), ('dialog_task_id', None), ('show_menu', False)]:
|
| 384 |
st.session_state.setdefault(k, v)
|
| 385 |
|
|
@@ -442,7 +503,7 @@ if not st.session_state.get("user_data"):
|
|
| 442 |
else:
|
| 443 |
clear_session(); st.switch_page("app.py")
|
| 444 |
|
| 445 |
-
user
|
| 446 |
img_url = user.get('image_url', '')
|
| 447 |
mins = session_mins()
|
| 448 |
|
|
@@ -626,16 +687,11 @@ def build_board(df, u, img, mins, show_menu, prios, stats, resps):
|
|
| 626 |
im = ''
|
| 627 |
ava = (f'<img src="{im}" onerror="this.style.display=\'none\'">'
|
| 628 |
if im else f'<div class="av-fb">{"".join(w[0].upper() for w in nm.split()[:2]) or "?"}</div>')
|
| 629 |
-
h += (f'<div class="card"
|
| 630 |
f' data-priority="{t.get("priority","")}" data-stat="{t.get("status","")}"'
|
| 631 |
f' data-title="{str(t.get("title","")).lower()}" data-desc="{str(t.get("description","")).lower()}"'
|
| 632 |
f' data-responsible="{nm.lower()}">'
|
| 633 |
-
|
| 634 |
-
f'<button class="act" onclick="event.stopPropagation();sa(\'edit\',{tid3})" title="Editar">'
|
| 635 |
-
f'<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4z"/></svg></button>'
|
| 636 |
-
f'<button class="act act-d" onclick="event.stopPropagation();sa(\'delete\',{tid3})" title="Excluir">'
|
| 637 |
-
f'<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14H6L5 6"/><path d="M10 11v6M14 11v6M9 6V4h6v2"/></svg></button>'
|
| 638 |
-
f'</div>'
|
| 639 |
f'<div class="ct">{t["title"]}</div>{dh}'
|
| 640 |
f'<div class="cb">{sbadge(t["status"])} {pbadge(t["priority"])}</div>'
|
| 641 |
f'<div class="cf"><span class="cd2">📅 {t["deadline"]}</span>'
|
|
@@ -695,16 +751,12 @@ html,body{{height:100%;overflow:hidden;background:#fff}}
|
|
| 695 |
.pf{{height:100%;border-radius:10px;opacity:.6;transition:width .3s ease}}
|
| 696 |
.ctg{{display:none;font-size:12px;color:#9ca3af;transition:transform .2s;margin-left:6px}}
|
| 697 |
.dz{{flex:1;overflow-y:auto;overflow-x:hidden;padding:6px;display:flex;flex-direction:column;gap:6px;min-height:60px}}
|
| 698 |
-
.
|
| 699 |
-
.card{{background:rgba(255,255,255,.8);border:1px solid rgba(255,255,255,.95);border-radius:8px;padding:9px 10px;cursor:grab;position:relative;transition:box-shadow .14s,transform .14s,background .14s,opacity .2s;flex-shrink:0;backdrop-filter:blur(4px)}}
|
| 700 |
.card:hover{{background:#fff;box-shadow:0 3px 12px rgba(0,0,0,.1);transform:translateY(-1px)}}
|
| 701 |
-
.card:hover .ca{{opacity:1;pointer-events:all}}.card:active{{cursor:
|
| 702 |
-
.card.
|
| 703 |
-
.
|
| 704 |
-
|
| 705 |
-
.act{{width:20px;height:20px;border-radius:5px;border:1px solid rgba(0,0,0,.09);background:rgba(255,255,255,.9);color:#6b7280;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .12s}}
|
| 706 |
-
.act:hover{{background:#f3f4f6;color:#111827}}.act-d:hover{{background:#fef2f2!important;color:#dc2626!important}}
|
| 707 |
-
.ct{{font-size:12px;font-weight:600;color:#111827;line-height:1.4;margin-bottom:4px;padding-right:48px;word-break:break-word}}
|
| 708 |
.cd{{font-size:10.5px;color:#6b7280;line-height:1.45;margin-bottom:6px;word-break:break-word;white-space:pre-wrap}}
|
| 709 |
.cb{{display:flex;flex-wrap:wrap;gap:3px;margin-bottom:7px}}
|
| 710 |
.badge{{display:inline-flex;align-items:center;font-size:9px;font-weight:600;padding:2px 6px;border-radius:4px;text-transform:uppercase;letter-spacing:.3px;border:1px solid transparent;line-height:1.4}}
|
|
@@ -716,7 +768,6 @@ html,body{{height:100%;overflow:hidden;background:#fff}}
|
|
| 716 |
.cu img{{width:18px;height:18px;border-radius:50%;object-fit:cover;border:1.5px solid rgba(0,0,0,.09)}}
|
| 717 |
.av-fb{{width:18px;height:18px;border-radius:50%;background:#e5e7eb;color:#374151;font-size:8px;font-weight:700;display:flex;align-items:center;justify-content:center;flex-shrink:0}}
|
| 718 |
.cu span{{font-size:10px;color:#6b7280;font-weight:500}}
|
| 719 |
-
.toast{{position:fixed;bottom:12px;right:12px;background:#111827;color:#f9fafb;padding:7px 14px;border-radius:7px;font-size:11px;font-weight:500;display:none;z-index:9999}}
|
| 720 |
@media(max-width:900px){{
|
| 721 |
html,body{{height:auto!important;overflow-x:hidden!important;overflow-y:auto!important;min-height:100%}}
|
| 722 |
.tbs{{display:none}}.tbf{{display:none}}.tft{{display:flex}}.tbun{{display:none}}
|
|
@@ -770,7 +821,6 @@ html,body{{height:100%;overflow:hidden;background:#fff}}
|
|
| 770 |
<select class="sel" id="fRm" onchange="af()">{ro}</select>
|
| 771 |
<button class="tbcl" id="bCm" onclick="cf()">✕</button>
|
| 772 |
</div>
|
| 773 |
-
<div class="toast" id="toast">Salvando…</div>
|
| 774 |
<div class="board">{cols}</div>
|
| 775 |
<script>
|
| 776 |
function sa(a,id,st){{
|
|
@@ -785,10 +835,6 @@ function sa(a,id,st){{
|
|
| 785 |
}}catch(e){{console.warn(e)}}
|
| 786 |
setTimeout(()=>{{try{{const u=new URL(window.parent.location.href);if(u.searchParams.get('action')===a)window.parent.location.href=u.toString();}}catch(e2){{}}}},150);
|
| 787 |
}}
|
| 788 |
-
function mv(id,st){{
|
| 789 |
-
const t=document.getElementById('toast');t.style.display='block';t.textContent='Salvando…';
|
| 790 |
-
sa('move',id,st);setTimeout(()=>{{t.textContent='✓ Salvo!';setTimeout(()=>t.style.display='none',1000);}},400);
|
| 791 |
-
}}
|
| 792 |
function gv(id){{const e=document.getElementById(id);return e?(e.value||'').toLowerCase().trim():''}}
|
| 793 |
function af(){{
|
| 794 |
const s=gv('fS')||gv('fSm'),p=gv('fP')||gv('fPm'),st=gv('fSt')||gv('fStm'),r=gv('fR')||gv('fRm');
|
|
@@ -820,18 +866,6 @@ function tfd(){{
|
|
| 820 |
const d=document.getElementById('fdw'),b=document.getElementById('bFT');
|
| 821 |
b.classList.toggle('active',d.classList.toggle('open'));
|
| 822 |
}}
|
| 823 |
-
let dc;
|
| 824 |
-
document.addEventListener('dragstart',e=>{{if(e.target.classList.contains('card')){{dc=e.target;setTimeout(()=>e.target.classList.add('dragging'),0);e.dataTransfer.effectAllowed='move';}}}});
|
| 825 |
-
document.addEventListener('dragend',e=>{{if(e.target.classList.contains('card'))e.target.classList.remove('dragging');}});
|
| 826 |
-
document.querySelectorAll('.dz').forEach(z=>{{
|
| 827 |
-
z.addEventListener('dragover',e=>{{e.preventDefault();z.classList.add('over');}});
|
| 828 |
-
z.addEventListener('dragleave',e=>{{if(!z.contains(e.relatedTarget))z.classList.remove('over');}});
|
| 829 |
-
z.addEventListener('drop',e=>{{
|
| 830 |
-
e.preventDefault();z.classList.remove('over');
|
| 831 |
-
if(dc){{const id=dc.getAttribute('data-id'),ov=dc.getAttribute('data-status'),nv=z.getAttribute('data-status');
|
| 832 |
-
if(ov!==nv){{dc.setAttribute('data-status',nv);z.appendChild(dc);mv(id,nv);}}}}
|
| 833 |
-
}});
|
| 834 |
-
}});
|
| 835 |
</script></body></html>'''
|
| 836 |
|
| 837 |
components.html(
|
|
|
|
| 8 |
from dotenv import load_dotenv
|
| 9 |
load_dotenv()
|
| 10 |
|
| 11 |
+
# ── CONFIG — deve ser o PRIMEIRO comando st. ──────────────────────────
|
| 12 |
+
st.set_page_config(
|
| 13 |
+
layout="wide",
|
| 14 |
+
initial_sidebar_state="collapsed",
|
| 15 |
+
page_title="TaskFlow | Kanban",
|
| 16 |
+
page_icon="📊"
|
| 17 |
+
)
|
| 18 |
+
|
| 19 |
+
# ── ESCONDE UI DO STREAMLIT imediatamente ─────────────────────────────
|
| 20 |
+
st.markdown("""<style>
|
| 21 |
+
#MainMenu,footer,header,.stDeployButton,[data-testid="stToolbar"],[data-testid="stToolbarActions"],
|
| 22 |
+
[data-testid="stDecoration"],[data-testid="stStatusWidget"],[data-testid="collapsedControl"],
|
| 23 |
+
[data-testid="stSidebarCollapsedControl"],[data-testid="manage-app-button"],[data-testid="stBaseButton-header"],
|
| 24 |
+
button[title="Deploy"],button[aria-label="Deploy"],button[aria-label="Share"],button[title="Share"],
|
| 25 |
+
.stAppDeployButton,section[data-testid="stSidebar"],div[class*="deployButton"],div[class*="viewerBadge"],
|
| 26 |
+
div[class*="StatusWidget"]{display:none!important}
|
| 27 |
+
footer{visibility:hidden!important;height:0!important}
|
| 28 |
+
.block-container,.element-container,.stMarkdown{padding:0!important;margin:0!important;max-width:100%!important}
|
| 29 |
+
.stButton>button{position:fixed!important;left:-9999px!important;opacity:0!important;pointer-events:all!important;width:1px!important;height:1px!important}
|
| 30 |
+
[data-testid="stHorizontalBlock"]{gap:0!important;margin:0!important;padding:0!important;height:0!important;overflow:visible!important;position:relative!important}
|
| 31 |
+
iframe{position:fixed!important;top:0!important;left:0!important;width:100vw!important;height:100dvh!important;border:none!important;z-index:9999!important}
|
| 32 |
+
@media(max-width:900px){
|
| 33 |
+
iframe{position:relative!important;width:100%!important;height:100dvh!important;overflow:auto!important}
|
| 34 |
+
[data-testid="stAppViewContainer"],[data-testid="stMain"],.main,.block-container{height:100dvh!important;overflow:visible!important;padding:0!important}
|
| 35 |
+
}
|
| 36 |
+
.main,[data-testid="stAppViewContainer"]{background:#fff!important}
|
| 37 |
+
div[data-testid="stDialog"]{z-index:99999!important;position:fixed!important}
|
| 38 |
+
div[data-testid="stDialog"] .stButton>button{font-size:13px!important;color:#374151!important;background:#fff!important;border:1px solid rgba(0,0,0,.12)!important;padding:6px 14px!important;height:34px!important;width:auto!important;border-radius:8px!important;cursor:pointer!important;position:relative!important;left:auto!important;top:auto!important;opacity:1!important}
|
| 39 |
+
div[data-testid="stDialog"] .stButton>button[kind="primary"]{background:#1d4ed8!important;border-color:#1d4ed8!important;color:#fff!important}
|
| 40 |
+
div[data-testid="stDialog"] [data-testid="stHorizontalBlock"]{height:auto!important;overflow:visible!important;gap:8px!important}
|
| 41 |
+
</style>""", unsafe_allow_html=True)
|
| 42 |
+
|
| 43 |
+
# ── TIMEZONE BRASÍLIA ─────────────────────────────────────────────────
|
| 44 |
def now_brt():
|
| 45 |
return datetime.now(ZoneInfo("America/Sao_Paulo"))
|
| 46 |
|
|
|
|
| 103 |
return max(0, int((exp - datetime.now()).total_seconds() // 60))
|
| 104 |
return 0
|
| 105 |
|
| 106 |
+
# ── AUTH ──────────────────────────────────────────────────────────────
|
| 107 |
+
if not st.session_state.get("logged_in"):
|
| 108 |
+
s = load_session()
|
| 109 |
+
if not s:
|
| 110 |
+
st.switch_page("app.py")
|
| 111 |
+
|
| 112 |
+
# ── AUTO SWITCH ───────────────────────────────────────────────────────
|
| 113 |
+
if 'inicio_exibicao' not in st.session_state:
|
| 114 |
+
st.session_state.inicio_exibicao = time.time()
|
| 115 |
+
if time.time() - st.session_state.inicio_exibicao >= 180:
|
| 116 |
+
st.session_state.inicio_exibicao = time.time()
|
| 117 |
+
st.switch_page("pages/rec.py")
|
| 118 |
+
|
| 119 |
+
# ── LOADING SCREEN ────────────────────────────────────────────────────
|
| 120 |
+
LOADING_MD = """
|
| 121 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700;800&family=Sora:wght@700;800&display=swap" rel="stylesheet">
|
| 122 |
+
<style>
|
| 123 |
+
#pmja-loading{position:fixed;inset:0;z-index:999999;
|
| 124 |
+
background:linear-gradient(135deg,#1d4ed8 0%,#1e40af 100%);
|
| 125 |
+
display:flex;flex-direction:column;align-items:center;justify-content:center;
|
| 126 |
+
font-family:'Inter',sans-serif;}
|
| 127 |
+
#pmja-loading .ld-logos{display:flex;align-items:center;gap:28px;margin-bottom:40px}
|
| 128 |
+
#pmja-loading .ld-logo{height:36px;filter:brightness(0) invert(1);opacity:.9}
|
| 129 |
+
#pmja-loading .ld-div{width:1px;height:36px;background:rgba(255,255,255,.25)}
|
| 130 |
+
#pmja-loading .ld-title{font-family:'Sora',sans-serif;font-size:clamp(16px,4vw,22px);
|
| 131 |
+
color:#fff;font-weight:800;letter-spacing:-.5px;margin-bottom:4px;text-align:center}
|
| 132 |
+
#pmja-loading .ld-sub{font-size:clamp(11px,3vw,13px);color:rgba(255,255,255,.7);
|
| 133 |
+
font-weight:600;margin-bottom:44px;text-align:center}
|
| 134 |
+
#pmja-loading .ld-spinner{width:34px;height:34px;border:3px solid rgba(255,255,255,.2);
|
| 135 |
+
border-top-color:#fff;border-radius:50%;animation:ldspin .8s linear infinite;margin-bottom:28px}
|
| 136 |
+
@keyframes ldspin{to{transform:rotate(360deg)}}
|
| 137 |
+
#pmja-loading .ld-bar-wrap{width:min(280px,70vw);height:4px;
|
| 138 |
+
background:rgba(255,255,255,.15);border-radius:99px;overflow:hidden;margin-bottom:16px}
|
| 139 |
+
#pmja-loading .ld-bar{height:100%;width:0%;background:#fff;border-radius:99px;transition:width .4s ease}
|
| 140 |
+
#pmja-loading .ld-status{font-size:12px;color:rgba(255,255,255,.65);font-weight:600;
|
| 141 |
+
letter-spacing:.3px;text-align:center}
|
| 142 |
+
#pmja-loading .ld-dots span{animation:ldblink 1.2s infinite;opacity:0}
|
| 143 |
+
#pmja-loading .ld-dots span:nth-child(2){animation-delay:.2s}
|
| 144 |
+
#pmja-loading .ld-dots span:nth-child(3){animation-delay:.4s}
|
| 145 |
+
@keyframes ldblink{0%,100%{opacity:0}50%{opacity:1}}
|
| 146 |
+
</style>
|
| 147 |
+
<div id="pmja-loading">
|
| 148 |
+
<div class="ld-logos">
|
| 149 |
+
<img class="ld-logo" src="https://companieslogo.com/img/orig/AZ2.F-d26946db.png?t=1720244490">
|
| 150 |
+
<div class="ld-div"></div>
|
| 151 |
+
<img class="ld-logo" src="https://upload.wikimedia.org/wikipedia/commons/8/8f/Logo-engie.svg">
|
| 152 |
+
</div>
|
| 153 |
+
<div class="ld-spinner"></div>
|
| 154 |
+
<div class="ld-title">PMJA — Kanban</div>
|
| 155 |
+
<div class="ld-sub">Gestão de Materiais</div>
|
| 156 |
+
<div class="ld-bar-wrap"><div class="ld-bar" id="pmja-bar"></div></div>
|
| 157 |
+
<div class="ld-status" id="pmja-status">Conectando ao Google Sheets
|
| 158 |
+
<span class="ld-dots"><span>.</span><span>.</span><span>.</span></span>
|
| 159 |
+
</div>
|
| 160 |
+
</div>
|
| 161 |
+
<script>
|
| 162 |
+
(function(){
|
| 163 |
+
var steps=[
|
| 164 |
+
{p:20,msg:'Conectando ao Google Sheets'},
|
| 165 |
+
{p:45,msg:'Carregando tarefas'},
|
| 166 |
+
{p:68,msg:'Carregando usuários'},
|
| 167 |
+
{p:88,msg:'Montando o board'},
|
| 168 |
+
{p:96,msg:'Quase pronto'}
|
| 169 |
+
];
|
| 170 |
+
var i=0;
|
| 171 |
+
function next(){
|
| 172 |
+
if(i>=steps.length)return;
|
| 173 |
+
var s=steps[i++];
|
| 174 |
+
var bar=document.getElementById('pmja-bar');
|
| 175 |
+
var st=document.getElementById('pmja-status');
|
| 176 |
+
if(bar)bar.style.width=s.p+'%';
|
| 177 |
+
if(st)st.innerHTML=s.msg+'<span class="ld-dots"><span>.</span><span>.</span><span>.</span></span>';
|
| 178 |
+
setTimeout(next,950+Math.random()*550);
|
| 179 |
+
}
|
| 180 |
+
setTimeout(next,300);
|
| 181 |
+
})();
|
| 182 |
+
</script>
|
| 183 |
+
"""
|
| 184 |
+
|
| 185 |
+
loading_placeholder = st.empty()
|
| 186 |
+
loading_placeholder.markdown(LOADING_MD, unsafe_allow_html=True)
|
| 187 |
+
|
| 188 |
+
# ── ACESSO A DADOS via gs_client ──────────────────────────────────────
|
| 189 |
+
def _read(planilha, ws, ttl=600):
|
| 190 |
+
if ttl == 0:
|
| 191 |
+
read_ws.clear()
|
| 192 |
+
df = read_ws(ws, spreadsheet=planilha)
|
| 193 |
+
if df is None or df.empty:
|
| 194 |
+
return pd.DataFrame()
|
| 195 |
+
for col in df.columns:
|
| 196 |
+
if df[col].dtype == object:
|
| 197 |
+
df[col] = df[col].fillna("").astype(str)
|
| 198 |
+
df[col] = df[col].replace("nan", "").replace("None", "")
|
| 199 |
+
return df
|
| 200 |
+
|
| 201 |
+
def _update(planilha, ws, df):
|
| 202 |
+
write_ws(ws, df, spreadsheet=planilha)
|
| 203 |
+
|
| 204 |
+
# ── DB ────────────────────────────────────────────────────────────────
|
| 205 |
+
def calc_status(d):
|
| 206 |
+
try:
|
| 207 |
+
dl = datetime.strptime(str(d).strip(), '%d/%m/%Y').date()
|
| 208 |
+
t = now_brt().date()
|
| 209 |
+
return "Atrasada" if dl < t else "Curto Prazo" if dl <= t + timedelta(days=3) else "Em dia"
|
| 210 |
+
except:
|
| 211 |
+
return "Em dia"
|
| 212 |
+
|
| 213 |
+
def make_formulas(row_num, responsible_id, user_id):
|
| 214 |
+
return {
|
| 215 |
+
'responsible': f'=SEERRO(PROCX(D{row_num};users_auth!A:A;users_auth!E:E;"");"")' ,
|
| 216 |
+
'url_responsible': f'=SEERRO(PROCX(D{row_num};users_auth!A:A;users_auth!K:K;"");"")' ,
|
| 217 |
+
'email_responsible': f'=SEERRO(PROCX(D{row_num};users_auth!A:A;users_auth!C:C;"");"")' ,
|
| 218 |
+
'user_full_name': f'=SEERRO(PROCX(N{row_num};users_auth!A:A;users_auth!E:E;"");"")' ,
|
| 219 |
+
'user_email': f'=SEERRO(PROCX(N{row_num};users_auth!A:A;users_auth!C:C;"");"")' ,
|
| 220 |
+
'user_image': f'=SEERRO(PROCX(N{row_num};users_auth!A:A;users_auth!K:K;"");"")' ,
|
| 221 |
+
'status': f'=SE(G{row_num}="";"";SE(G{row_num}<HOJE();"Atrasada";SE(G{row_num}<=HOJE()+3;"Curto Prazo";"Em dia")))',
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
+
def recalc():
|
| 225 |
+
try:
|
| 226 |
+
df = _read("users", "tasks", ttl=0)
|
| 227 |
+
if df.empty: return True
|
| 228 |
+
for i in df.index:
|
| 229 |
+
row_num = i + 2
|
| 230 |
+
resp_id = df.loc[i, 'responsible_id'] if 'responsible_id' in df.columns else ''
|
| 231 |
+
user_id = df.loc[i, 'user_id'] if 'user_id' in df.columns else ''
|
| 232 |
+
formulas = make_formulas(row_num, resp_id, user_id)
|
| 233 |
+
for k, val in formulas.items():
|
| 234 |
+
df.loc[i, k] = val
|
| 235 |
+
_update("users", "tasks", df)
|
| 236 |
+
load_data.clear()
|
| 237 |
+
return True
|
| 238 |
+
except Exception as e:
|
| 239 |
+
st.error(f"recalc: {e}")
|
| 240 |
+
return False
|
| 241 |
+
|
| 242 |
+
@st.cache_data(ttl=600)
|
| 243 |
+
def load_data():
|
| 244 |
+
try:
|
| 245 |
+
u = _read("users", "users_auth")
|
| 246 |
+
c = _read("users", "config")
|
| 247 |
+
t = _read("users", "tasks")
|
| 248 |
+
|
| 249 |
+
if not t.empty:
|
| 250 |
+
_map = {
|
| 251 |
+
'a fazer': 'A Fazer', 'em andamento': 'Em Andamento',
|
| 252 |
+
'paralizada': 'Paralizada', 'finalizada': 'Finalizada',
|
| 253 |
+
'finalizado': 'Finalizada',
|
| 254 |
+
}
|
| 255 |
+
if 'my_task' not in t.columns:
|
| 256 |
+
t['my_task'] = 'A Fazer'
|
| 257 |
+
else:
|
| 258 |
+
t['my_task'] = t['my_task'].apply(
|
| 259 |
+
lambda v: _map.get(str(v).strip().lower(), str(v).strip()) or 'A Fazer'
|
| 260 |
+
)
|
| 261 |
+
if 'deadline' in t.columns:
|
| 262 |
+
t['status'] = t['deadline'].apply(calc_status)
|
| 263 |
+
if 'id' in t.columns:
|
| 264 |
+
t['id'] = pd.to_numeric(t['id'], errors='coerce').fillna(0).astype(int)
|
| 265 |
+
t = t[t['id'] != 0].reset_index(drop=True)
|
| 266 |
+
|
| 267 |
+
return (
|
| 268 |
+
u, c, t,
|
| 269 |
+
u['full_name'].tolist() if not u.empty and 'full_name' in u.columns else [],
|
| 270 |
+
c['priority'].tolist() if not c.empty and 'priority' in c.columns else [],
|
| 271 |
+
)
|
| 272 |
+
except Exception as e:
|
| 273 |
+
st.error(f"Erro ao carregar dados: {e}")
|
| 274 |
+
return pd.DataFrame(), pd.DataFrame(), pd.DataFrame(), [], []
|
| 275 |
+
|
| 276 |
+
users_df, config_df, tasks_df, users_list, prio_list = load_data()
|
| 277 |
+
|
| 278 |
+
# ── FECHA O LOADING ───────────────────────────────────────────────────
|
| 279 |
+
loading_placeholder.empty()
|
| 280 |
+
|
| 281 |
# ── EMAIL: TAREFA CRIADA ──────────────────────────────────────────────
|
| 282 |
def send_task_created_email(task_row):
|
| 283 |
def clean(v, fb=""):
|
|
|
|
| 335 |
except Exception as e:
|
| 336 |
st.warning(f"Falha ao enviar email: {e}")
|
| 337 |
|
|
|
|
| 338 |
# ── EMAIL: TAREFA FINALIZADA ──────────────────────────────────────────
|
| 339 |
def send_task_done_email(task_row):
|
| 340 |
def clean(v, fb=""):
|
|
|
|
| 390 |
except Exception as e:
|
| 391 |
st.warning(f"Falha ao enviar email: {e}")
|
| 392 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 393 |
def update_sheet(td, action):
|
| 394 |
try:
|
| 395 |
df = _read("users", "tasks", ttl=0)
|
|
|
|
| 440 |
st.error(f"update_sheet: {e}")
|
| 441 |
return False
|
| 442 |
|
| 443 |
+
# ── STATE ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 444 |
for k, v in [('dialog_action', None), ('dialog_task_id', None), ('show_menu', False)]:
|
| 445 |
st.session_state.setdefault(k, v)
|
| 446 |
|
|
|
|
| 503 |
else:
|
| 504 |
clear_session(); st.switch_page("app.py")
|
| 505 |
|
| 506 |
+
user = st.session_state.user_data
|
| 507 |
img_url = user.get('image_url', '')
|
| 508 |
mins = session_mins()
|
| 509 |
|
|
|
|
| 687 |
im = ''
|
| 688 |
ava = (f'<img src="{im}" onerror="this.style.display=\'none\'">'
|
| 689 |
if im else f'<div class="av-fb">{"".join(w[0].upper() for w in nm.split()[:2]) or "?"}</div>')
|
| 690 |
+
h += (f'<div class="card" data-id="{tid3}" data-status="{t["my_task"]}"'
|
| 691 |
f' data-priority="{t.get("priority","")}" data-stat="{t.get("status","")}"'
|
| 692 |
f' data-title="{str(t.get("title","")).lower()}" data-desc="{str(t.get("description","")).lower()}"'
|
| 693 |
f' data-responsible="{nm.lower()}">'
|
| 694 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 695 |
f'<div class="ct">{t["title"]}</div>{dh}'
|
| 696 |
f'<div class="cb">{sbadge(t["status"])} {pbadge(t["priority"])}</div>'
|
| 697 |
f'<div class="cf"><span class="cd2">📅 {t["deadline"]}</span>'
|
|
|
|
| 751 |
.pf{{height:100%;border-radius:10px;opacity:.6;transition:width .3s ease}}
|
| 752 |
.ctg{{display:none;font-size:12px;color:#9ca3af;transition:transform .2s;margin-left:6px}}
|
| 753 |
.dz{{flex:1;overflow-y:auto;overflow-x:hidden;padding:6px;display:flex;flex-direction:column;gap:6px;min-height:60px}}
|
| 754 |
+
.card{{background:rgba(255,255,255,.8);border:1px solid rgba(255,255,255,.95);border-radius:8px;padding:9px 10px;cursor:default;position:relative;transition:box-shadow .14s,transform .14s,background .14s,opacity .2s;flex-shrink:0;backdrop-filter:blur(4px)}}
|
|
|
|
| 755 |
.card:hover{{background:#fff;box-shadow:0 3px 12px rgba(0,0,0,.1);transform:translateY(-1px)}}
|
| 756 |
+
.card:hover .ca{{opacity:1;pointer-events:all}}.card:active{{cursor:default}}
|
| 757 |
+
.card.hidden{{display:none}}
|
| 758 |
+
.act-d:hover{{background:#fef2f2!important;color:#dc2626!important}}
|
| 759 |
+
.ct{{font-size:12px;font-weight:600;color:#111827;line-height:1.4;margin-bottom:4px;word-break:break-word}}
|
|
|
|
|
|
|
|
|
|
| 760 |
.cd{{font-size:10.5px;color:#6b7280;line-height:1.45;margin-bottom:6px;word-break:break-word;white-space:pre-wrap}}
|
| 761 |
.cb{{display:flex;flex-wrap:wrap;gap:3px;margin-bottom:7px}}
|
| 762 |
.badge{{display:inline-flex;align-items:center;font-size:9px;font-weight:600;padding:2px 6px;border-radius:4px;text-transform:uppercase;letter-spacing:.3px;border:1px solid transparent;line-height:1.4}}
|
|
|
|
| 768 |
.cu img{{width:18px;height:18px;border-radius:50%;object-fit:cover;border:1.5px solid rgba(0,0,0,.09)}}
|
| 769 |
.av-fb{{width:18px;height:18px;border-radius:50%;background:#e5e7eb;color:#374151;font-size:8px;font-weight:700;display:flex;align-items:center;justify-content:center;flex-shrink:0}}
|
| 770 |
.cu span{{font-size:10px;color:#6b7280;font-weight:500}}
|
|
|
|
| 771 |
@media(max-width:900px){{
|
| 772 |
html,body{{height:auto!important;overflow-x:hidden!important;overflow-y:auto!important;min-height:100%}}
|
| 773 |
.tbs{{display:none}}.tbf{{display:none}}.tft{{display:flex}}.tbun{{display:none}}
|
|
|
|
| 821 |
<select class="sel" id="fRm" onchange="af()">{ro}</select>
|
| 822 |
<button class="tbcl" id="bCm" onclick="cf()">✕</button>
|
| 823 |
</div>
|
|
|
|
| 824 |
<div class="board">{cols}</div>
|
| 825 |
<script>
|
| 826 |
function sa(a,id,st){{
|
|
|
|
| 835 |
}}catch(e){{console.warn(e)}}
|
| 836 |
setTimeout(()=>{{try{{const u=new URL(window.parent.location.href);if(u.searchParams.get('action')===a)window.parent.location.href=u.toString();}}catch(e2){{}}}},150);
|
| 837 |
}}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 838 |
function gv(id){{const e=document.getElementById(id);return e?(e.value||'').toLowerCase().trim():''}}
|
| 839 |
function af(){{
|
| 840 |
const s=gv('fS')||gv('fSm'),p=gv('fP')||gv('fPm'),st=gv('fSt')||gv('fStm'),r=gv('fR')||gv('fRm');
|
|
|
|
| 866 |
const d=document.getElementById('fdw'),b=document.getElementById('bFT');
|
| 867 |
b.classList.toggle('active',d.classList.toggle('open'));
|
| 868 |
}}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 869 |
</script></body></html>'''
|
| 870 |
|
| 871 |
components.html(
|
pages/exp.py
CHANGED
|
@@ -8,6 +8,22 @@ from gs_client import read_ws
|
|
| 8 |
|
| 9 |
st.set_page_config(layout="wide", initial_sidebar_state="collapsed", page_title="PMJA - Dashboard Expedição")
|
| 10 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
# ── SESSION ───────────────────────────────────────────────────────────
|
| 12 |
_cc = CookieController()
|
| 13 |
COOKIE_NAME = "pmja_session"
|
|
@@ -38,38 +54,71 @@ if time.time() - st.session_state.inicio_exibicao >= 120:
|
|
| 38 |
st.session_state.inicio_exibicao = time.time()
|
| 39 |
st.switch_page("pages/full.py")
|
| 40 |
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
.
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
}
|
| 55 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
|
| 57 |
# ── HELPERS ───────────────────────────────────────────────────────────
|
| 58 |
def fmt(n):
|
| 59 |
if pd.isna(n): return "0"
|
| 60 |
-
return
|
| 61 |
-
|
| 62 |
-
def fix_df(df):
|
| 63 |
-
for col in df.columns:
|
| 64 |
-
if df[col].dtype == 'object':
|
| 65 |
-
num = pd.to_numeric(df[col], errors='coerce')
|
| 66 |
-
df[col] = num if num.notna().sum() == df[col].notna().sum() \
|
| 67 |
-
else df[col].fillna('').astype(str).replace('nan', '')
|
| 68 |
-
return df
|
| 69 |
|
| 70 |
def extrair_categoria(col):
|
| 71 |
-
for s in [' Requisições',
|
| 72 |
-
' Itens por unidade',
|
| 73 |
col = col.replace(s, '')
|
| 74 |
return col.strip()
|
| 75 |
|
|
@@ -82,90 +131,75 @@ def proc_exp(df_raw):
|
|
| 82 |
df = df.dropna(subset=['data']).copy()
|
| 83 |
df['ano'] = df['data'].dt.year
|
| 84 |
df['mes_num'] = df['data'].dt.month
|
| 85 |
-
|
| 86 |
req, unid, itens = [], [], []
|
| 87 |
for col in df.columns:
|
| 88 |
-
c = col.lower().strip().replace('.',
|
| 89 |
if 'requisi' in c and 'unid' not in c and 'por' not in c: req.append(col)
|
| 90 |
if 'unid' in c and 'iten' in c and 'por' not in c: unid.append(col)
|
| 91 |
if 'iten' in c and 'por' in c and 'unid' in c: itens.append(col)
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
if col in df.columns:
|
| 95 |
-
df[col] = pd.to_numeric(df[col], errors='coerce').fillna(0)
|
| 96 |
-
|
| 97 |
df['qtd_requisicoes'] = df[req].sum(axis=1)
|
| 98 |
df['qtd_unidades_emitidas'] = df[unid].sum(axis=1)
|
| 99 |
df['qtd_itens_total'] = df[itens].sum(axis=1) if itens else 0
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
return df_final, df_det, req, unid, itens
|
| 105 |
|
| 106 |
# ── BUILD FRAMES ──────────────────────────────────────────────────────
|
| 107 |
def build_frames(df_plot, df_det, req_cols, unid_cols, itens_cols, anos_unicos, mes_max, CORES_EXP):
|
| 108 |
MESES_PT = {1:'Jan',2:'Fev',3:'Mar',4:'Abr',5:'Mai',6:'Jun',
|
| 109 |
7:'Jul',8:'Ago',9:'Set',10:'Out',11:'Nov',12:'Dez'}
|
| 110 |
-
|
| 111 |
metricas_linha = [
|
| 112 |
-
{'col':
|
| 113 |
-
{'col':
|
| 114 |
-
{'col':
|
| 115 |
]
|
| 116 |
-
tipos_l1 = ['barra_h',
|
| 117 |
-
|
| 118 |
frames = []
|
| 119 |
-
for mes_atual in range(1, mes_max
|
| 120 |
frame = {"mes": mes_atual, "cards": {}, "linha1": [], "linha2": []}
|
| 121 |
-
|
| 122 |
df_ate = df_plot[df_plot['mes_num'] <= mes_atual]
|
| 123 |
df_det_ate = df_det[df_det['mes_num'] <= mes_atual]
|
| 124 |
-
|
| 125 |
-
# cards
|
| 126 |
for col, tit, cor, suf in [
|
| 127 |
-
('qtd_requisicoes',
|
| 128 |
-
('qtd_unidades_emitidas',
|
| 129 |
-
('qtd_itens_total',
|
| 130 |
]:
|
| 131 |
frame["cards"][col] = {"valor": fmt(df_ate[col].sum()), "cor": cor, "titulo": tit, "sufixo": suf}
|
| 132 |
-
|
| 133 |
-
# linha1
|
| 134 |
meses_ex = sorted(df_plot[df_plot['mes_num'] <= mes_atual]['mes_num'].unique())
|
| 135 |
for idx_m, cfg in enumerate(metricas_linha):
|
| 136 |
col = cfg['col']
|
| 137 |
traces = []
|
| 138 |
for idx_ano, ano in enumerate(anos_unicos):
|
| 139 |
-
d = df_plot[(df_plot['ano']
|
| 140 |
if d.empty: continue
|
| 141 |
cor = cfg['cores'][idx_ano % len(cfg['cores'])]
|
| 142 |
vals, texts = [], []
|
| 143 |
for m in meses_ex:
|
| 144 |
-
row = d[d['mes_num']
|
| 145 |
v = float(row[col].iloc[0]) if not row.empty else 0.0
|
| 146 |
-
vals.append(v)
|
| 147 |
-
texts.append(fmt(v) if v > 0 else "")
|
| 148 |
traces.append({
|
| 149 |
"ano": str(ano), "cor": cor,
|
| 150 |
"meses": meses_ex, "y": vals, "text": texts,
|
| 151 |
-
"fill_rgba":
|
| 152 |
})
|
| 153 |
frame["linha1"].append({"tipo": tipos_l1[idx_m], "traces": traces, "meses_ex": meses_ex})
|
| 154 |
-
|
| 155 |
-
# linha2: sistemas
|
| 156 |
-
grupos = [
|
| 157 |
{'cols': req_cols, 'tipo': 'donut'},
|
| 158 |
{'cols': unid_cols, 'tipo': 'linha_sis'},
|
| 159 |
{'cols': itens_cols, 'tipo': 'barv_sis'},
|
| 160 |
-
]
|
| 161 |
-
for grp in grupos:
|
| 162 |
totais = {}
|
| 163 |
for c in grp['cols']:
|
| 164 |
cat = extrair_categoria(c)
|
| 165 |
if cat not in totais:
|
| 166 |
totais[cat] = {'v': 0.0, 'cor': CORES_EXP[len(totais) % len(CORES_EXP)]}
|
| 167 |
totais[cat]['v'] += float(df_det_ate[c].sum()) if c in df_det_ate.columns else 0.0
|
| 168 |
-
dados = sorted([(k,
|
| 169 |
key=lambda x: x[1]['v'], reverse=True)
|
| 170 |
frame["linha2"].append({
|
| 171 |
"tipo": grp['tipo'],
|
|
@@ -173,62 +207,49 @@ def build_frames(df_plot, df_det, req_cols, unid_cols, itens_cols, anos_unicos,
|
|
| 173 |
"valores":[d[1]['v'] for d in dados],
|
| 174 |
"cores": [d[1]['cor'] for d in dados],
|
| 175 |
})
|
| 176 |
-
|
| 177 |
frames.append(frame)
|
| 178 |
return frames
|
| 179 |
|
| 180 |
-
# ──
|
| 181 |
-
with st.spinner('📊 Carregando dados...'):
|
| 182 |
-
df_exp_raw = read_ws("exp_dados") # ← gs_client (era: load_ws("exp_dados"))
|
| 183 |
-
|
| 184 |
result = proc_exp(df_exp_raw) if df_exp_raw is not None and not df_exp_raw.empty else None
|
| 185 |
|
| 186 |
if result is not None:
|
| 187 |
df_expedicao, df_det, req_cols, unid_cols, itens_cols = result
|
| 188 |
-
|
| 189 |
-
for col in ['qtd_requisicoes', 'qtd_unidades_emitidas', 'qtd_itens_total']:
|
| 190 |
df_expedicao[col] = pd.to_numeric(df_expedicao[col], errors='coerce').fillna(0)
|
| 191 |
-
|
| 192 |
mes_max = int(df_expedicao['mes_num'].max())
|
| 193 |
anos_unicos = sorted(df_expedicao['ano'].unique())
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
'#006db3','#007dcc','#1a8dd4','#3399dd','#4da6e6',
|
| 197 |
-
'#66b3ee','#80c0f5','#99ccff','#b3d9ff']
|
| 198 |
-
|
| 199 |
MESES_PT = {1:'Jan',2:'Fev',3:'Mar',4:'Abr',5:'Mai',6:'Jun',
|
| 200 |
7:'Jul',8:'Ago',9:'Set',10:'Out',11:'Nov',12:'Dez'}
|
| 201 |
-
|
| 202 |
metricas_cfg = [
|
| 203 |
-
{'col':'qtd_requisicoes',
|
| 204 |
-
{'col':'qtd_unidades_emitidas',
|
| 205 |
-
{'col':'qtd_itens_total',
|
| 206 |
]
|
| 207 |
|
| 208 |
def sub_anos(cfg):
|
| 209 |
parts = []
|
| 210 |
for i, ano in enumerate(anos_unicos):
|
| 211 |
cor = cfg['cores'][i % len(cfg['cores'])]
|
| 212 |
-
total = fmt(df_expedicao[df_expedicao['ano']
|
| 213 |
-
parts.append(
|
| 214 |
return " <span style='color:#666'>|</span> ".join(parts)
|
| 215 |
|
| 216 |
def sub_sistemas(cols):
|
| 217 |
totais = {}
|
| 218 |
for c in cols:
|
| 219 |
cat = extrair_categoria(c)
|
| 220 |
-
if cat not in totais:
|
| 221 |
-
totais[cat] = {'v': 0.0, 'cor': CORES_EXP[len(totais) % len(CORES_EXP)]}
|
| 222 |
totais[cat]['v'] += float(df_det[c].sum()) if c in df_det.columns else 0.0
|
| 223 |
-
dados = sorted([(k,
|
| 224 |
key=lambda x: x[1]['v'], reverse=True)
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
return " <span style='color:#666'>|</span> ".join(parts)
|
| 229 |
|
| 230 |
-
frames = build_frames(df_expedicao, df_det, req_cols, unid_cols, itens_cols,
|
| 231 |
-
anos_unicos, mes_max, CORES_EXP)
|
| 232 |
|
| 233 |
import numpy as np
|
| 234 |
class NpEncoder(json.JSONEncoder):
|
|
@@ -238,72 +259,77 @@ if result is not None:
|
|
| 238 |
if isinstance(obj, np.ndarray): return obj.tolist()
|
| 239 |
return super().default(obj)
|
| 240 |
|
| 241 |
-
frames_json
|
| 242 |
-
meses_json
|
| 243 |
-
TITULOS_L1
|
| 244 |
-
TITULOS_L2
|
| 245 |
-
subs_l1
|
| 246 |
-
subs_l2
|
| 247 |
|
| 248 |
-
html =
|
|
|
|
|
|
|
| 249 |
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
| 250 |
-
<link
|
| 251 |
-
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 252 |
-
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:opsz,wght@9..40,400;9..40,600;9..40,700;9..40,800&family=Sora:wght@700;800;900&display=swap" rel="stylesheet">
|
| 253 |
<script src="https://cdn.plot.ly/plotly-2.27.0.min.js" charset="utf-8"></script>
|
| 254 |
<style>
|
| 255 |
-
*{
|
| 256 |
-
html,body{
|
| 257 |
background:linear-gradient(135deg,#f5f7fa 0%,#e8eef5 100%);
|
| 258 |
-
font-family:'DM Sans',sans-serif;}
|
| 259 |
-
.root{
|
| 260 |
-
grid-template-rows:auto auto 1fr 1fr;}
|
| 261 |
-
.hdr{
|
| 262 |
-
border-radius:8px;padding:4px 14px;display:flex;align-items:center;
|
| 263 |
-
box-shadow:0 4px 12px rgba(0,58,112,.12);}
|
| 264 |
-
.logo{
|
| 265 |
-
.ht{
|
| 266 |
-
|
| 267 |
-
.
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
.ml{
|
| 274 |
-
font-weight:700;text-transform:uppercase;
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
.
|
|
|
|
| 279 |
box-shadow:0 2px 12px rgba(0,58,112,.07),0 1px 3px rgba(0,58,112,.04);
|
| 280 |
-
overflow:visible;min-height:0;display:flex;flex-direction:column;
|
| 281 |
-
border:1px solid rgba(0,58,112,.04);}
|
| 282 |
-
.ct{
|
| 283 |
-
padding:4px 8px 3px;border-radius:10px 10px 0 0;
|
| 284 |
-
border-bottom:1px solid #eef1f6;flex-shrink:0;}
|
| 285 |
-
.ct-title{
|
| 286 |
-
color:#1a1a1a;font-weight:800;text-align:center;}
|
| 287 |
-
.ct-sub{
|
| 288 |
-
margin-top:1px;line-height:1.3;font-family:'DM Sans',sans-serif;}
|
| 289 |
-
.cp{
|
| 290 |
-
@media(max-width:900px){
|
| 291 |
-
html,body{
|
| 292 |
-
.root{
|
| 293 |
-
.cards{
|
| 294 |
-
.row3{
|
| 295 |
-
.cb{
|
| 296 |
-
.cp{
|
| 297 |
-
}
|
| 298 |
-
@media(max-width:600px){
|
| 299 |
-
|
| 300 |
-
.
|
| 301 |
-
.
|
| 302 |
-
.
|
| 303 |
-
.
|
| 304 |
-
}
|
|
|
|
|
|
|
|
|
|
| 305 |
</style>
|
| 306 |
-
</head>
|
|
|
|
| 307 |
<div class="root">
|
| 308 |
<div class="hdr">
|
| 309 |
<img class="logo" src="https://companieslogo.com/img/orig/AZ2.F-d26946db.png?t=1720244490" alt="AZ">
|
|
@@ -317,389 +343,448 @@ html,body{{width:100%;height:100%;overflow:hidden;
|
|
| 317 |
<div class="row3" id="row1"></div>
|
| 318 |
<div class="row3" id="row2"></div>
|
| 319 |
</div>
|
| 320 |
-
|
| 321 |
<script>
|
| 322 |
-
var FRAMES =
|
| 323 |
-
var MESES =
|
| 324 |
-
var TITULOS_L1 =
|
| 325 |
-
var TITULOS_L2 =
|
| 326 |
-
var SUBS_L1 =
|
| 327 |
-
var SUBS_L2 =
|
| 328 |
|
| 329 |
var CARDS_CFG = [
|
| 330 |
-
{
|
| 331 |
-
{
|
| 332 |
-
{
|
| 333 |
];
|
| 334 |
|
| 335 |
-
var FRAME_MS
|
| 336 |
-
var PAUSE_MS
|
| 337 |
-
var frameIdx
|
| 338 |
-
var
|
|
|
|
|
|
|
| 339 |
|
| 340 |
-
function buildDOM() {
|
| 341 |
var cr = document.getElementById('cards-row');
|
| 342 |
-
CARDS_CFG.forEach(function(c) {
|
| 343 |
cr.innerHTML += '<div class="mc" style="border-left-color:'+c.cor+'">'
|
| 344 |
-
+'<div class="ml">'+c.titulo+'</div>'
|
| 345 |
-
+'<div class="mv" id="mv_'+c.col+'" style="color:'+c.cor+'">0</div></div>';
|
| 346 |
-
}
|
| 347 |
var r1 = document.getElementById('row1');
|
| 348 |
-
for(var i=0;i<3;i++) {
|
| 349 |
r1.innerHTML += '<div class="cb"><div class="ct">'
|
| 350 |
-
+'<div class="ct-title">'+TITULOS_L1[i]+'</div>'
|
| 351 |
-
+'<div class="ct-sub">'+SUBS_L1[i]+'</div></div>'
|
| 352 |
-
+'<div class="cp" id="l1_'+i+'"></div></div>';
|
| 353 |
-
}
|
| 354 |
var r2 = document.getElementById('row2');
|
| 355 |
-
for(var i=0;i<3;i++) {
|
| 356 |
r2.innerHTML += '<div class="cb"><div class="ct">'
|
| 357 |
-
+'<div class="ct-title">'+TITULOS_L2[i]+'</div>'
|
| 358 |
-
+'<div class="ct-sub">'+SUBS_L2[i]+'</div></div>'
|
| 359 |
-
+'<div class="cp" id="l2_'+i+'"></div></div>';
|
| 360 |
-
}
|
| 361 |
-
}
|
| 362 |
|
| 363 |
-
function gsize(id) {
|
| 364 |
var el = document.getElementById(id);
|
| 365 |
-
if(!el) return [200,150];
|
| 366 |
return [el.clientWidth||200, el.clientHeight||150];
|
| 367 |
-
}
|
|
|
|
| 368 |
var tickvals = Object.keys(MESES).map(Number);
|
| 369 |
-
var ticktext
|
| 370 |
-
|
| 371 |
-
function fmtDot(v) {
|
| 372 |
-
var n=Math.round(v);
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
}});
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 403 |
y: yOrder[i],
|
| 404 |
-
xref:'x',yref:'y',showarrow:false,
|
| 405 |
xanchor: inside?'center':'left',
|
| 406 |
yanchor:'middle',
|
| 407 |
text:'<b>'+fmtDot(v)+'</b>',
|
| 408 |
-
font:{
|
| 409 |
-
}
|
| 410 |
-
accum[i]+=v;
|
| 411 |
-
}
|
| 412 |
-
}
|
| 413 |
-
var layout={
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
|
| 420 |
-
showgrid:
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
}
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
|
| 441 |
-
|
| 442 |
-
|
| 443 |
-
|
| 444 |
-
|
| 445 |
-
|
| 446 |
-
|
| 447 |
-
|
| 448 |
-
}});
|
| 449 |
-
}},80);
|
| 450 |
-
}}
|
| 451 |
-
|
| 452 |
-
function renderBarV(item, elId) {{
|
| 453 |
-
var wh=gsize(elId), el=document.getElementById(elId);
|
| 454 |
-
var meses=item.meses_ex||[], data=[];
|
| 455 |
-
var maxByMes={{}};
|
| 456 |
-
meses.forEach(function(m,mi){{var s=0;item.traces.forEach(function(t){{s+=t.y[mi]||0;}});maxByMes[mi]=s;}});
|
| 457 |
-
var globalMax=Math.max.apply(null,meses.map(function(_,mi){{return maxByMes[mi];}}));
|
| 458 |
-
var annotations=[];
|
| 459 |
-
meses.forEach(function(m,mi) {{
|
| 460 |
-
var tot=maxByMes[mi];
|
| 461 |
-
if(!tot) return;
|
| 462 |
-
annotations.push({{x:MESES[m],y:tot,xref:'x',yref:'y',showarrow:false,
|
| 463 |
yanchor:'bottom',xanchor:'center',yshift:5,
|
| 464 |
-
text:'<b>'+fmtDot(tot)+'</b>',font:{
|
| 465 |
-
}
|
| 466 |
-
var accumV=
|
| 467 |
-
|
| 468 |
-
|
| 469 |
-
|
| 470 |
-
|
| 471 |
-
|
| 472 |
-
|
| 473 |
-
|
| 474 |
-
if(v>=minH) {{
|
| 475 |
-
segAnnots.push({{x:MESES[meses[mi]],y:yBase+v/2,xref:'x',yref:'y',showarrow:false,
|
| 476 |
xanchor:'center',yanchor:'middle',
|
| 477 |
-
text:'<b>'+fmtDot(v)+'</b>',font:{
|
| 478 |
-
}
|
| 479 |
-
accumV[mi]+=v;
|
| 480 |
-
}
|
| 481 |
-
}
|
| 482 |
-
segAnnots.forEach(function(a){
|
| 483 |
-
data.push({
|
| 484 |
-
x:meses.map(function(m){
|
| 485 |
-
text:t.y.map(function(){
|
| 486 |
-
marker:{
|
| 487 |
-
hovertemplate:'<b>'+t.ano+' - %{
|
| 488 |
-
}
|
| 489 |
-
var layout={
|
| 490 |
-
|
| 491 |
-
|
| 492 |
-
|
| 493 |
-
|
| 494 |
-
|
| 495 |
-
|
| 496 |
-
|
| 497 |
-
|
| 498 |
-
|
| 499 |
-
|
| 500 |
-
|
| 501 |
-
|
| 502 |
-
|
| 503 |
-
|
| 504 |
-
|
| 505 |
-
|
| 506 |
-
|
| 507 |
-
|
| 508 |
-
|
| 509 |
-
|
| 510 |
-
|
| 511 |
-
|
| 512 |
-
|
| 513 |
-
|
| 514 |
-
|
| 515 |
-
|
| 516 |
-
|
| 517 |
-
|
| 518 |
-
|
| 519 |
-
|
| 520 |
-
|
| 521 |
-
|
| 522 |
-
|
| 523 |
-
|
| 524 |
-
|
| 525 |
-
|
| 526 |
-
|
| 527 |
-
function renderLine(item, elId) {{
|
| 528 |
-
var wh=gsize(elId), el=document.getElementById(elId);
|
| 529 |
-
var data=[], annotations=[], yshifts=[18,32,46,60];
|
| 530 |
-
item.traces.forEach(function(t,i) {{
|
| 531 |
-
data.push({{type:'scatter',mode:'lines+markers',x:t.meses,y:t.y,
|
| 532 |
-
line:{{width:3,color:t.cor,shape:'spline'}},
|
| 533 |
-
marker:{{size:8,color:t.cor,line:{{width:2,color:'white'}}}},
|
| 534 |
-
fill:i>0?'tonexty':null,fillcolor:t.fill_rgba,
|
| 535 |
-
cliponaxis:false,showlegend:false,name:t.ano,
|
| 536 |
-
hovertemplate:'<b>'+t.ano+' - %{{customdata}}</b><br>%{{y:,.0f}}<extra></extra>',
|
| 537 |
-
customdata:t.meses.map(function(m){{return MESES[m];}})
|
| 538 |
-
}});
|
| 539 |
-
var shift=yshifts[i%yshifts.length];
|
| 540 |
-
t.meses.forEach(function(mx,j) {{
|
| 541 |
-
if(!t.text[j]) return;
|
| 542 |
-
annotations.push({{x:mx,y:t.y[j],xref:'x',yref:'y',text:t.text[j],
|
| 543 |
showarrow:false,yshift:shift,
|
| 544 |
-
font:{
|
| 545 |
-
}
|
| 546 |
-
}
|
| 547 |
-
var layout={
|
| 548 |
-
|
| 549 |
-
|
| 550 |
-
|
| 551 |
-
|
| 552 |
-
|
| 553 |
-
|
| 554 |
-
|
| 555 |
-
|
| 556 |
-
|
| 557 |
-
|
| 558 |
-
}
|
| 559 |
-
|
| 560 |
-
|
| 561 |
-
|
| 562 |
-
|
| 563 |
-
|
| 564 |
-
|
| 565 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 566 |
return l+': '+fmtDot(item.valores[i])+' ('+pct+'%)';
|
| 567 |
-
}
|
| 568 |
-
var textF=item.valores.map(function(v){
|
| 569 |
-
var data=[{
|
| 570 |
-
|
| 571 |
-
|
| 572 |
-
|
| 573 |
-
|
| 574 |
-
|
| 575 |
-
|
| 576 |
-
|
| 577 |
-
|
| 578 |
-
|
| 579 |
-
|
| 580 |
-
|
| 581 |
-
|
| 582 |
-
}
|
| 583 |
-
|
| 584 |
-
|
| 585 |
-
if(!
|
| 586 |
-
|
| 587 |
-
|
| 588 |
-
|
| 589 |
-
|
| 590 |
-
|
| 591 |
-
|
| 592 |
-
|
| 593 |
-
var
|
| 594 |
-
var
|
| 595 |
-
var
|
| 596 |
-
var
|
| 597 |
-
|
| 598 |
-
|
| 599 |
-
|
| 600 |
-
|
| 601 |
-
|
| 602 |
-
var
|
| 603 |
-
|
| 604 |
-
|
| 605 |
-
|
| 606 |
-
|
| 607 |
-
|
| 608 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 609 |
zeroline:false,showline:false,showticklabels:false,ticks:'',
|
| 610 |
-
range:[0,Math.max.apply(null,item.valores)*1.6]}
|
| 611 |
-
hoverlabel:{
|
| 612 |
-
|
| 613 |
-
|
| 614 |
-
}
|
| 615 |
-
|
| 616 |
-
|
| 617 |
-
|
| 618 |
-
|
| 619 |
-
|
| 620 |
-
|
| 621 |
-
var
|
| 622 |
-
|
| 623 |
-
|
| 624 |
-
|
| 625 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 626 |
text:'<b>'+fmtDot(v)+'</b><br><span style="font-size:10px">'+pct+'%</span>',
|
| 627 |
-
font:{
|
| 628 |
-
}
|
| 629 |
-
var data=[{
|
| 630 |
-
|
| 631 |
-
|
| 632 |
-
|
| 633 |
-
|
| 634 |
-
|
| 635 |
-
|
| 636 |
-
|
| 637 |
-
|
| 638 |
-
|
| 639 |
-
|
| 640 |
-
|
| 641 |
-
|
| 642 |
-
|
| 643 |
-
|
| 644 |
-
|
| 645 |
-
}
|
| 646 |
-
|
| 647 |
-
|
| 648 |
-
|
| 649 |
-
|
| 650 |
-
|
| 651 |
-
|
| 652 |
-
|
| 653 |
-
|
| 654 |
-
|
| 655 |
-
|
| 656 |
-
|
| 657 |
-
|
| 658 |
-
|
| 659 |
-
|
| 660 |
-
|
| 661 |
-
|
| 662 |
-
}
|
| 663 |
-
|
| 664 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 665 |
renderFrame(FRAMES[frameIdx]);
|
| 666 |
frameIdx++;
|
| 667 |
-
if(frameIdx>=FRAMES.length) {
|
| 668 |
clearTimeout(animTimer);
|
| 669 |
-
cdownTimer=setTimeout(function(){
|
| 670 |
-
}
|
| 671 |
-
animTimer=setTimeout(stepFrame,FRAME_MS);
|
| 672 |
-
}
|
| 673 |
-
}
|
| 674 |
|
| 675 |
-
function startAnimation() {
|
| 676 |
-
clearTimeout(animTimer);
|
| 677 |
-
|
| 678 |
-
|
|
|
|
| 679 |
|
| 680 |
var _rt;
|
| 681 |
-
window.addEventListener('resize',function(){
|
| 682 |
clearTimeout(_rt);
|
| 683 |
-
_rt=setTimeout(function(){
|
| 684 |
-
document.querySelectorAll('.cp').forEach(function(box){
|
| 685 |
-
if(box.data) Plotly.relayout(box,{
|
| 686 |
-
}
|
| 687 |
-
}
|
| 688 |
-
}
|
| 689 |
-
|
| 690 |
-
window.addEventListener('load',function(){
|
| 691 |
buildDOM();
|
| 692 |
-
setTimeout(function(){
|
| 693 |
-
}
|
| 694 |
</script>
|
| 695 |
-
</body>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 696 |
|
| 697 |
-
components.html(html, height=8000, scrolling=
|
| 698 |
|
| 699 |
else:
|
| 700 |
-
components.html("""<!DOCTYPE html><html><body style=
|
| 701 |
-
justify-content:center;height:100dvh;font-family:sans-serif;color:#003a70;background:#f5f7fa;
|
| 702 |
-
<div style=
|
| 703 |
<p>Aguardando dados... Recarregue a página.</p></div></body></html>""",
|
| 704 |
height=4000, scrolling=False)
|
| 705 |
|
|
|
|
| 8 |
|
| 9 |
st.set_page_config(layout="wide", initial_sidebar_state="collapsed", page_title="PMJA - Dashboard Expedição")
|
| 10 |
|
| 11 |
+
st.markdown("""<style>
|
| 12 |
+
#MainMenu,footer,header,[data-testid="stToolbar"],[data-testid="stDecoration"],
|
| 13 |
+
[data-testid="stStatusWidget"]{display:none!important}
|
| 14 |
+
.block-container,.element-container,.stMarkdown{padding:0!important;margin:0!important;max-width:100%!important}
|
| 15 |
+
iframe{position:fixed!important;top:0!important;left:0!important;
|
| 16 |
+
width:100vw!important;height:100dvh!important;border:none!important;z-index:9999!important}
|
| 17 |
+
[data-testid="stHorizontalBlock"]{gap:0!important;margin:0!important;padding:0!important;
|
| 18 |
+
height:0!important;overflow:visible!important}
|
| 19 |
+
.main,[data-testid="stAppViewContainer"]{background:#f5f7fa!important}
|
| 20 |
+
@media(max-width:900px){
|
| 21 |
+
iframe{position:static!important;width:100%!important;height:7500px!important;z-index:1!important}
|
| 22 |
+
.main,[data-testid="stAppViewContainer"]{overflow-y:auto!important;height:auto!important}
|
| 23 |
+
.block-container{height:auto!important}
|
| 24 |
+
}
|
| 25 |
+
</style>""", unsafe_allow_html=True)
|
| 26 |
+
|
| 27 |
# ── SESSION ───────────────────────────────────────────────────────────
|
| 28 |
_cc = CookieController()
|
| 29 |
COOKIE_NAME = "pmja_session"
|
|
|
|
| 54 |
st.session_state.inicio_exibicao = time.time()
|
| 55 |
st.switch_page("pages/full.py")
|
| 56 |
|
| 57 |
+
# ── LOADING ───────────────────────────────────────────────────────────
|
| 58 |
+
loading_placeholder = st.empty()
|
| 59 |
+
loading_placeholder.markdown("""
|
| 60 |
+
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;700;800&family=Sora:wght@800&display=swap" rel="stylesheet">
|
| 61 |
+
<style>
|
| 62 |
+
#ld{position:fixed;inset:0;z-index:999999;background:linear-gradient(135deg,#003a70,#0056A3);
|
| 63 |
+
display:flex;flex-direction:column;align-items:center;justify-content:center;font-family:'DM Sans',sans-serif}
|
| 64 |
+
#ld .lg{display:flex;align-items:center;gap:24px;margin-bottom:36px}
|
| 65 |
+
#ld img{height:34px;filter:brightness(0) invert(1);opacity:.9}
|
| 66 |
+
#ld .dv{width:1px;height:32px;background:rgba(255,255,255,.25)}
|
| 67 |
+
#ld h2{font-family:'Sora',sans-serif;font-size:clamp(15px,4vw,21px);color:#fff;font-weight:800;margin-bottom:4px;text-align:center}
|
| 68 |
+
#ld p{font-size:clamp(11px,3vw,13px);color:rgba(255,255,255,.7);font-weight:600;margin-bottom:40px;text-align:center}
|
| 69 |
+
#ld .sp{width:32px;height:32px;border:3px solid rgba(255,255,255,.2);border-top-color:#fff;
|
| 70 |
+
border-radius:50%;animation:sp .8s linear infinite;margin-bottom:24px}
|
| 71 |
+
@keyframes sp{to{transform:rotate(360deg)}}
|
| 72 |
+
#ld .bw{width:min(260px,70vw);height:4px;background:rgba(255,255,255,.15);border-radius:99px;overflow:hidden;margin-bottom:14px}
|
| 73 |
+
#ld .bf{height:100%;width:0%;background:#fff;border-radius:99px;transition:width .4s ease}
|
| 74 |
+
#ld .st{font-size:11px;color:rgba(255,255,255,.6);font-weight:600;text-align:center}
|
| 75 |
+
#ld .dt span{animation:dt 1.2s infinite;opacity:0}
|
| 76 |
+
#ld .dt span:nth-child(2){animation-delay:.2s}
|
| 77 |
+
#ld .dt span:nth-child(3){animation-delay:.4s}
|
| 78 |
+
@keyframes dt{0%,100%{opacity:0}50%{opacity:1}}
|
| 79 |
+
</style>
|
| 80 |
+
<div id="ld">
|
| 81 |
+
<div class="lg">
|
| 82 |
+
<img src="https://companieslogo.com/img/orig/AZ2.F-d26946db.png?t=1720244490">
|
| 83 |
+
<div class="dv"></div>
|
| 84 |
+
<img src="https://upload.wikimedia.org/wikipedia/commons/8/8f/Logo-engie.svg">
|
| 85 |
+
</div>
|
| 86 |
+
<div class="sp"></div>
|
| 87 |
+
<h2>PMJA — Dashboard Expedição</h2>
|
| 88 |
+
<p>Gestão de Materiais</p>
|
| 89 |
+
<div class="bw"><div class="bf" id="ldbf"></div></div>
|
| 90 |
+
<div class="st" id="ldst">Conectando ao Google Sheets<span class="dt"><span>.</span><span>.</span><span>.</span></span></div>
|
| 91 |
+
</div>
|
| 92 |
+
<script>
|
| 93 |
+
(function(){
|
| 94 |
+
var s=[{p:18,m:'Conectando ao Google Sheets'},{p:40,m:'Carregando dados de expedição'},
|
| 95 |
+
{p:62,m:'Processando métricas'},{p:80,m:'Montando gráficos'},{p:95,m:'Quase pronto'}];
|
| 96 |
+
var i=0;
|
| 97 |
+
function n(){
|
| 98 |
+
if(i>=s.length)return;
|
| 99 |
+
var x=s[i++];
|
| 100 |
+
var b=document.getElementById('ldbf'),t=document.getElementById('ldst');
|
| 101 |
+
if(b)b.style.width=x.p+'%';
|
| 102 |
+
if(t)t.innerHTML=x.m+'<span class="dt"><span>.</span><span>.</span><span>.</span></span>';
|
| 103 |
+
setTimeout(n,950+Math.random()*550);
|
| 104 |
+
}
|
| 105 |
+
setTimeout(n,300);
|
| 106 |
+
})();
|
| 107 |
+
</script>
|
| 108 |
+
""", unsafe_allow_html=True)
|
| 109 |
+
|
| 110 |
+
# ── LOAD DATA ─────────────────────────────────────────────────────────
|
| 111 |
+
df_exp_raw = read_ws("exp_dados")
|
| 112 |
+
loading_placeholder.empty()
|
| 113 |
|
| 114 |
# ── HELPERS ───────────────────────────────────────────────────────────
|
| 115 |
def fmt(n):
|
| 116 |
if pd.isna(n): return "0"
|
| 117 |
+
return "{:,}".format(int(n)).replace(',', '.')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 118 |
|
| 119 |
def extrair_categoria(col):
|
| 120 |
+
for s in [' Requisições',' Requisiçoes',' Unid. Itens',' Unid Itens',
|
| 121 |
+
' Itens por unidade',' Items por unidade']:
|
| 122 |
col = col.replace(s, '')
|
| 123 |
return col.strip()
|
| 124 |
|
|
|
|
| 131 |
df = df.dropna(subset=['data']).copy()
|
| 132 |
df['ano'] = df['data'].dt.year
|
| 133 |
df['mes_num'] = df['data'].dt.month
|
|
|
|
| 134 |
req, unid, itens = [], [], []
|
| 135 |
for col in df.columns:
|
| 136 |
+
c = col.lower().strip().replace('.','').replace(' ',' ')
|
| 137 |
if 'requisi' in c and 'unid' not in c and 'por' not in c: req.append(col)
|
| 138 |
if 'unid' in c and 'iten' in c and 'por' not in c: unid.append(col)
|
| 139 |
if 'iten' in c and 'por' in c and 'unid' in c: itens.append(col)
|
| 140 |
+
for col in req+unid+itens:
|
| 141 |
+
if col in df.columns: df[col] = pd.to_numeric(df[col], errors='coerce').fillna(0)
|
|
|
|
|
|
|
|
|
|
| 142 |
df['qtd_requisicoes'] = df[req].sum(axis=1)
|
| 143 |
df['qtd_unidades_emitidas'] = df[unid].sum(axis=1)
|
| 144 |
df['qtd_itens_total'] = df[itens].sum(axis=1) if itens else 0
|
| 145 |
+
df_final = df[['mes_num','ano','qtd_requisicoes','qtd_unidades_emitidas','qtd_itens_total']]\
|
| 146 |
+
.sort_values(['ano','mes_num']).reset_index(drop=True)
|
| 147 |
+
df_det = df[['mes_num','ano']+req+unid+itens]\
|
| 148 |
+
.sort_values(['ano','mes_num']).reset_index(drop=True)
|
| 149 |
return df_final, df_det, req, unid, itens
|
| 150 |
|
| 151 |
# ── BUILD FRAMES ──────────────────────────────────────────────────────
|
| 152 |
def build_frames(df_plot, df_det, req_cols, unid_cols, itens_cols, anos_unicos, mes_max, CORES_EXP):
|
| 153 |
MESES_PT = {1:'Jan',2:'Fev',3:'Mar',4:'Abr',5:'Mai',6:'Jun',
|
| 154 |
7:'Jul',8:'Ago',9:'Set',10:'Out',11:'Nov',12:'Dez'}
|
|
|
|
| 155 |
metricas_linha = [
|
| 156 |
+
{'col':'qtd_requisicoes', 'cores':['#001f3f','#0056A3','#00a8e8','#7ecfef']},
|
| 157 |
+
{'col':'qtd_unidades_emitidas','cores':['#001f3f','#0056A3','#00a8e8','#7ecfef']},
|
| 158 |
+
{'col':'qtd_itens_total', 'cores':['#001f3f','#0056A3','#00a8e8','#7ecfef']},
|
| 159 |
]
|
| 160 |
+
tipos_l1 = ['barra_h','barra_v','linha']
|
|
|
|
| 161 |
frames = []
|
| 162 |
+
for mes_atual in range(1, mes_max+1):
|
| 163 |
frame = {"mes": mes_atual, "cards": {}, "linha1": [], "linha2": []}
|
|
|
|
| 164 |
df_ate = df_plot[df_plot['mes_num'] <= mes_atual]
|
| 165 |
df_det_ate = df_det[df_det['mes_num'] <= mes_atual]
|
|
|
|
|
|
|
| 166 |
for col, tit, cor, suf in [
|
| 167 |
+
('qtd_requisicoes', 'Requisições', '#003a70',''),
|
| 168 |
+
('qtd_unidades_emitidas','Unidades Emitidas','#005a9c',''),
|
| 169 |
+
('qtd_itens_total', 'Total de Itens', '#0075be',''),
|
| 170 |
]:
|
| 171 |
frame["cards"][col] = {"valor": fmt(df_ate[col].sum()), "cor": cor, "titulo": tit, "sufixo": suf}
|
|
|
|
|
|
|
| 172 |
meses_ex = sorted(df_plot[df_plot['mes_num'] <= mes_atual]['mes_num'].unique())
|
| 173 |
for idx_m, cfg in enumerate(metricas_linha):
|
| 174 |
col = cfg['col']
|
| 175 |
traces = []
|
| 176 |
for idx_ano, ano in enumerate(anos_unicos):
|
| 177 |
+
d = df_plot[(df_plot['ano']==ano)&(df_plot['mes_num']<=mes_atual)].sort_values('mes_num')
|
| 178 |
if d.empty: continue
|
| 179 |
cor = cfg['cores'][idx_ano % len(cfg['cores'])]
|
| 180 |
vals, texts = [], []
|
| 181 |
for m in meses_ex:
|
| 182 |
+
row = d[d['mes_num']==m]
|
| 183 |
v = float(row[col].iloc[0]) if not row.empty else 0.0
|
| 184 |
+
vals.append(v); texts.append(fmt(v) if v > 0 else "")
|
|
|
|
| 185 |
traces.append({
|
| 186 |
"ano": str(ano), "cor": cor,
|
| 187 |
"meses": meses_ex, "y": vals, "text": texts,
|
| 188 |
+
"fill_rgba": "rgba({},{},{},0.1)".format(int(cor[1:3],16),int(cor[3:5],16),int(cor[5:7],16))
|
| 189 |
})
|
| 190 |
frame["linha1"].append({"tipo": tipos_l1[idx_m], "traces": traces, "meses_ex": meses_ex})
|
| 191 |
+
for grp in [
|
|
|
|
|
|
|
| 192 |
{'cols': req_cols, 'tipo': 'donut'},
|
| 193 |
{'cols': unid_cols, 'tipo': 'linha_sis'},
|
| 194 |
{'cols': itens_cols, 'tipo': 'barv_sis'},
|
| 195 |
+
]:
|
|
|
|
| 196 |
totais = {}
|
| 197 |
for c in grp['cols']:
|
| 198 |
cat = extrair_categoria(c)
|
| 199 |
if cat not in totais:
|
| 200 |
totais[cat] = {'v': 0.0, 'cor': CORES_EXP[len(totais) % len(CORES_EXP)]}
|
| 201 |
totais[cat]['v'] += float(df_det_ate[c].sum()) if c in df_det_ate.columns else 0.0
|
| 202 |
+
dados = sorted([(k,v) for k,v in totais.items() if v['v']>0],
|
| 203 |
key=lambda x: x[1]['v'], reverse=True)
|
| 204 |
frame["linha2"].append({
|
| 205 |
"tipo": grp['tipo'],
|
|
|
|
| 207 |
"valores":[d[1]['v'] for d in dados],
|
| 208 |
"cores": [d[1]['cor'] for d in dados],
|
| 209 |
})
|
|
|
|
| 210 |
frames.append(frame)
|
| 211 |
return frames
|
| 212 |
|
| 213 |
+
# ── MAIN ──────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
| 214 |
result = proc_exp(df_exp_raw) if df_exp_raw is not None and not df_exp_raw.empty else None
|
| 215 |
|
| 216 |
if result is not None:
|
| 217 |
df_expedicao, df_det, req_cols, unid_cols, itens_cols = result
|
| 218 |
+
for col in ['qtd_requisicoes','qtd_unidades_emitidas','qtd_itens_total']:
|
|
|
|
| 219 |
df_expedicao[col] = pd.to_numeric(df_expedicao[col], errors='coerce').fillna(0)
|
|
|
|
| 220 |
mes_max = int(df_expedicao['mes_num'].max())
|
| 221 |
anos_unicos = sorted(df_expedicao['ano'].unique())
|
| 222 |
+
CORES_EXP = ['#001a33','#002d4d','#003d66','#004d80','#005d99','#006db3',
|
| 223 |
+
'#007dcc','#1a8dd4','#3399dd','#4da6e6','#66b3ee','#80c0f5','#99ccff','#b3d9ff']
|
|
|
|
|
|
|
|
|
|
| 224 |
MESES_PT = {1:'Jan',2:'Fev',3:'Mar',4:'Abr',5:'Mai',6:'Jun',
|
| 225 |
7:'Jul',8:'Ago',9:'Set',10:'Out',11:'Nov',12:'Dez'}
|
|
|
|
| 226 |
metricas_cfg = [
|
| 227 |
+
{'col':'qtd_requisicoes', 'cores':['#001f3f','#0056A3','#00a8e8','#7ecfef'],'suf':''},
|
| 228 |
+
{'col':'qtd_unidades_emitidas','cores':['#001f3f','#0056A3','#00a8e8','#7ecfef'],'suf':''},
|
| 229 |
+
{'col':'qtd_itens_total', 'cores':['#001f3f','#0056A3','#00a8e8','#7ecfef'],'suf':''},
|
| 230 |
]
|
| 231 |
|
| 232 |
def sub_anos(cfg):
|
| 233 |
parts = []
|
| 234 |
for i, ano in enumerate(anos_unicos):
|
| 235 |
cor = cfg['cores'][i % len(cfg['cores'])]
|
| 236 |
+
total = fmt(df_expedicao[df_expedicao['ano']==ano][cfg['col']].sum())
|
| 237 |
+
parts.append("<span style='color:{};font-weight:700'>{}: {}{}</span>".format(cor, ano, total, cfg['suf']))
|
| 238 |
return " <span style='color:#666'>|</span> ".join(parts)
|
| 239 |
|
| 240 |
def sub_sistemas(cols):
|
| 241 |
totais = {}
|
| 242 |
for c in cols:
|
| 243 |
cat = extrair_categoria(c)
|
| 244 |
+
if cat not in totais: totais[cat] = {'v':0.0,'cor':CORES_EXP[len(totais)%len(CORES_EXP)]}
|
|
|
|
| 245 |
totais[cat]['v'] += float(df_det[c].sum()) if c in df_det.columns else 0.0
|
| 246 |
+
dados = sorted([(k,v) for k,v in totais.items() if v['v']>0],
|
| 247 |
key=lambda x: x[1]['v'], reverse=True)
|
| 248 |
+
return " <span style='color:#666'>|</span> ".join(
|
| 249 |
+
["<span style='color:{};font-weight:700'>{}: {}</span>".format(d[1]['cor'], d[0], fmt(d[1]['v']))
|
| 250 |
+
for d in dados])
|
|
|
|
| 251 |
|
| 252 |
+
frames = build_frames(df_expedicao, df_det, req_cols, unid_cols, itens_cols, anos_unicos, mes_max, CORES_EXP)
|
|
|
|
| 253 |
|
| 254 |
import numpy as np
|
| 255 |
class NpEncoder(json.JSONEncoder):
|
|
|
|
| 259 |
if isinstance(obj, np.ndarray): return obj.tolist()
|
| 260 |
return super().default(obj)
|
| 261 |
|
| 262 |
+
frames_json = json.dumps(frames, cls=NpEncoder, ensure_ascii=False)
|
| 263 |
+
meses_json = json.dumps(MESES_PT)
|
| 264 |
+
TITULOS_L1 = ['Requisições','Unidades Emitidas','Total de Itens']
|
| 265 |
+
TITULOS_L2 = ['Requisições por Sistema','Unidades Emitidas por Sistema','Total de Itens por Sistema']
|
| 266 |
+
subs_l1 = [sub_anos(c) for c in metricas_cfg]
|
| 267 |
+
subs_l2 = [sub_sistemas(req_cols), sub_sistemas(unid_cols), sub_sistemas(itens_cols)]
|
| 268 |
|
| 269 |
+
html = """<!DOCTYPE html>
|
| 270 |
+
<html>
|
| 271 |
+
<head>
|
| 272 |
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
| 273 |
+
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;600;700;800&family=Sora:wght@700;800;900&display=swap" rel="stylesheet">
|
|
|
|
|
|
|
| 274 |
<script src="https://cdn.plot.ly/plotly-2.27.0.min.js" charset="utf-8"></script>
|
| 275 |
<style>
|
| 276 |
+
* { margin:0; padding:0; box-sizing:border-box; }
|
| 277 |
+
html, body { width:100%; height:100%; overflow:hidden;
|
| 278 |
background:linear-gradient(135deg,#f5f7fa 0%,#e8eef5 100%);
|
| 279 |
+
font-family:'DM Sans',sans-serif; }
|
| 280 |
+
.root { display:grid; width:100%; height:100dvh; gap:3px; padding:4px; overflow:hidden;
|
| 281 |
+
grid-template-rows:auto auto 1fr 1fr; }
|
| 282 |
+
.hdr { background:linear-gradient(135deg,#003a70 0%,#0056A3 100%);
|
| 283 |
+
border-radius:8px; padding:4px 14px; display:flex; align-items:center;
|
| 284 |
+
justify-content:space-between; box-shadow:0 4px 12px rgba(0,58,112,.12); }
|
| 285 |
+
.logo { height:clamp(22px,3.2dvh,36px); filter:brightness(0) invert(1); opacity:.92; }
|
| 286 |
+
.ht { font-family:'Sora',sans-serif; font-size:clamp(12px,1.8dvh,20px);
|
| 287 |
+
color:#fff; font-weight:800; letter-spacing:-.5px; }
|
| 288 |
+
.hs { font-family:'DM Sans',sans-serif; font-size:clamp(9px,1.2dvh,14px);
|
| 289 |
+
color:rgba(255,255,255,.9); font-weight:700; }
|
| 290 |
+
.cards { display:grid; grid-template-columns:repeat(3,1fr); gap:3px; }
|
| 291 |
+
.mc { background:linear-gradient(135deg,#ffffff 0%,#f8fafd 100%);
|
| 292 |
+
border-radius:8px; padding:clamp(4px,.6dvh,10px) clamp(6px,.7vw,14px);
|
| 293 |
+
border-left:4px solid; box-shadow:0 2px 12px rgba(0,58,112,.08); overflow:hidden; }
|
| 294 |
+
.ml { font-family:'DM Sans',sans-serif; font-size:clamp(7px,.85dvh,11px);
|
| 295 |
+
color:#666; font-weight:700; text-transform:uppercase;
|
| 296 |
+
letter-spacing:.3px; margin-bottom:2px; white-space:nowrap; }
|
| 297 |
+
.mv { font-family:'Sora',sans-serif; font-size:clamp(13px,2dvh,26px);
|
| 298 |
+
font-weight:800; line-height:1; white-space:nowrap; }
|
| 299 |
+
.row3 { display:grid; grid-template-columns:repeat(3,1fr); gap:3px; min-height:0; }
|
| 300 |
+
.cb { background:white; border-radius:10px;
|
| 301 |
box-shadow:0 2px 12px rgba(0,58,112,.07),0 1px 3px rgba(0,58,112,.04);
|
| 302 |
+
overflow:visible; min-height:0; display:flex; flex-direction:column;
|
| 303 |
+
border:1px solid rgba(0,58,112,.04); }
|
| 304 |
+
.ct { background:linear-gradient(180deg,#fafbfd 0%,#ffffff 100%);
|
| 305 |
+
padding:4px 8px 3px; border-radius:10px 10px 0 0;
|
| 306 |
+
border-bottom:1px solid #eef1f6; flex-shrink:0; }
|
| 307 |
+
.ct-title { font-family:'Sora',sans-serif; font-size:clamp(9px,1.15dvh,15px);
|
| 308 |
+
color:#1a1a1a; font-weight:800; text-align:center; }
|
| 309 |
+
.ct-sub { text-align:center; font-size:clamp(7px,.8dvh,10px);
|
| 310 |
+
font-weight:700; margin-top:1px; line-height:1.3; font-family:'DM Sans',sans-serif; }
|
| 311 |
+
.cp { flex:1; min-height:0; overflow:visible; }
|
| 312 |
+
@media(max-width:900px){
|
| 313 |
+
html,body { overflow:hidden!important; }
|
| 314 |
+
.root { height:100dvh!important; overflow:hidden!important; }
|
| 315 |
+
.cards { grid-template-columns:repeat(3,1fr)!important; }
|
| 316 |
+
.row3 { grid-template-columns:repeat(2,1fr)!important; }
|
| 317 |
+
.cb { min-height:0!important; }
|
| 318 |
+
.cp { min-height:0!important; }
|
| 319 |
+
}
|
| 320 |
+
@media(max-width:600px){
|
| 321 |
+
html,body { overflow-y:auto!important; height:auto!important; }
|
| 322 |
+
.root { height:auto!important; overflow:visible!important; grid-template-rows:auto!important; }
|
| 323 |
+
.cards { grid-template-columns:1fr 1fr!important; }
|
| 324 |
+
.row3 { grid-template-columns:1fr!important; }
|
| 325 |
+
.cb { height:260px!important; }
|
| 326 |
+
.cp { height:210px!important; flex:none!important; }
|
| 327 |
+
.hs { display:none; }
|
| 328 |
+
.hdr { padding:4px 8px!important; }
|
| 329 |
+
}
|
| 330 |
</style>
|
| 331 |
+
</head>
|
| 332 |
+
<body>
|
| 333 |
<div class="root">
|
| 334 |
<div class="hdr">
|
| 335 |
<img class="logo" src="https://companieslogo.com/img/orig/AZ2.F-d26946db.png?t=1720244490" alt="AZ">
|
|
|
|
| 343 |
<div class="row3" id="row1"></div>
|
| 344 |
<div class="row3" id="row2"></div>
|
| 345 |
</div>
|
|
|
|
| 346 |
<script>
|
| 347 |
+
var FRAMES = __FRAMES__;
|
| 348 |
+
var MESES = __MESES__;
|
| 349 |
+
var TITULOS_L1 = __TITULOS_L1__;
|
| 350 |
+
var TITULOS_L2 = __TITULOS_L2__;
|
| 351 |
+
var SUBS_L1 = __SUBS_L1__;
|
| 352 |
+
var SUBS_L2 = __SUBS_L2__;
|
| 353 |
|
| 354 |
var CARDS_CFG = [
|
| 355 |
+
{col:'qtd_requisicoes', titulo:'Requisições', cor:'#003a70', sufixo:''},
|
| 356 |
+
{col:'qtd_unidades_emitidas', titulo:'Unidades Emitidas', cor:'#005a9c', sufixo:''},
|
| 357 |
+
{col:'qtd_itens_total', titulo:'Total de Itens', cor:'#0075be', sufixo:''}
|
| 358 |
];
|
| 359 |
|
| 360 |
+
var FRAME_MS = 700;
|
| 361 |
+
var PAUSE_MS = 60000;
|
| 362 |
+
var frameIdx = 0;
|
| 363 |
+
var animTimer = null;
|
| 364 |
+
var cdownTimer= null;
|
| 365 |
+
var inited = {};
|
| 366 |
|
| 367 |
+
function buildDOM() {
|
| 368 |
var cr = document.getElementById('cards-row');
|
| 369 |
+
CARDS_CFG.forEach(function(c) {
|
| 370 |
cr.innerHTML += '<div class="mc" style="border-left-color:'+c.cor+'">'
|
| 371 |
+
+ '<div class="ml">'+c.titulo+'</div>'
|
| 372 |
+
+ '<div class="mv" id="mv_'+c.col+'" style="color:'+c.cor+'">0</div></div>';
|
| 373 |
+
});
|
| 374 |
var r1 = document.getElementById('row1');
|
| 375 |
+
for (var i=0; i<3; i++) {
|
| 376 |
r1.innerHTML += '<div class="cb"><div class="ct">'
|
| 377 |
+
+ '<div class="ct-title">'+TITULOS_L1[i]+'</div>'
|
| 378 |
+
+ '<div class="ct-sub">'+SUBS_L1[i]+'</div></div>'
|
| 379 |
+
+ '<div class="cp" id="l1_'+i+'"></div></div>';
|
| 380 |
+
}
|
| 381 |
var r2 = document.getElementById('row2');
|
| 382 |
+
for (var i=0; i<3; i++) {
|
| 383 |
r2.innerHTML += '<div class="cb"><div class="ct">'
|
| 384 |
+
+ '<div class="ct-title">'+TITULOS_L2[i]+'</div>'
|
| 385 |
+
+ '<div class="ct-sub">'+SUBS_L2[i]+'</div></div>'
|
| 386 |
+
+ '<div class="cp" id="l2_'+i+'"></div></div>';
|
| 387 |
+
}
|
| 388 |
+
}
|
| 389 |
|
| 390 |
+
function gsize(id) {
|
| 391 |
var el = document.getElementById(id);
|
| 392 |
+
if (!el) return [200,150];
|
| 393 |
return [el.clientWidth||200, el.clientHeight||150];
|
| 394 |
+
}
|
| 395 |
+
|
| 396 |
var tickvals = Object.keys(MESES).map(Number);
|
| 397 |
+
var ticktext = Object.values(MESES);
|
| 398 |
+
|
| 399 |
+
function fmtDot(v) {
|
| 400 |
+
var n = Math.round(v);
|
| 401 |
+
if (n===0) return '0';
|
| 402 |
+
var s=n.toString(), r='', c=0;
|
| 403 |
+
for (var i=s.length-1; i>=0; i--) {
|
| 404 |
+
if (c>0 && c%3===0) r='.'+r;
|
| 405 |
+
r=s[i]+r; c++;
|
| 406 |
+
}
|
| 407 |
+
return r;
|
| 408 |
+
}
|
| 409 |
+
|
| 410 |
+
function renderBarH(item, elId) {
|
| 411 |
+
var wh = gsize(elId);
|
| 412 |
+
var el = document.getElementById(elId);
|
| 413 |
+
var meses = item.meses_ex || [];
|
| 414 |
+
var data = [];
|
| 415 |
+
var yOrder = meses.map(function(m){ return MESES[m]; }).slice().reverse();
|
| 416 |
+
var maxVal = 0;
|
| 417 |
+
item.traces.forEach(function(t){ t.y.forEach(function(v){ if(v>maxVal) maxVal=v; }); });
|
| 418 |
+
item.traces.forEach(function(t) {
|
| 419 |
+
var yV = t.y.slice().reverse();
|
| 420 |
+
data.push({
|
| 421 |
+
type:'bar', orientation:'h', name:t.ano,
|
| 422 |
+
x:yV, y:yOrder,
|
| 423 |
+
text:yV.map(function(){ return ''; }),
|
| 424 |
+
marker:{color:t.cor, line:{color:'white',width:1}},
|
| 425 |
+
hovertemplate:'<b>'+t.ano+' - %{y}</b><br>%{x:,.0f}<extra></extra>'
|
| 426 |
+
});
|
| 427 |
+
});
|
| 428 |
+
var annotations = [];
|
| 429 |
+
var accum = yOrder.map(function(){ return 0; });
|
| 430 |
+
item.traces.forEach(function(t) {
|
| 431 |
+
var yV = t.y.slice().reverse();
|
| 432 |
+
yV.forEach(function(v, i) {
|
| 433 |
+
if (!v) { accum[i]+=(v||0); return; }
|
| 434 |
+
var xStart = accum[i];
|
| 435 |
+
var inside = v >= maxVal*0.10;
|
| 436 |
+
annotations.push({
|
| 437 |
+
x: inside ? xStart+v/2 : xStart+v+maxVal*0.015,
|
| 438 |
y: yOrder[i],
|
| 439 |
+
xref:'x', yref:'y', showarrow:false,
|
| 440 |
xanchor: inside?'center':'left',
|
| 441 |
yanchor:'middle',
|
| 442 |
text:'<b>'+fmtDot(v)+'</b>',
|
| 443 |
+
font:{size:11,family:'DM Sans',color:inside?'white':'#1e3a5f'}
|
| 444 |
+
});
|
| 445 |
+
accum[i] += v;
|
| 446 |
+
});
|
| 447 |
+
});
|
| 448 |
+
var layout = {
|
| 449 |
+
width:wh[0], height:wh[1], autosize:true, barmode:'stack',
|
| 450 |
+
template:'plotly_white', showlegend:false, font:{family:'DM Sans',size:11},
|
| 451 |
+
margin:{l:70,r:56,t:8,b:18},
|
| 452 |
+
plot_bgcolor:'rgba(248,250,252,0.8)', paper_bgcolor:'white',
|
| 453 |
+
bargap:0.28, annotations:annotations,
|
| 454 |
+
xaxis:{tickfont:{size:9,family:'DM Sans',color:'#9ca3af'},
|
| 455 |
+
showgrid:true, gridcolor:'rgba(200,200,200,0.25)',
|
| 456 |
+
rangemode:'tozero', zeroline:false, range:[0,maxVal*1.25]},
|
| 457 |
+
yaxis:{tickfont:{size:11,family:'DM Sans',color:'#374151'},
|
| 458 |
+
showgrid:false, categoryorder:'array', categoryarray:yOrder, ticksuffix:' '},
|
| 459 |
+
hoverlabel:{bgcolor:'white',font_size:12,font_family:'DM Sans'}
|
| 460 |
+
};
|
| 461 |
+
if (!inited[elId]) {
|
| 462 |
+
Plotly.newPlot(el, data, layout, {displayModeBar:false, responsive:true});
|
| 463 |
+
inited[elId] = true;
|
| 464 |
+
} else { Plotly.react(el, data, layout); }
|
| 465 |
+
roundBars(el, 8, 'h');
|
| 466 |
+
}
|
| 467 |
+
|
| 468 |
+
function renderBarV(item, elId) {
|
| 469 |
+
var wh = gsize(elId);
|
| 470 |
+
var el = document.getElementById(elId);
|
| 471 |
+
var meses = item.meses_ex || [];
|
| 472 |
+
var data = [];
|
| 473 |
+
var maxByMes = {};
|
| 474 |
+
meses.forEach(function(m,mi) {
|
| 475 |
+
var s=0; item.traces.forEach(function(t){ s+=t.y[mi]||0; }); maxByMes[mi]=s;
|
| 476 |
+
});
|
| 477 |
+
var globalMax = Math.max.apply(null, meses.map(function(_,mi){ return maxByMes[mi]; }));
|
| 478 |
+
var annotations = [];
|
| 479 |
+
meses.forEach(function(m,mi) {
|
| 480 |
+
var tot = maxByMes[mi];
|
| 481 |
+
if (!tot) return;
|
| 482 |
+
annotations.push({x:MESES[m],y:tot,xref:'x',yref:'y',showarrow:false,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 483 |
yanchor:'bottom',xanchor:'center',yshift:5,
|
| 484 |
+
text:'<b>'+fmtDot(tot)+'</b>',font:{size:11,family:'DM Sans',color:'#1e3a5f'}});
|
| 485 |
+
});
|
| 486 |
+
var accumV = meses.map(function(){ return 0; });
|
| 487 |
+
item.traces.forEach(function(t) {
|
| 488 |
+
var segAnnots = [];
|
| 489 |
+
t.y.forEach(function(v,mi) {
|
| 490 |
+
if (v>0) {
|
| 491 |
+
var yBase = accumV[mi];
|
| 492 |
+
if (v >= globalMax*0.08) {
|
| 493 |
+
segAnnots.push({x:MESES[meses[mi]],y:yBase+v/2,xref:'x',yref:'y',showarrow:false,
|
|
|
|
|
|
|
| 494 |
xanchor:'center',yanchor:'middle',
|
| 495 |
+
text:'<b>'+fmtDot(v)+'</b>',font:{size:10,family:'DM Sans',color:'white'}});
|
| 496 |
+
}
|
| 497 |
+
accumV[mi] += v;
|
| 498 |
+
} else { accumV[mi]+=(v||0); }
|
| 499 |
+
});
|
| 500 |
+
segAnnots.forEach(function(a){ annotations.push(a); });
|
| 501 |
+
data.push({type:'bar',name:t.ano,
|
| 502 |
+
x:meses.map(function(m){ return MESES[m]; }), y:t.y,
|
| 503 |
+
text:t.y.map(function(){ return ''; }),
|
| 504 |
+
marker:{color:t.cor, line:{color:'white',width:1}},
|
| 505 |
+
hovertemplate:'<b>'+t.ano+' - %{x}</b><br>%{y:,.0f}<extra></extra>'});
|
| 506 |
+
});
|
| 507 |
+
var layout = {
|
| 508 |
+
width:wh[0], height:wh[1], autosize:true, barmode:'stack',
|
| 509 |
+
template:'plotly_white', showlegend:false, font:{family:'DM Sans',size:11},
|
| 510 |
+
margin:{l:34,r:16,t:30,b:32},
|
| 511 |
+
plot_bgcolor:'rgba(248,250,252,0.8)', paper_bgcolor:'white',
|
| 512 |
+
bargap:0.28, annotations:annotations,
|
| 513 |
+
xaxis:{tickfont:{size:11,family:'DM Sans',color:'#374151'},showgrid:false,tickangle:0},
|
| 514 |
+
yaxis:{tickfont:{size:9,family:'DM Sans',color:'#9ca3af'},showgrid:true,
|
| 515 |
+
gridcolor:'rgba(200,200,200,0.25)',zeroline:false,range:[0,globalMax*1.20]},
|
| 516 |
+
hoverlabel:{bgcolor:'white',font_size:12,font_family:'DM Sans'}
|
| 517 |
+
};
|
| 518 |
+
if (!inited[elId]) {
|
| 519 |
+
Plotly.newPlot(el, data, layout, {displayModeBar:false, responsive:true});
|
| 520 |
+
inited[elId] = true;
|
| 521 |
+
} else { Plotly.react(el, data, layout); }
|
| 522 |
+
roundBars(el, 8, 'v');
|
| 523 |
+
}
|
| 524 |
+
|
| 525 |
+
function renderLine(item, elId) {
|
| 526 |
+
var wh = gsize(elId);
|
| 527 |
+
var el = document.getElementById(elId);
|
| 528 |
+
var data = [], annotations = [], yshifts = [18,32,46,60];
|
| 529 |
+
item.traces.forEach(function(t, i) {
|
| 530 |
+
data.push({
|
| 531 |
+
type:'scatter', mode:'lines+markers',
|
| 532 |
+
x:t.meses, y:t.y,
|
| 533 |
+
line:{width:3, color:t.cor, shape:'spline'},
|
| 534 |
+
marker:{size:8, color:t.cor, line:{width:2,color:'white'}},
|
| 535 |
+
fill: i>0?'tonexty':null, fillcolor:t.fill_rgba,
|
| 536 |
+
cliponaxis:false, showlegend:false, name:t.ano,
|
| 537 |
+
hovertemplate:'<b>'+t.ano+' - %{customdata}</b><br>%{y:,.0f}<extra></extra>',
|
| 538 |
+
customdata:t.meses.map(function(m){ return MESES[m]; })
|
| 539 |
+
});
|
| 540 |
+
var shift = yshifts[i % yshifts.length];
|
| 541 |
+
t.meses.forEach(function(mx, j) {
|
| 542 |
+
if (!t.text[j]) return;
|
| 543 |
+
annotations.push({x:mx,y:t.y[j],xref:'x',yref:'y',text:t.text[j],
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 544 |
showarrow:false,yshift:shift,
|
| 545 |
+
font:{size:11,family:'DM Sans',color:'#1a1a1a'},bgcolor:'rgba(0,0,0,0)'});
|
| 546 |
+
});
|
| 547 |
+
});
|
| 548 |
+
var layout = {
|
| 549 |
+
autosize:true,
|
| 550 |
+
hovermode:'x unified', template:'plotly_white',
|
| 551 |
+
font:{family:'DM Sans',size:9}, showlegend:false,
|
| 552 |
+
margin:{l:38,r:18,t:30,b:38},
|
| 553 |
+
plot_bgcolor:'rgba(248,249,250,0.5)', annotations:annotations,
|
| 554 |
+
xaxis:{tickmode:'array',tickvals:tickvals,ticktext:ticktext,tickangle:0,
|
| 555 |
+
tickfont:{size:9,family:'DM Sans'},showgrid:true,
|
| 556 |
+
gridcolor:'rgba(200,200,200,0.2)',ticklabelstandoff:8},
|
| 557 |
+
yaxis:{tickfont:{size:9,family:'DM Sans'},rangemode:'tozero',
|
| 558 |
+
showgrid:true,gridcolor:'rgba(200,200,200,0.2)'},
|
| 559 |
+
hoverlabel:{bgcolor:'white',font_size:11,font_family:'DM Sans'}
|
| 560 |
+
};
|
| 561 |
+
if (!inited[elId]) {
|
| 562 |
+
Plotly.newPlot(el, data, layout, {displayModeBar:false, responsive:true});
|
| 563 |
+
inited[elId] = true;
|
| 564 |
+
} else { Plotly.react(el, data, layout); }
|
| 565 |
+
}
|
| 566 |
+
|
| 567 |
+
function renderDonut(item, elId) {
|
| 568 |
+
if (!item.valores.length) return;
|
| 569 |
+
var wh = gsize(elId);
|
| 570 |
+
var el = document.getElementById(elId);
|
| 571 |
+
var total = item.valores.reduce(function(s,v){ return s+v; }, 0);
|
| 572 |
+
var labels = item.labels.map(function(l,i) {
|
| 573 |
+
var pct = total>0 ? (item.valores[i]/total*100).toFixed(1) : '0.0';
|
| 574 |
return l+': '+fmtDot(item.valores[i])+' ('+pct+'%)';
|
| 575 |
+
});
|
| 576 |
+
var textF = item.valores.map(function(v){ return total>0?(v/total*100).toFixed(1)+'%':''; });
|
| 577 |
+
var data = [{
|
| 578 |
+
type:'pie', labels:labels, values:item.valores,
|
| 579 |
+
marker:{colors:item.cores, line:{color:'white',width:3}},
|
| 580 |
+
hole:0.4, text:textF, textinfo:'text',
|
| 581 |
+
textfont:{size:12,family:'Sora'}, insidetextorientation:'radial',
|
| 582 |
+
hovertemplate:'<b>%{label}</b><extra></extra>',
|
| 583 |
+
pull:item.valores.map(function(v){ return v===Math.max.apply(null,item.valores)?0.03:0; }),
|
| 584 |
+
rotation:90, direction:'clockwise', sort:false
|
| 585 |
+
}];
|
| 586 |
+
var layout = {
|
| 587 |
+
autosize:true,
|
| 588 |
+
template:'plotly_white', font:{family:'DM Sans',size:9}, showlegend:true,
|
| 589 |
+
legend:{orientation:'v',yanchor:'middle',y:0.5,xanchor:'left',x:1.02,font:{size:8,family:'DM Sans',color:'#333'}},
|
| 590 |
+
margin:{l:10,r:130,t:10,b:10},
|
| 591 |
+
hoverlabel:{bgcolor:'white',font_size:11,font_family:'DM Sans'}
|
| 592 |
+
};
|
| 593 |
+
if (!inited[elId]) {
|
| 594 |
+
Plotly.newPlot(el, data, layout, {displayModeBar:false, responsive:true});
|
| 595 |
+
inited[elId] = true;
|
| 596 |
+
} else { Plotly.react(el, data, layout); }
|
| 597 |
+
}
|
| 598 |
+
|
| 599 |
+
function renderLineSis(item, elId) {
|
| 600 |
+
if (!item.valores.length) return;
|
| 601 |
+
var wh = gsize(elId);
|
| 602 |
+
var el = document.getElementById(elId);
|
| 603 |
+
var total = item.valores.reduce(function(s,v){ return s+v; }, 0);
|
| 604 |
+
var xVals = item.labels.map(function(_,i){ return i; });
|
| 605 |
+
var texts = item.valores.map(function(v) {
|
| 606 |
+
var pct = total>0 ? (v/total*100).toFixed(1) : '0.0';
|
| 607 |
+
return v>0 ? '<b>'+fmtDot(v)+'</b><br>'+pct+'%' : '';
|
| 608 |
+
});
|
| 609 |
+
var cor = item.cores[0] || '#0056A3';
|
| 610 |
+
var r_c = parseInt(cor.slice(1,3),16);
|
| 611 |
+
var g_c = parseInt(cor.slice(3,5),16);
|
| 612 |
+
var b_c = parseInt(cor.slice(5,7),16);
|
| 613 |
+
var fill = 'rgba('+r_c+','+g_c+','+b_c+',0.2)';
|
| 614 |
+
var data = [{
|
| 615 |
+
type:'scatter', mode:'lines+markers+text',
|
| 616 |
+
x:xVals, y:item.valores,
|
| 617 |
+
text:texts, textposition:'top center',
|
| 618 |
+
textfont:{size:11,family:'DM Sans',color:'#1a1a1a'},
|
| 619 |
+
line:{width:4, color:cor, shape:'spline'},
|
| 620 |
+
marker:{size:14, color:item.cores, symbol:'circle'},
|
| 621 |
+
fill:'tozeroy', fillcolor:fill,
|
| 622 |
+
hovertemplate:'<b>%{customdata}</b><br>%{y:,.0f}<extra></extra>',
|
| 623 |
+
customdata:item.labels, showlegend:false, cliponaxis:false
|
| 624 |
+
}];
|
| 625 |
+
var layout = {
|
| 626 |
+
autosize:true,
|
| 627 |
+
template:'plotly_white', showlegend:false, font:{family:'DM Sans',size:9},
|
| 628 |
+
margin:{l:10,r:10,t:30,b:72},
|
| 629 |
+
plot_bgcolor:'rgba(248,249,250,0.5)', paper_bgcolor:'white',
|
| 630 |
+
xaxis:{tickmode:'array',tickvals:xVals,ticktext:item.labels,
|
| 631 |
+
tickfont:{size:8,family:'DM Sans',color:'#333'},tickangle:-35,
|
| 632 |
+
showgrid:false,zeroline:false,showline:false,ticks:'',automargin:true},
|
| 633 |
+
yaxis:{showgrid:true,gridcolor:'rgba(200,200,200,0.35)',gridwidth:1,
|
| 634 |
zeroline:false,showline:false,showticklabels:false,ticks:'',
|
| 635 |
+
range:[0,Math.max.apply(null,item.valores)*1.6]},
|
| 636 |
+
hoverlabel:{bgcolor:'white',font_size:11,font_family:'DM Sans'}
|
| 637 |
+
};
|
| 638 |
+
if (!inited[elId]) {
|
| 639 |
+
Plotly.newPlot(el, data, layout, {displayModeBar:false, responsive:true});
|
| 640 |
+
inited[elId] = true;
|
| 641 |
+
} else { Plotly.react(el, data, layout); }
|
| 642 |
+
}
|
| 643 |
+
|
| 644 |
+
function renderBarVSis(item, elId) {
|
| 645 |
+
if (!item.valores.length) return;
|
| 646 |
+
var wh = gsize(elId);
|
| 647 |
+
var el = document.getElementById(elId);
|
| 648 |
+
var total = item.valores.reduce(function(s,v){ return s+v; }, 0);
|
| 649 |
+
var maxVal = Math.max.apply(null, item.valores);
|
| 650 |
+
var annotations = item.labels.map(function(l,i) {
|
| 651 |
+
var v = item.valores[i];
|
| 652 |
+
var pct = total>0 ? (v/total*100).toFixed(1) : '0.0';
|
| 653 |
+
return {x:l, y:v, xref:'x', yref:'y', showarrow:false,
|
| 654 |
+
yanchor:'bottom', xanchor:'center', yshift:4,
|
| 655 |
text:'<b>'+fmtDot(v)+'</b><br><span style="font-size:10px">'+pct+'%</span>',
|
| 656 |
+
font:{size:11,family:'DM Sans',color:'#1e3a5f'}};
|
| 657 |
+
});
|
| 658 |
+
var data = [{
|
| 659 |
+
type:'bar', x:item.labels, y:item.valores,
|
| 660 |
+
text:item.valores.map(function(){ return ''; }),
|
| 661 |
+
marker:{color:item.cores, line:{color:'white',width:2}},
|
| 662 |
+
hovertemplate:'<b>%{x}</b><br>%{y:,.0f}<extra></extra>',
|
| 663 |
+
width:0.6, showlegend:false
|
| 664 |
+
}];
|
| 665 |
+
var layout = {
|
| 666 |
+
autosize:true,
|
| 667 |
+
template:'plotly_white', showlegend:false, font:{family:'DM Sans',size:11},
|
| 668 |
+
margin:{l:30,r:18,t:40,b:72},
|
| 669 |
+
plot_bgcolor:'rgba(248,249,250,0.6)', paper_bgcolor:'white',
|
| 670 |
+
bargap:0.3, annotations:annotations,
|
| 671 |
+
xaxis:{tickfont:{size:9,family:'DM Sans',color:'#374151'},showgrid:false,tickangle:-30,automargin:true},
|
| 672 |
+
yaxis:{tickfont:{size:9,family:'DM Sans'},showgrid:true,
|
| 673 |
+
gridcolor:'rgba(200,200,200,0.35)',range:[0,maxVal*1.35],showticklabels:false,zeroline:false},
|
| 674 |
+
hoverlabel:{bgcolor:'white',font_size:12,font_family:'DM Sans'}
|
| 675 |
+
};
|
| 676 |
+
if (!inited[elId]) {
|
| 677 |
+
Plotly.newPlot(el, data, layout, {displayModeBar:false, responsive:true});
|
| 678 |
+
inited[elId] = true;
|
| 679 |
+
} else { Plotly.react(el, data, layout); }
|
| 680 |
+
}
|
| 681 |
+
|
| 682 |
+
function roundBars(boxEl, r, orientation) {
|
| 683 |
+
setTimeout(function() {
|
| 684 |
+
boxEl.querySelectorAll('.bars path').forEach(function(p) {
|
| 685 |
+
var d = p.getAttribute('d');
|
| 686 |
+
if (!d) return;
|
| 687 |
+
var tok = d.match(/[A-Za-z][^A-Za-z]*/g) || [];
|
| 688 |
+
var cx=0, cy=0, aX=[], aY=[];
|
| 689 |
+
tok.forEach(function(t) {
|
| 690 |
+
var cmd = t[0];
|
| 691 |
+
var pts = t.slice(1).trim().split(/[\\s,]+/).map(Number).filter(function(n){ return !isNaN(n); });
|
| 692 |
+
if (cmd==='M'||cmd==='L') { aX.push(pts[0]); aY.push(pts[1]); cx=pts[0]; cy=pts[1]; }
|
| 693 |
+
else if (cmd==='H') { aX.push(pts[0]); aY.push(cy); cx=pts[0]; }
|
| 694 |
+
else if (cmd==='V') { aX.push(cx); aY.push(pts[0]); cy=pts[0]; }
|
| 695 |
+
});
|
| 696 |
+
if (aX.length < 2) return;
|
| 697 |
+
var x0=Math.min.apply(null,aX), x1=Math.max.apply(null,aX);
|
| 698 |
+
var y0=Math.min.apply(null,aY), y1=Math.max.apply(null,aY);
|
| 699 |
+
var w=x1-x0, h=y1-y0;
|
| 700 |
+
if (w<=0 || h<=0) return;
|
| 701 |
+
var rr = Math.min(r, w/2, h/2);
|
| 702 |
+
var nd;
|
| 703 |
+
if (orientation === 'h') {
|
| 704 |
+
nd = 'M '+x0+','+y0
|
| 705 |
+
+ ' H '+(x1-rr)+' Q '+x1+','+y0+' '+x1+','+(y0+rr)
|
| 706 |
+
+ ' V '+(y1-rr)+' Q '+x1+','+y1+' '+(x1-rr)+','+y1
|
| 707 |
+
+ ' H '+x0+' V '+y0+' Z';
|
| 708 |
+
} else {
|
| 709 |
+
nd = 'M '+x0+','+y1
|
| 710 |
+
+ ' H '+x1
|
| 711 |
+
+ ' V '+(y0+rr)
|
| 712 |
+
+ ' Q '+x1+','+y0+' '+(x1-rr)+','+y0
|
| 713 |
+
+ ' H '+(x0+rr)+' Q '+x0+','+y0+' '+x0+','+(y0+rr)
|
| 714 |
+
+ ' V '+y1+' Z';
|
| 715 |
+
}
|
| 716 |
+
p.setAttribute('d', nd);
|
| 717 |
+
});
|
| 718 |
+
}, 80);
|
| 719 |
+
}
|
| 720 |
+
|
| 721 |
+
function renderFrame(f) {
|
| 722 |
+
CARDS_CFG.forEach(function(c) {
|
| 723 |
+
var cd = f.cards[c.col];
|
| 724 |
+
if (cd) document.getElementById('mv_'+c.col).textContent = cd.valor+cd.sufixo;
|
| 725 |
+
});
|
| 726 |
+
f.linha1.forEach(function(item, i) {
|
| 727 |
+
if (item.tipo==='barra_h') renderBarH(item, 'l1_'+i);
|
| 728 |
+
else if (item.tipo==='barra_v') renderBarV(item, 'l1_'+i);
|
| 729 |
+
else if (item.tipo==='linha') renderLine(item, 'l1_'+i);
|
| 730 |
+
});
|
| 731 |
+
f.linha2.forEach(function(item, i) {
|
| 732 |
+
if (item.tipo==='donut') renderDonut(item, 'l2_'+i);
|
| 733 |
+
else if (item.tipo==='linha_sis') renderLineSis(item, 'l2_'+i);
|
| 734 |
+
else if (item.tipo==='barv_sis') renderBarVSis(item, 'l2_'+i);
|
| 735 |
+
});
|
| 736 |
+
}
|
| 737 |
+
|
| 738 |
+
function stepFrame() {
|
| 739 |
renderFrame(FRAMES[frameIdx]);
|
| 740 |
frameIdx++;
|
| 741 |
+
if (frameIdx >= FRAMES.length) {
|
| 742 |
clearTimeout(animTimer);
|
| 743 |
+
cdownTimer = setTimeout(function(){ frameIdx=0; startAnimation(); }, PAUSE_MS);
|
| 744 |
+
} else {
|
| 745 |
+
animTimer = setTimeout(stepFrame, FRAME_MS);
|
| 746 |
+
}
|
| 747 |
+
}
|
| 748 |
|
| 749 |
+
function startAnimation() {
|
| 750 |
+
clearTimeout(animTimer);
|
| 751 |
+
clearTimeout(cdownTimer);
|
| 752 |
+
animTimer = setTimeout(stepFrame, FRAME_MS);
|
| 753 |
+
}
|
| 754 |
|
| 755 |
var _rt;
|
| 756 |
+
window.addEventListener('resize', function() {
|
| 757 |
clearTimeout(_rt);
|
| 758 |
+
_rt = setTimeout(function() {
|
| 759 |
+
document.querySelectorAll('.cp').forEach(function(box) {
|
| 760 |
+
if (box.data) Plotly.relayout(box, {width:box.clientWidth, height:box.clientHeight});
|
| 761 |
+
});
|
| 762 |
+
}, 200);
|
| 763 |
+
});
|
| 764 |
+
|
| 765 |
+
window.addEventListener('load', function() {
|
| 766 |
buildDOM();
|
| 767 |
+
setTimeout(function(){ frameIdx=0; startAnimation(); }, 400);
|
| 768 |
+
});
|
| 769 |
</script>
|
| 770 |
+
</body>
|
| 771 |
+
</html>"""
|
| 772 |
+
|
| 773 |
+
html = (html
|
| 774 |
+
.replace('__FRAMES__', frames_json)
|
| 775 |
+
.replace('__MESES__', meses_json)
|
| 776 |
+
.replace('__TITULOS_L1__', json.dumps(TITULOS_L1))
|
| 777 |
+
.replace('__TITULOS_L2__', json.dumps(TITULOS_L2))
|
| 778 |
+
.replace('__SUBS_L1__', json.dumps(subs_l1))
|
| 779 |
+
.replace('__SUBS_L2__', json.dumps(subs_l2))
|
| 780 |
+
)
|
| 781 |
|
| 782 |
+
components.html(html, height=8000, scrolling=True)
|
| 783 |
|
| 784 |
else:
|
| 785 |
+
components.html("""<!DOCTYPE html><html><body style="display:flex;align-items:center;
|
| 786 |
+
justify-content:center;height:100dvh;font-family:sans-serif;color:#003a70;background:#f5f7fa;">
|
| 787 |
+
<div style="text-align:center"><div style="font-size:2em">📊</div>
|
| 788 |
<p>Aguardando dados... Recarregue a página.</p></div></body></html>""",
|
| 789 |
height=4000, scrolling=False)
|
| 790 |
|
pages/full.py
CHANGED
|
@@ -8,6 +8,22 @@ from gs_client import read_ws
|
|
| 8 |
|
| 9 |
st.set_page_config(layout="wide", initial_sidebar_state="collapsed", page_title="PMJA - Dashboard")
|
| 10 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
# ── SESSION ───────────────────────────────────────────────────────────
|
| 12 |
_cc = CookieController()
|
| 13 |
COOKIE_NAME = "pmja_session"
|
|
@@ -38,33 +54,70 @@ if time.time() - st.session_state.inicio_exibicao >= 120:
|
|
| 38 |
st.session_state.inicio_exibicao = time.time()
|
| 39 |
st.switch_page("pages/atual.py")
|
| 40 |
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
.
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
.
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
|
| 57 |
# ── HELPERS ───────────────────────────────────────────────────────────
|
| 58 |
def fmt(n):
|
| 59 |
-
return "0" if pd.isna(n) else
|
| 60 |
-
|
| 61 |
-
def fix_df(df):
|
| 62 |
-
for col in df.columns:
|
| 63 |
-
if df[col].dtype == 'object':
|
| 64 |
-
num = pd.to_numeric(df[col], errors='coerce')
|
| 65 |
-
df[col] = num if num.notna().sum() == df[col].notna().sum() \
|
| 66 |
-
else df[col].fillna('').astype(str).replace('nan','')
|
| 67 |
-
return df
|
| 68 |
|
| 69 |
def proc_exp(df_raw):
|
| 70 |
if df_raw is None or df_raw.empty: return None, None
|
|
@@ -114,23 +167,16 @@ COR_SIS = ['#001a33','#002d4d','#003d66','#004d80','#005d99','#006db3',
|
|
| 114 |
COR_SIS_EXP = ['#064e3b','#065f46','#047857','#059669','#10b981','#34d399',
|
| 115 |
'#4ade80','#6ee7b7','#86efac','#a7f3d0','#bbf7d0','#d1fae5','#ecfdf5','#f0fdf4']
|
| 116 |
|
| 117 |
-
# ── BUILD ALL FRAMES ──────────────────────────────────────────────────
|
| 118 |
def build_all_frames(df_rec, df_exp, df_rec_sis, df_exp_det, sistemas_rec,
|
| 119 |
anos_rec, anos_exp, mes_max, mes_max_rec, mes_max_exp, mes_max_sis):
|
| 120 |
frames = []
|
| 121 |
-
|
| 122 |
for mes in range(1, mes_max+1):
|
| 123 |
frame = {"mes": mes, "metrics": {}, "charts": {}}
|
| 124 |
-
|
| 125 |
for col, key, src in [
|
| 126 |
-
('qtd_descarregamentos','desc','rec'),
|
| 127 |
-
('
|
| 128 |
-
('
|
| 129 |
-
('
|
| 130 |
-
('qtd_itens_por_unidade','itens','rec'),
|
| 131 |
-
('qtd_requisicoes','req','exp'),
|
| 132 |
-
('qtd_unidades_emitidas','uem','exp'),
|
| 133 |
-
('qtd_itens_total','itot','exp'),
|
| 134 |
]:
|
| 135 |
if src=='rec':
|
| 136 |
v = df_rec[df_rec['mes']<=mes][col].sum() if mes<=mes_max_rec else df_rec[col].sum()
|
|
@@ -139,17 +185,17 @@ def build_all_frames(df_rec, df_exp, df_rec_sis, df_exp_det, sistemas_rec,
|
|
| 139 |
frame["metrics"][key] = fmt(v)
|
| 140 |
|
| 141 |
def line_traces(df, col, anos, cores, lim):
|
| 142 |
-
traces
|
| 143 |
m = mes if mes<=lim else lim
|
| 144 |
for i,ano in enumerate(anos):
|
| 145 |
d = df[(df['ano']==ano)&(df['mes']<=m)].sort_values('mes')
|
| 146 |
if d.empty: continue
|
| 147 |
-
|
| 148 |
traces.append({
|
| 149 |
-
"x":
|
| 150 |
-
"y":
|
| 151 |
-
"text":
|
| 152 |
-
"line_color":
|
| 153 |
})
|
| 154 |
return traces
|
| 155 |
|
|
@@ -159,14 +205,16 @@ def build_all_frames(df_rec, df_exp, df_rec_sis, df_exp_det, sistemas_rec,
|
|
| 159 |
frame["charts"]["r1_2"] = line_traces(df_rec,'qtd_volumes_recebidos',anos_rec,COR_REC,mes_max_rec)
|
| 160 |
|
| 161 |
if df_rec_sis is not None and mes<=mes_max_sis and sistemas_rec:
|
| 162 |
-
cols_map = {
|
| 163 |
-
|
| 164 |
-
|
|
|
|
|
|
|
| 165 |
slices = []
|
| 166 |
for i,s in enumerate(sistemas_rec):
|
| 167 |
col = next((c for c,v in cols_map.items() if v==s), None)
|
| 168 |
if col and col in df_rec_sis.columns:
|
| 169 |
-
t = float(pd.to_numeric(df_rec_sis.loc[df_rec_sis['mes']<=mes,
|
| 170 |
if t>0: slices.append({"label":s,"value":t,"color":COR_SIS[i%len(COR_SIS)]})
|
| 171 |
if slices: frame["charts"]["r1_3"] = slices
|
| 172 |
|
|
@@ -175,18 +223,17 @@ def build_all_frames(df_rec, df_exp, df_rec_sis, df_exp_det, sistemas_rec,
|
|
| 175 |
frame["charts"]["r2_2"] = line_traces(df_rec,'qtd_itens_por_unidade',anos_rec,COR_REC,mes_max_rec)
|
| 176 |
|
| 177 |
if df_rec_sis is not None and mes<=mes_max_sis and sistemas_rec:
|
| 178 |
-
for slot, palavra, excluir in [
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
if palavra in col and not any(e in col for e in excluir) and s in col}
|
| 185 |
bars = []
|
| 186 |
for i,s in enumerate(sistemas_rec):
|
| 187 |
col = next((c for c,v in cols_map2.items() if v==s), None)
|
| 188 |
if col and col in df_rec_sis.columns:
|
| 189 |
-
t = float(pd.to_numeric(df_rec_sis.loc[df_rec_sis['mes']<=mes,
|
| 190 |
if t>0: bars.append({"label":s,"value":t,"color":COR_SIS[i%len(COR_SIS)]})
|
| 191 |
if bars: frame["charts"][slot] = bars
|
| 192 |
|
|
@@ -196,32 +243,25 @@ def build_all_frames(df_rec, df_exp, df_rec_sis, df_exp_det, sistemas_rec,
|
|
| 196 |
frame["charts"]["e1_2"] = line_traces(df_exp,'qtd_itens_total',anos_exp,COR_EXP,mes_max_exp)
|
| 197 |
|
| 198 |
if df_exp_det is not None and mes<=mes_max_exp:
|
| 199 |
-
det
|
| 200 |
-
req_c
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
for slot, cols in [('es_0',req_c),('es_1',
|
| 204 |
if not cols: continue
|
| 205 |
tots = {}
|
| 206 |
for col in cols:
|
| 207 |
cat = ext_cat(col)
|
| 208 |
if cat not in tots: tots[cat]={'v':0.0,'c':COR_SIS_EXP[len(tots)%len(COR_SIS_EXP)]}
|
| 209 |
-
det.loc[:,col] = pd.to_numeric(det[col],
|
| 210 |
tots[cat]['v'] += float(det[col].sum())
|
| 211 |
-
dados = sorted([(k,v) for k,v in tots.items() if v['v']>0],
|
| 212 |
-
if dados:
|
| 213 |
-
frame["charts"][slot] = [{"label":k,"value":v['v'],"color":v['c']} for k,v in dados]
|
| 214 |
|
| 215 |
frames.append(frame)
|
| 216 |
return frames
|
| 217 |
|
| 218 |
-
|
| 219 |
-
# ── LOAD DATA ─────────────────────────────────────────────────────────
|
| 220 |
-
with st.spinner('📊 Carregando dados...'):
|
| 221 |
-
df_recebimento = read_ws("recebimento") # ← gs_client (era: load_ws("recebimento"))
|
| 222 |
-
df_rec_dados = read_ws("rec_dados") # ← gs_client (era: load_ws("rec_dados"))
|
| 223 |
-
df_exp_raw = read_ws("exp_dados") # ← gs_client (era: load_ws("exp_dados"))
|
| 224 |
-
|
| 225 |
df_exp_raw = df_exp_raw if df_exp_raw is not None and not df_exp_raw.empty else None
|
| 226 |
df_recebimento = df_recebimento if df_recebimento is not None and not df_recebimento.empty else None
|
| 227 |
df_rec_dados = df_rec_dados if df_rec_dados is not None and not df_rec_dados.empty else None
|
|
@@ -236,8 +276,7 @@ if df_recebimento is not None:
|
|
| 236 |
'peso_recebido','qtd_unidades_recebidos','qtd_itens_por_unidade']
|
| 237 |
df_rec['data'] = pd.to_datetime(df_rec['mes_ano'], format='%m/%Y', errors='coerce')
|
| 238 |
df_rec = df_rec.dropna(subset=['data']).sort_values('data')
|
| 239 |
-
df_rec['ano'] = df_rec['data'].dt.year
|
| 240 |
-
df_rec['mes'] = df_rec['data'].dt.month
|
| 241 |
for col in ['qtd_descarregamentos','qtd_volumes_recebidos','peso_recebido',
|
| 242 |
'qtd_unidades_recebidos','qtd_itens_por_unidade']:
|
| 243 |
if col in df_rec.columns:
|
|
@@ -262,85 +301,91 @@ if df_rec_dados is not None:
|
|
| 262 |
sistemas_rec = get_sistemas(df_rec_sis)
|
| 263 |
mes_max_sis = int(df_rec_sis['mes'].max())
|
| 264 |
|
| 265 |
-
# ── BUILD & RENDER ────────────────────────────────────────────────────
|
| 266 |
if df_rec is not None and df_exp is not None and mes_max_rec>0 and mes_max_exp>0:
|
| 267 |
anos_rec = [int(a) for a in sorted(df_rec['ano'].unique())]
|
| 268 |
anos_exp = [int(a) for a in sorted(df_exp['ano'].unique())]
|
| 269 |
mes_max = max(mes_max_rec, mes_max_exp, mes_max_sis if mes_max_sis>0 else 0)
|
| 270 |
|
| 271 |
-
frames = build_all_frames(
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
)
|
| 275 |
frames_json = json.dumps(frames, ensure_ascii=False)
|
| 276 |
meses_json = json.dumps(MESES)
|
| 277 |
|
| 278 |
-
html =
|
|
|
|
|
|
|
| 279 |
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
| 280 |
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;600;700;800&family=Sora:wght@700;800;900&display=swap" rel="stylesheet">
|
| 281 |
<script src="https://cdn.plot.ly/plotly-2.27.0.min.js" charset="utf-8"></script>
|
| 282 |
<style>
|
| 283 |
-
*{
|
| 284 |
-
html,body{
|
| 285 |
background:linear-gradient(160deg,#f0faf6 0%,#e6f7f1 55%,#d1fae5 100%);
|
| 286 |
-
font-family:'DM Sans',sans-serif;}
|
| 287 |
-
::-webkit-scrollbar{
|
| 288 |
-
::-webkit-scrollbar-thumb{
|
| 289 |
-
.root{
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
.logo{
|
| 299 |
-
.ht{
|
| 300 |
-
|
| 301 |
-
.
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
.
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
.
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
.
|
| 314 |
-
|
| 315 |
-
.
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
.
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
}
|
| 334 |
-
|
| 335 |
-
.
|
| 336 |
-
.
|
| 337 |
-
.cb{
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
.
|
| 341 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 342 |
</style>
|
| 343 |
-
</head>
|
|
|
|
| 344 |
<div class="root">
|
| 345 |
<div class="hdr">
|
| 346 |
<img class="logo" src="https://companieslogo.com/img/orig/AZ2.F-d26946db.png?t=1720244490" alt="AZ">
|
|
@@ -350,7 +395,6 @@ html,body{{width:100%;height:100%;overflow:hidden;
|
|
| 350 |
</div>
|
| 351 |
<img class="logo" src="https://upload.wikimedia.org/wikipedia/commons/8/8f/Logo-engie.svg" alt="Engie">
|
| 352 |
</div>
|
| 353 |
-
|
| 354 |
<div class="mrow">
|
| 355 |
<div class="mc" style="border-left-color:#003a70"><div class="ml">Descarregamentos</div><div class="mv" id="m_desc" style="color:#003a70">0</div></div>
|
| 356 |
<div class="mc" style="border-left-color:#005a9c"><div class="ml">Peso Recebido</div><div class="mv" id="m_peso" style="color:#005a9c">0 kg</div></div>
|
|
@@ -361,7 +405,6 @@ html,body{{width:100%;height:100%;overflow:hidden;
|
|
| 361 |
<div class="mc" style="border-left-color:#059669"><div class="ml">Unid. Emitidas</div><div class="mv" id="m_uem" style="color:#059669">0</div></div>
|
| 362 |
<div class="mc" style="border-left-color:#10b981"><div class="ml">Total Itens</div><div class="mv" id="m_itot" style="color:#10b981">0</div></div>
|
| 363 |
</div>
|
| 364 |
-
|
| 365 |
<div class="div div-rec"><span class="div-lbl">📦 RECEBIMENTO</span></div>
|
| 366 |
<div class="crow crow4">
|
| 367 |
<div class="cb" id="r1_0"></div><div class="cb" id="r1_1"></div>
|
|
@@ -371,7 +414,6 @@ html,body{{width:100%;height:100%;overflow:hidden;
|
|
| 371 |
<div class="cb" id="r2_0"></div><div class="cb" id="r2_1"></div>
|
| 372 |
<div class="cb" id="r2_2"></div><div class="cb" id="r2_3"></div>
|
| 373 |
</div>
|
| 374 |
-
|
| 375 |
<div class="div div-exp"><span class="div-lbl">🚚 EXPEDIÇÃO</span></div>
|
| 376 |
<div class="crow crow3">
|
| 377 |
<div class="cb" id="e1_0"></div><div class="cb" id="e1_1"></div><div class="cb" id="e1_2"></div>
|
|
@@ -380,403 +422,411 @@ html,body{{width:100%;height:100%;overflow:hidden;
|
|
| 380 |
<div class="cb" id="es_0"></div><div class="cb" id="es_1"></div><div class="cb" id="es_2"></div>
|
| 381 |
</div>
|
| 382 |
</div>
|
| 383 |
-
|
| 384 |
<div id="countdown-badge">⏸ Reiniciando em <span id="cd-secs">60</span>s</div>
|
| 385 |
|
| 386 |
<script>
|
| 387 |
-
var FRAMES
|
| 388 |
-
var MESES
|
| 389 |
-
|
| 390 |
-
var
|
| 391 |
-
var
|
| 392 |
-
var
|
| 393 |
-
var
|
| 394 |
-
|
| 395 |
-
var
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
var
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 411 |
var el = document.getElementById(id);
|
| 412 |
if (!el) return [200,150];
|
| 413 |
-
return [el.clientWidth
|
| 414 |
-
}
|
| 415 |
|
| 416 |
var ticktext = Object.values(MESES);
|
| 417 |
var tickvals = Object.keys(MESES).map(Number);
|
| 418 |
|
| 419 |
-
function fmtNum(v) {
|
| 420 |
var n = parseFloat(v);
|
| 421 |
if (isNaN(n)) return v+'';
|
| 422 |
-
|
| 423 |
-
return
|
| 424 |
-
}
|
| 425 |
-
|
| 426 |
-
function
|
| 427 |
-
var
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 431 |
var data = [], anns = [];
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
|
| 438 |
-
|
| 439 |
-
marker:{{size:6,color:t.line_color,line:{{width:1.5,color:'white'}}}},
|
| 440 |
cliponaxis:false, showlegend:false, name:t.ano
|
| 441 |
-
}
|
| 442 |
-
|
| 443 |
-
|
|
|
|
|
|
|
|
|
|
| 444 |
showarrow:false, xanchor:'left', yanchor:'top',
|
| 445 |
-
font:{
|
| 446 |
-
text:'<b>■ '+t.ano+'</b> '+t.total
|
| 447 |
-
}
|
| 448 |
-
|
| 449 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 450 |
data: data,
|
| 451 |
-
layout: {
|
| 452 |
width:wh[0], height:wh[1], autosize:false,
|
| 453 |
-
margin:{
|
| 454 |
-
paper_bgcolor:'white', plot_bgcolor:
|
| 455 |
showlegend:false, annotations:anns,
|
| 456 |
-
xaxis:{
|
| 457 |
-
|
| 458 |
-
|
| 459 |
-
|
| 460 |
-
|
| 461 |
-
|
| 462 |
-
}
|
| 463 |
-
|
| 464 |
-
|
| 465 |
-
|
| 466 |
-
|
| 467 |
-
|
| 468 |
-
|
| 469 |
-
|
| 470 |
-
|
| 471 |
-
|
| 472 |
-
|
| 473 |
-
|
| 474 |
-
|
| 475 |
-
|
| 476 |
-
|
| 477 |
-
|
| 478 |
-
|
| 479 |
-
|
| 480 |
-
|
| 481 |
-
|
| 482 |
-
|
| 483 |
-
|
| 484 |
-
|
| 485 |
-
|
| 486 |
-
|
| 487 |
-
|
| 488 |
-
|
| 489 |
-
|
| 490 |
-
|
| 491 |
-
|
| 492 |
-
|
| 493 |
-
|
| 494 |
-
|
| 495 |
-
|
| 496 |
-
|
| 497 |
-
|
| 498 |
-
|
| 499 |
-
|
| 500 |
-
|
| 501 |
-
|
| 502 |
-
|
| 503 |
-
|
| 504 |
-
|
| 505 |
-
|
| 506 |
-
|
| 507 |
-
|
| 508 |
-
|
| 509 |
-
|
| 510 |
-
|
| 511 |
-
|
| 512 |
-
|
| 513 |
-
|
| 514 |
-
|
| 515 |
-
|
| 516 |
-
|
| 517 |
-
|
| 518 |
-
|
| 519 |
-
|
| 520 |
-
|
| 521 |
-
|
| 522 |
-
|
| 523 |
-
|
| 524 |
-
|
| 525 |
-
|
| 526 |
-
|
| 527 |
-
|
| 528 |
-
|
| 529 |
-
|
| 530 |
-
|
| 531 |
-
|
| 532 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 533 |
var thresh = maxVal * 0.15;
|
| 534 |
-
var anns
|
| 535 |
-
var
|
| 536 |
-
return {
|
| 537 |
-
x:
|
| 538 |
y: b.label,
|
| 539 |
xref:'x', yref:'y', showarrow:false,
|
| 540 |
-
xanchor:
|
| 541 |
-
yanchor:'middle',
|
| 542 |
text:'<b>'+fmtNum(b.value)+'</b>',
|
| 543 |
-
font:{
|
| 544 |
-
}
|
| 545 |
-
}
|
| 546 |
-
return {
|
| 547 |
-
data:[{
|
| 548 |
type:'bar', orientation:'h',
|
| 549 |
-
x:
|
| 550 |
-
y:
|
| 551 |
-
text:
|
| 552 |
-
marker:{{
|
| 553 |
-
|
| 554 |
-
|
| 555 |
-
line:{{color:'rgba(255,255,255,0)',width:0}}
|
| 556 |
-
}}
|
| 557 |
-
}}],
|
| 558 |
-
layout:{{
|
| 559 |
width:wh[0], height:wh[1], autosize:false,
|
| 560 |
-
margin:{
|
| 561 |
paper_bgcolor:'white', plot_bgcolor:'rgba(248,252,250,0.6)',
|
| 562 |
-
showlegend:false, bargap:
|
| 563 |
-
yaxis:{
|
| 564 |
-
|
| 565 |
-
|
| 566 |
-
|
| 567 |
-
}
|
| 568 |
-
|
| 569 |
-
}
|
| 570 |
-
}
|
| 571 |
-
|
| 572 |
-
function makeBarV(bars, id) {
|
| 573 |
-
var wh
|
| 574 |
-
var
|
| 575 |
-
var isRec
|
| 576 |
-
var
|
| 577 |
-
var anns
|
| 578 |
-
return {
|
| 579 |
-
|
| 580 |
-
xref:'x', yref:'y', showarrow:false,
|
| 581 |
-
xanchor:'center', yanchor:'bottom', yshift:4,
|
| 582 |
text:'<b>'+fmtNum(b.value)+'</b>',
|
| 583 |
-
font:{
|
| 584 |
-
|
| 585 |
-
|
| 586 |
-
|
| 587 |
-
data:[{{
|
| 588 |
type:'bar',
|
| 589 |
-
x:
|
| 590 |
-
y:
|
| 591 |
-
text:
|
| 592 |
-
marker:{{
|
| 593 |
-
|
| 594 |
-
|
| 595 |
-
line:{{color:'rgba(255,255,255,0)',width:0}}
|
| 596 |
-
}}
|
| 597 |
-
}}],
|
| 598 |
-
layout:{{
|
| 599 |
width:wh[0], height:wh[1], autosize:false,
|
| 600 |
-
margin:{
|
| 601 |
paper_bgcolor:'white', plot_bgcolor:'rgba(248,252,250,0.6)',
|
| 602 |
-
showlegend:false, bargap:
|
| 603 |
-
xaxis:{
|
| 604 |
-
|
| 605 |
-
|
| 606 |
-
|
| 607 |
-
}
|
| 608 |
-
|
| 609 |
-
}
|
| 610 |
-
}
|
| 611 |
-
|
| 612 |
-
function addTitle(layout, title, isRec) {{
|
| 613 |
-
var titleColor = isRec ? '#003a70' : '#064e3b';
|
| 614 |
-
layout.title = {{
|
| 615 |
-
text: '<b>'+title+'</b>',
|
| 616 |
-
font:{{family:'Sora',size:11,color:titleColor}},
|
| 617 |
-
x:0.5, xanchor:'center'
|
| 618 |
-
}};
|
| 619 |
-
return layout;
|
| 620 |
-
}}
|
| 621 |
|
| 622 |
-
|
| 623 |
-
|
| 624 |
-
|
| 625 |
-
|
| 626 |
-
|
| 627 |
-
|
| 628 |
-
|
| 629 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 630 |
|
| 631 |
-
function
|
| 632 |
-
|
| 633 |
-
var data = [], anns = [];
|
| 634 |
-
var yshifts = [18, 32, 46, 60];
|
| 635 |
-
traces.forEach(function(t,i) {{
|
| 636 |
-
data.push({{
|
| 637 |
-
type:'scatter', mode:'lines+markers',
|
| 638 |
-
x: t.x, y: t.y,
|
| 639 |
-
line:{{width:2.2,color:t.line_color,shape:'spline'}},
|
| 640 |
-
marker:{{size:6,color:t.line_color,line:{{width:1.5,color:'white'}}}},
|
| 641 |
-
cliponaxis:false, showlegend:false, name:t.ano
|
| 642 |
-
}});
|
| 643 |
-
anns.push({{
|
| 644 |
-
x: i * 0.28, y: 1.13, xref:'paper', yref:'paper',
|
| 645 |
-
showarrow:false, xanchor:'left', yanchor:'top',
|
| 646 |
-
font:{{size:9,family:'DM Sans',color:t.line_color}},
|
| 647 |
-
text:'<b>■ '+t.ano+'</b> '+t.total
|
| 648 |
-
}});
|
| 649 |
-
var shift = yshifts[i % yshifts.length];
|
| 650 |
-
t.x.forEach(function(mx, j) {{
|
| 651 |
-
if (!t.text[j]) return;
|
| 652 |
-
anns.push({{
|
| 653 |
-
x:mx, y:t.y[j], xref:'x', yref:'y',
|
| 654 |
-
showarrow:false, yshift:shift,
|
| 655 |
-
font:{{size:8,family:'DM Sans',color:'#1a1a1a'}},
|
| 656 |
-
bgcolor:'rgba(0,0,0,0)',
|
| 657 |
-
xanchor:'center', yanchor:'bottom',
|
| 658 |
-
text:t.text[j]
|
| 659 |
-
}});
|
| 660 |
-
}});
|
| 661 |
-
}});
|
| 662 |
-
return {{
|
| 663 |
-
data: data,
|
| 664 |
-
layout: {{
|
| 665 |
-
width:wh[0], height:wh[1], autosize:false,
|
| 666 |
-
margin:{{l:38,r:12,t:52,b:36}},
|
| 667 |
-
paper_bgcolor:'white', plot_bgcolor:'rgba(240,250,246,0.4)',
|
| 668 |
-
showlegend:false, annotations:anns,
|
| 669 |
-
xaxis:{{tickmode:'array',tickvals:tickvals,ticktext:ticktext,
|
| 670 |
-
tickfont:{{size:8,family:'DM Sans'}},
|
| 671 |
-
gridcolor:'rgba(16,185,129,0.10)',zeroline:false,
|
| 672 |
-
ticklabelstandoff:8}},
|
| 673 |
-
yaxis:{{tickfont:{{size:8,family:'DM Sans'}},rangemode:'tozero',
|
| 674 |
-
gridcolor:'rgba(16,185,129,0.10)',zeroline:false}},
|
| 675 |
-
}}
|
| 676 |
-
}};
|
| 677 |
-
}}
|
| 678 |
-
|
| 679 |
-
function renderFrame(f) {{
|
| 680 |
-
Object.keys(METRIC_IDS).forEach(function(k) {{
|
| 681 |
var el = document.getElementById(METRIC_IDS[k]);
|
| 682 |
-
if (el && f.metrics[k] !== undefined)
|
| 683 |
-
|
| 684 |
-
}}
|
| 685 |
-
}});
|
| 686 |
|
| 687 |
-
|
| 688 |
-
slots.forEach(function(id) {{
|
| 689 |
var box = document.getElementById(id);
|
| 690 |
if (!box) return;
|
| 691 |
-
var d
|
| 692 |
-
var spec;
|
| 693 |
var isRec = REC_SLOTS.indexOf(id) >= 0;
|
| 694 |
-
var
|
| 695 |
-
|
| 696 |
-
if (LINE_SLOTS.indexOf(id) >= 0) {
|
| 697 |
-
spec =
|
| 698 |
-
}
|
| 699 |
-
|
| 700 |
-
else if (BARH_SLOTS.indexOf(id) >= 0)
|
| 701 |
-
|
|
|
|
|
|
|
|
|
|
| 702 |
if (!spec) return;
|
| 703 |
|
| 704 |
-
addTitle(spec.layout,
|
| 705 |
var isBar = (BARH_SLOTS.indexOf(id)>=0 || BARV_SLOTS.indexOf(id)>=0);
|
| 706 |
-
var orient = spec.
|
| 707 |
|
| 708 |
-
if (!
|
| 709 |
-
Plotly.newPlot(box, spec.data, spec.layout, {
|
| 710 |
-
|
| 711 |
-
}
|
| 712 |
Plotly.react(box, spec.data, spec.layout);
|
| 713 |
-
}
|
| 714 |
-
if (isBar)
|
| 715 |
-
}
|
| 716 |
-
}
|
| 717 |
|
| 718 |
-
function showCountdown(
|
| 719 |
badge.classList.add('visible');
|
| 720 |
-
var
|
| 721 |
-
cdSecs.textContent =
|
| 722 |
-
|
| 723 |
-
|
| 724 |
-
cdSecs.textContent =
|
| 725 |
-
if (
|
| 726 |
-
clearInterval(
|
| 727 |
badge.classList.remove('visible');
|
| 728 |
frameIdx = 0;
|
| 729 |
startAnimation();
|
| 730 |
-
}
|
| 731 |
-
}
|
| 732 |
-
}
|
| 733 |
|
| 734 |
-
function stepFrame() {
|
| 735 |
renderFrame(FRAMES[frameIdx]);
|
| 736 |
frameIdx++;
|
| 737 |
-
if (frameIdx >= FRAMES.length) {
|
| 738 |
clearTimeout(animTimer);
|
| 739 |
showCountdown(PAUSE_AFTER_MS / 1000);
|
| 740 |
-
}
|
| 741 |
animTimer = setTimeout(stepFrame, FRAME_INTERVAL_MS);
|
| 742 |
-
}
|
| 743 |
-
}
|
| 744 |
|
| 745 |
-
function startAnimation() {
|
| 746 |
clearTimeout(animTimer);
|
| 747 |
-
clearInterval(
|
| 748 |
animTimer = setTimeout(stepFrame, FRAME_INTERVAL_MS);
|
| 749 |
-
}
|
| 750 |
|
| 751 |
-
window.addEventListener('load', function() {
|
| 752 |
-
badge
|
| 753 |
-
cdSecs
|
| 754 |
-
setTimeout(function()
|
| 755 |
-
|
| 756 |
-
startAnimation();
|
| 757 |
-
}}, 300);
|
| 758 |
-
}});
|
| 759 |
|
| 760 |
var _rt;
|
| 761 |
-
window.addEventListener('resize', function() {
|
| 762 |
clearTimeout(_rt);
|
| 763 |
-
_rt = setTimeout(function() {
|
| 764 |
-
document.querySelectorAll('.cb').forEach(function(box) {
|
| 765 |
-
if (box.data) {
|
| 766 |
-
|
| 767 |
-
|
| 768 |
-
|
| 769 |
-
}}, 200);
|
| 770 |
-
}});
|
| 771 |
</script>
|
| 772 |
-
</body>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 773 |
|
| 774 |
components.html(html, height=8000, scrolling=False)
|
| 775 |
|
| 776 |
else:
|
| 777 |
-
components.html("""<!DOCTYPE html><html><body style=
|
| 778 |
-
|
| 779 |
-
<
|
|
|
|
|
|
|
| 780 |
|
| 781 |
time.sleep(60)
|
| 782 |
-
st.rerun()
|
|
|
|
| 8 |
|
| 9 |
st.set_page_config(layout="wide", initial_sidebar_state="collapsed", page_title="PMJA - Dashboard")
|
| 10 |
|
| 11 |
+
st.markdown("""<style>
|
| 12 |
+
#MainMenu,footer,header,[data-testid="stToolbar"],[data-testid="stDecoration"],
|
| 13 |
+
[data-testid="stStatusWidget"]{display:none!important}
|
| 14 |
+
.block-container,.element-container,.stMarkdown{padding:0!important;margin:0!important;max-width:100%!important}
|
| 15 |
+
iframe{position:fixed!important;top:0!important;left:0!important;
|
| 16 |
+
width:100vw!important;height:100dvh!important;border:none!important;z-index:9999!important}
|
| 17 |
+
[data-testid="stHorizontalBlock"]{gap:0!important;margin:0!important;padding:0!important;
|
| 18 |
+
height:0!important;overflow:visible!important}
|
| 19 |
+
.main,[data-testid="stAppViewContainer"]{background:#f0faf6!important}
|
| 20 |
+
@media(max-width:900px){
|
| 21 |
+
iframe{position:static!important;width:100%!important;height:7500px!important;z-index:1!important}
|
| 22 |
+
.main,[data-testid="stAppViewContainer"]{overflow-y:auto!important;height:auto!important}
|
| 23 |
+
.block-container{height:auto!important}
|
| 24 |
+
}
|
| 25 |
+
</style>""", unsafe_allow_html=True)
|
| 26 |
+
|
| 27 |
# ── SESSION ───────────────────────────────────────────────────────────
|
| 28 |
_cc = CookieController()
|
| 29 |
COOKIE_NAME = "pmja_session"
|
|
|
|
| 54 |
st.session_state.inicio_exibicao = time.time()
|
| 55 |
st.switch_page("pages/atual.py")
|
| 56 |
|
| 57 |
+
# ── LOADING ───────────────────────────────────────────────────────────
|
| 58 |
+
loading_placeholder = st.empty()
|
| 59 |
+
loading_placeholder.markdown("""
|
| 60 |
+
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;700;800&family=Sora:wght@800&display=swap" rel="stylesheet">
|
| 61 |
+
<style>
|
| 62 |
+
#ld{position:fixed;inset:0;z-index:999999;
|
| 63 |
+
background:linear-gradient(135deg,#003a70 0%,#0056A3 50%,#064e3b 100%);
|
| 64 |
+
display:flex;flex-direction:column;align-items:center;justify-content:center;font-family:'DM Sans',sans-serif}
|
| 65 |
+
#ld .lg{display:flex;align-items:center;gap:24px;margin-bottom:36px}
|
| 66 |
+
#ld img{height:34px;filter:brightness(0) invert(1);opacity:.9}
|
| 67 |
+
#ld .dv{width:1px;height:32px;background:rgba(255,255,255,.25)}
|
| 68 |
+
#ld h2{font-family:'Sora',sans-serif;font-size:clamp(15px,4vw,21px);color:#fff;font-weight:800;margin-bottom:4px;text-align:center}
|
| 69 |
+
#ld p{font-size:clamp(11px,3vw,13px);color:rgba(255,255,255,.7);font-weight:600;margin-bottom:40px;text-align:center}
|
| 70 |
+
#ld .sp{width:32px;height:32px;border:3px solid rgba(255,255,255,.2);border-top-color:#fff;
|
| 71 |
+
border-radius:50%;animation:sp .8s linear infinite;margin-bottom:24px}
|
| 72 |
+
@keyframes sp{to{transform:rotate(360deg)}}
|
| 73 |
+
#ld .bw{width:min(260px,70vw);height:4px;background:rgba(255,255,255,.15);border-radius:99px;overflow:hidden;margin-bottom:14px}
|
| 74 |
+
#ld .bf{height:100%;width:0%;background:#fff;border-radius:99px;transition:width .4s ease}
|
| 75 |
+
#ld .st{font-size:11px;color:rgba(255,255,255,.6);font-weight:600;text-align:center}
|
| 76 |
+
#ld .dt span{animation:dt 1.2s infinite;opacity:0}
|
| 77 |
+
#ld .dt span:nth-child(2){animation-delay:.2s}
|
| 78 |
+
#ld .dt span:nth-child(3){animation-delay:.4s}
|
| 79 |
+
@keyframes dt{0%,100%{opacity:0}50%{opacity:1}}
|
| 80 |
+
</style>
|
| 81 |
+
<div id="ld">
|
| 82 |
+
<div class="lg">
|
| 83 |
+
<img src="https://companieslogo.com/img/orig/AZ2.F-d26946db.png?t=1720244490">
|
| 84 |
+
<div class="dv"></div>
|
| 85 |
+
<img src="https://upload.wikimedia.org/wikipedia/commons/8/8f/Logo-engie.svg">
|
| 86 |
+
</div>
|
| 87 |
+
<div class="sp"></div>
|
| 88 |
+
<h2>PMJA — Dashboard Gestão de Materiais</h2>
|
| 89 |
+
<p>Visão Completa · Recebimento & Expedição</p>
|
| 90 |
+
<div class="bw"><div class="bf" id="ldbf"></div></div>
|
| 91 |
+
<div class="st" id="ldst">Conectando ao Google Sheets<span class="dt"><span>.</span><span>.</span><span>.</span></span></div>
|
| 92 |
+
</div>
|
| 93 |
+
<script>
|
| 94 |
+
(function(){
|
| 95 |
+
var s=[{p:15,m:'Conectando ao Google Sheets'},{p:35,m:'Carregando dados de recebimento'},
|
| 96 |
+
{p:55,m:'Carregando dados de expedição'},{p:72,m:'Processando métricas'},
|
| 97 |
+
{p:88,m:'Montando gráficos'},{p:96,m:'Quase pronto'}];
|
| 98 |
+
var i=0;
|
| 99 |
+
function n(){
|
| 100 |
+
if(i>=s.length)return;
|
| 101 |
+
var x=s[i++];
|
| 102 |
+
var b=document.getElementById('ldbf'),t=document.getElementById('ldst');
|
| 103 |
+
if(b)b.style.width=x.p+'%';
|
| 104 |
+
if(t)t.innerHTML=x.m+'<span class="dt"><span>.</span><span>.</span><span>.</span></span>';
|
| 105 |
+
setTimeout(n,950+Math.random()*550);
|
| 106 |
+
}
|
| 107 |
+
setTimeout(n,300);
|
| 108 |
+
})();
|
| 109 |
+
</script>
|
| 110 |
+
""", unsafe_allow_html=True)
|
| 111 |
+
|
| 112 |
+
# ── LOAD DATA ─────────────────────────────────────────────────────────
|
| 113 |
+
df_recebimento = read_ws("recebimento")
|
| 114 |
+
df_rec_dados = read_ws("rec_dados")
|
| 115 |
+
df_exp_raw = read_ws("exp_dados")
|
| 116 |
+
loading_placeholder.empty()
|
| 117 |
|
| 118 |
# ── HELPERS ───────────────────────────────────────────────────────────
|
| 119 |
def fmt(n):
|
| 120 |
+
return "0" if pd.isna(n) else "{:,}".format(int(n)).replace(',','.')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 121 |
|
| 122 |
def proc_exp(df_raw):
|
| 123 |
if df_raw is None or df_raw.empty: return None, None
|
|
|
|
| 167 |
COR_SIS_EXP = ['#064e3b','#065f46','#047857','#059669','#10b981','#34d399',
|
| 168 |
'#4ade80','#6ee7b7','#86efac','#a7f3d0','#bbf7d0','#d1fae5','#ecfdf5','#f0fdf4']
|
| 169 |
|
|
|
|
| 170 |
def build_all_frames(df_rec, df_exp, df_rec_sis, df_exp_det, sistemas_rec,
|
| 171 |
anos_rec, anos_exp, mes_max, mes_max_rec, mes_max_exp, mes_max_sis):
|
| 172 |
frames = []
|
|
|
|
| 173 |
for mes in range(1, mes_max+1):
|
| 174 |
frame = {"mes": mes, "metrics": {}, "charts": {}}
|
|
|
|
| 175 |
for col, key, src in [
|
| 176 |
+
('qtd_descarregamentos','desc','rec'),('peso_recebido','peso','rec'),
|
| 177 |
+
('qtd_volumes_recebidos','vol','rec'),('qtd_unidades_recebidos','unid','rec'),
|
| 178 |
+
('qtd_itens_por_unidade','itens','rec'),('qtd_requisicoes','req','exp'),
|
| 179 |
+
('qtd_unidades_emitidas','uem','exp'),('qtd_itens_total','itot','exp'),
|
|
|
|
|
|
|
|
|
|
|
|
|
| 180 |
]:
|
| 181 |
if src=='rec':
|
| 182 |
v = df_rec[df_rec['mes']<=mes][col].sum() if mes<=mes_max_rec else df_rec[col].sum()
|
|
|
|
| 185 |
frame["metrics"][key] = fmt(v)
|
| 186 |
|
| 187 |
def line_traces(df, col, anos, cores, lim):
|
| 188 |
+
traces = []
|
| 189 |
m = mes if mes<=lim else lim
|
| 190 |
for i,ano in enumerate(anos):
|
| 191 |
d = df[(df['ano']==ano)&(df['mes']<=m)].sort_values('mes')
|
| 192 |
if d.empty: continue
|
| 193 |
+
cor = cores[i%len(cores)]
|
| 194 |
traces.append({
|
| 195 |
+
"x":[int(x) for x in d['mes']],
|
| 196 |
+
"y":[float(y) for y in d[col]],
|
| 197 |
+
"text":[fmt(v) if v>0 else "" for v in d[col]],
|
| 198 |
+
"line_color":cor, "ano":str(ano), "total":fmt(d[col].sum())
|
| 199 |
})
|
| 200 |
return traces
|
| 201 |
|
|
|
|
| 205 |
frame["charts"]["r1_2"] = line_traces(df_rec,'qtd_volumes_recebidos',anos_rec,COR_REC,mes_max_rec)
|
| 206 |
|
| 207 |
if df_rec_sis is not None and mes<=mes_max_sis and sistemas_rec:
|
| 208 |
+
cols_map = {}
|
| 209 |
+
for col in df_rec_sis.columns:
|
| 210 |
+
for s in sistemas_rec:
|
| 211 |
+
if 'Volumes' in col and not any(e in col for e in ['Unid.','unidade']) and s in col:
|
| 212 |
+
cols_map[col] = s
|
| 213 |
slices = []
|
| 214 |
for i,s in enumerate(sistemas_rec):
|
| 215 |
col = next((c for c,v in cols_map.items() if v==s), None)
|
| 216 |
if col and col in df_rec_sis.columns:
|
| 217 |
+
t = float(pd.to_numeric(df_rec_sis.loc[df_rec_sis['mes']<=mes,col],errors='coerce').fillna(0).sum())
|
| 218 |
if t>0: slices.append({"label":s,"value":t,"color":COR_SIS[i%len(COR_SIS)]})
|
| 219 |
if slices: frame["charts"]["r1_3"] = slices
|
| 220 |
|
|
|
|
| 223 |
frame["charts"]["r2_2"] = line_traces(df_rec,'qtd_itens_por_unidade',anos_rec,COR_REC,mes_max_rec)
|
| 224 |
|
| 225 |
if df_rec_sis is not None and mes<=mes_max_sis and sistemas_rec:
|
| 226 |
+
for slot, palavra, excluir in [('r2_1','Unid. Itens',['por unidade']),('r2_3','Itens por unidade',['Unid. Itens'])]:
|
| 227 |
+
cols_map2 = {}
|
| 228 |
+
for col in df_rec_sis.columns:
|
| 229 |
+
for s in sistemas_rec:
|
| 230 |
+
if palavra in col and not any(e in col for e in excluir) and s in col:
|
| 231 |
+
cols_map2[col] = s
|
|
|
|
| 232 |
bars = []
|
| 233 |
for i,s in enumerate(sistemas_rec):
|
| 234 |
col = next((c for c,v in cols_map2.items() if v==s), None)
|
| 235 |
if col and col in df_rec_sis.columns:
|
| 236 |
+
t = float(pd.to_numeric(df_rec_sis.loc[df_rec_sis['mes']<=mes,col],errors='coerce').fillna(0).sum())
|
| 237 |
if t>0: bars.append({"label":s,"value":t,"color":COR_SIS[i%len(COR_SIS)]})
|
| 238 |
if bars: frame["charts"][slot] = bars
|
| 239 |
|
|
|
|
| 243 |
frame["charts"]["e1_2"] = line_traces(df_exp,'qtd_itens_total',anos_exp,COR_EXP,mes_max_exp)
|
| 244 |
|
| 245 |
if df_exp_det is not None and mes<=mes_max_exp:
|
| 246 |
+
det = df_exp_det[df_exp_det['mes_num']<=mes].copy()
|
| 247 |
+
req_c = [c for c in df_exp_det.columns if 'requisi' in c.lower() and 'unid' not in c.lower() and 'por' not in c.lower()]
|
| 248 |
+
uni_c = [c for c in df_exp_det.columns if 'unid' in c.lower() and 'iten' in c.lower().replace('.','') and 'por' not in c.lower()]
|
| 249 |
+
it_c = [c for c in df_exp_det.columns if 'iten' in c.lower().replace('.','') and 'por' in c.lower() and 'unid' in c.lower()]
|
| 250 |
+
for slot, cols in [('es_0',req_c),('es_1',uni_c),('es_2',it_c)]:
|
| 251 |
if not cols: continue
|
| 252 |
tots = {}
|
| 253 |
for col in cols:
|
| 254 |
cat = ext_cat(col)
|
| 255 |
if cat not in tots: tots[cat]={'v':0.0,'c':COR_SIS_EXP[len(tots)%len(COR_SIS_EXP)]}
|
| 256 |
+
det.loc[:,col] = pd.to_numeric(det[col],errors='coerce').fillna(0)
|
| 257 |
tots[cat]['v'] += float(det[col].sum())
|
| 258 |
+
dados = sorted([(k,v) for k,v in tots.items() if v['v']>0],key=lambda x:x[1]['v'],reverse=True)
|
| 259 |
+
if dados: frame["charts"][slot] = [{"label":k,"value":v['v'],"color":v['c']} for k,v in dados]
|
|
|
|
| 260 |
|
| 261 |
frames.append(frame)
|
| 262 |
return frames
|
| 263 |
|
| 264 |
+
# ── PROCESS DATA ──────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 265 |
df_exp_raw = df_exp_raw if df_exp_raw is not None and not df_exp_raw.empty else None
|
| 266 |
df_recebimento = df_recebimento if df_recebimento is not None and not df_recebimento.empty else None
|
| 267 |
df_rec_dados = df_rec_dados if df_rec_dados is not None and not df_rec_dados.empty else None
|
|
|
|
| 276 |
'peso_recebido','qtd_unidades_recebidos','qtd_itens_por_unidade']
|
| 277 |
df_rec['data'] = pd.to_datetime(df_rec['mes_ano'], format='%m/%Y', errors='coerce')
|
| 278 |
df_rec = df_rec.dropna(subset=['data']).sort_values('data')
|
| 279 |
+
df_rec['ano'] = df_rec['data'].dt.year; df_rec['mes'] = df_rec['data'].dt.month
|
|
|
|
| 280 |
for col in ['qtd_descarregamentos','qtd_volumes_recebidos','peso_recebido',
|
| 281 |
'qtd_unidades_recebidos','qtd_itens_por_unidade']:
|
| 282 |
if col in df_rec.columns:
|
|
|
|
| 301 |
sistemas_rec = get_sistemas(df_rec_sis)
|
| 302 |
mes_max_sis = int(df_rec_sis['mes'].max())
|
| 303 |
|
|
|
|
| 304 |
if df_rec is not None and df_exp is not None and mes_max_rec>0 and mes_max_exp>0:
|
| 305 |
anos_rec = [int(a) for a in sorted(df_rec['ano'].unique())]
|
| 306 |
anos_exp = [int(a) for a in sorted(df_exp['ano'].unique())]
|
| 307 |
mes_max = max(mes_max_rec, mes_max_exp, mes_max_sis if mes_max_sis>0 else 0)
|
| 308 |
|
| 309 |
+
frames = build_all_frames(df_rec, df_exp, df_rec_sis, df_exp_det, sistemas_rec,
|
| 310 |
+
anos_rec, anos_exp, mes_max, mes_max_rec, mes_max_exp, mes_max_sis)
|
| 311 |
+
|
|
|
|
| 312 |
frames_json = json.dumps(frames, ensure_ascii=False)
|
| 313 |
meses_json = json.dumps(MESES)
|
| 314 |
|
| 315 |
+
html = """<!DOCTYPE html>
|
| 316 |
+
<html>
|
| 317 |
+
<head>
|
| 318 |
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
| 319 |
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;600;700;800&family=Sora:wght@700;800;900&display=swap" rel="stylesheet">
|
| 320 |
<script src="https://cdn.plot.ly/plotly-2.27.0.min.js" charset="utf-8"></script>
|
| 321 |
<style>
|
| 322 |
+
* { margin:0; padding:0; box-sizing:border-box; }
|
| 323 |
+
html, body { width:100%; height:100%; overflow:hidden;
|
| 324 |
background:linear-gradient(160deg,#f0faf6 0%,#e6f7f1 55%,#d1fae5 100%);
|
| 325 |
+
font-family:'DM Sans',sans-serif; }
|
| 326 |
+
::-webkit-scrollbar { width:4px; }
|
| 327 |
+
::-webkit-scrollbar-thumb { background:linear-gradient(#047857,#10b981); border-radius:10px; }
|
| 328 |
+
.root { display:grid; width:100%; height:100dvh; gap:2px; padding:3px 4px 2px; overflow:hidden;
|
| 329 |
+
grid-template-rows:auto auto auto 1fr 1fr auto 1fr 1fr; }
|
| 330 |
+
.hdr { background:linear-gradient(135deg,#003a70 0%,#0056A3 50%,#064e3b 100%);
|
| 331 |
+
border-radius:6px; padding:5px 14px; display:flex; align-items:center;
|
| 332 |
+
justify-content:space-between; box-shadow:0 3px 14px rgba(0,58,112,.18);
|
| 333 |
+
position:relative; overflow:hidden; }
|
| 334 |
+
.hdr::before { content:''; position:absolute; top:-50%; right:-3%; width:200px; height:200px;
|
| 335 |
+
border-radius:50%; background:radial-gradient(circle,rgba(16,185,129,.18) 0%,transparent 70%);
|
| 336 |
+
pointer-events:none; }
|
| 337 |
+
.logo { height:clamp(20px,3.2dvh,36px); filter:brightness(0) invert(1); opacity:.92; }
|
| 338 |
+
.ht { font-family:'Sora',sans-serif; font-size:clamp(12px,1.9dvh,20px);
|
| 339 |
+
color:#fff; font-weight:800; letter-spacing:-.3px; }
|
| 340 |
+
.hs { font-family:'DM Sans',sans-serif; font-size:clamp(9px,1.2dvh,13px);
|
| 341 |
+
color:rgba(255,255,255,.82); font-weight:600; }
|
| 342 |
+
.mrow { display:grid; grid-template-columns:repeat(8,1fr); gap:3px; }
|
| 343 |
+
.mc { background:#fff; border-radius:5px;
|
| 344 |
+
padding:clamp(3px,.5dvh,7px) clamp(4px,.5vw,10px);
|
| 345 |
+
border-left:3px solid; box-shadow:0 1px 6px rgba(4,120,87,.07); overflow:hidden; }
|
| 346 |
+
.ml { font-family:'DM Sans',sans-serif; font-size:clamp(6px,.85dvh,10px);
|
| 347 |
+
color:#6b7280; font-weight:700; text-transform:uppercase;
|
| 348 |
+
letter-spacing:.4px; white-space:nowrap; }
|
| 349 |
+
.mv { font-family:'Sora',sans-serif; font-size:clamp(11px,1.9dvh,22px);
|
| 350 |
+
font-weight:800; line-height:1.1; letter-spacing:-.4px;
|
| 351 |
+
white-space:nowrap; transition:all .3s; }
|
| 352 |
+
.div { border-radius:5px; padding:clamp(2px,.35dvh,5px) 10px;
|
| 353 |
+
display:flex; align-items:center; justify-content:center; }
|
| 354 |
+
.div-rec { background:linear-gradient(135deg,#003a70 0%,#0075be 100%);
|
| 355 |
+
box-shadow:0 2px 8px rgba(0,86,163,.15); }
|
| 356 |
+
.div-exp { background:linear-gradient(135deg,#064e3b 0%,#10b981 100%);
|
| 357 |
+
box-shadow:0 2px 8px rgba(6,78,59,.18); }
|
| 358 |
+
.div-lbl { font-family:'Sora',sans-serif; font-size:clamp(9px,1.3dvh,15px);
|
| 359 |
+
color:#fff; font-weight:800; letter-spacing:.5px; }
|
| 360 |
+
.crow { display:grid; gap:3px; min-height:0; }
|
| 361 |
+
.crow4 { grid-template-columns:repeat(4,1fr); }
|
| 362 |
+
.crow3 { grid-template-columns:repeat(3,1fr); }
|
| 363 |
+
.cb { background:#fff; border-radius:6px; border:1px solid rgba(16,185,129,.12);
|
| 364 |
+
box-shadow:0 2px 8px rgba(4,120,87,.07); overflow:hidden; min-height:0; position:relative; }
|
| 365 |
+
#countdown-badge { position:fixed; bottom:10px; right:14px; z-index:99999;
|
| 366 |
+
background:rgba(6,78,59,0.82); color:#fff; font-family:'DM Sans',sans-serif;
|
| 367 |
+
font-size:11px; font-weight:700; padding:4px 10px; border-radius:20px;
|
| 368 |
+
letter-spacing:.4px; opacity:0; transition:opacity .4s; pointer-events:none; }
|
| 369 |
+
#countdown-badge.visible { opacity:1; }
|
| 370 |
+
@media(max-width:900px){
|
| 371 |
+
html,body { overflow-y:auto!important; height:auto!important; }
|
| 372 |
+
.root { height:auto!important; overflow:visible!important; grid-template-rows:auto!important; }
|
| 373 |
+
.mrow { grid-template-columns:repeat(4,1fr)!important; }
|
| 374 |
+
.crow4 { grid-template-columns:repeat(2,1fr)!important; min-height:200px; }
|
| 375 |
+
.crow3 { grid-template-columns:repeat(2,1fr)!important; min-height:200px; }
|
| 376 |
+
.cb { min-height:220px!important; }
|
| 377 |
+
}
|
| 378 |
+
@media(max-width:600px){
|
| 379 |
+
.mrow { grid-template-columns:repeat(2,1fr)!important; }
|
| 380 |
+
.crow4,.crow3 { grid-template-columns:1fr!important; }
|
| 381 |
+
.cb { min-height:200px!important; }
|
| 382 |
+
.hs { display:none; }
|
| 383 |
+
.hdr { padding:4px 8px!important; }
|
| 384 |
+
.div-lbl { font-size:12px!important; }
|
| 385 |
+
}
|
| 386 |
</style>
|
| 387 |
+
</head>
|
| 388 |
+
<body>
|
| 389 |
<div class="root">
|
| 390 |
<div class="hdr">
|
| 391 |
<img class="logo" src="https://companieslogo.com/img/orig/AZ2.F-d26946db.png?t=1720244490" alt="AZ">
|
|
|
|
| 395 |
</div>
|
| 396 |
<img class="logo" src="https://upload.wikimedia.org/wikipedia/commons/8/8f/Logo-engie.svg" alt="Engie">
|
| 397 |
</div>
|
|
|
|
| 398 |
<div class="mrow">
|
| 399 |
<div class="mc" style="border-left-color:#003a70"><div class="ml">Descarregamentos</div><div class="mv" id="m_desc" style="color:#003a70">0</div></div>
|
| 400 |
<div class="mc" style="border-left-color:#005a9c"><div class="ml">Peso Recebido</div><div class="mv" id="m_peso" style="color:#005a9c">0 kg</div></div>
|
|
|
|
| 405 |
<div class="mc" style="border-left-color:#059669"><div class="ml">Unid. Emitidas</div><div class="mv" id="m_uem" style="color:#059669">0</div></div>
|
| 406 |
<div class="mc" style="border-left-color:#10b981"><div class="ml">Total Itens</div><div class="mv" id="m_itot" style="color:#10b981">0</div></div>
|
| 407 |
</div>
|
|
|
|
| 408 |
<div class="div div-rec"><span class="div-lbl">📦 RECEBIMENTO</span></div>
|
| 409 |
<div class="crow crow4">
|
| 410 |
<div class="cb" id="r1_0"></div><div class="cb" id="r1_1"></div>
|
|
|
|
| 414 |
<div class="cb" id="r2_0"></div><div class="cb" id="r2_1"></div>
|
| 415 |
<div class="cb" id="r2_2"></div><div class="cb" id="r2_3"></div>
|
| 416 |
</div>
|
|
|
|
| 417 |
<div class="div div-exp"><span class="div-lbl">🚚 EXPEDIÇÃO</span></div>
|
| 418 |
<div class="crow crow3">
|
| 419 |
<div class="cb" id="e1_0"></div><div class="cb" id="e1_1"></div><div class="cb" id="e1_2"></div>
|
|
|
|
| 422 |
<div class="cb" id="es_0"></div><div class="cb" id="es_1"></div><div class="cb" id="es_2"></div>
|
| 423 |
</div>
|
| 424 |
</div>
|
|
|
|
| 425 |
<div id="countdown-badge">⏸ Reiniciando em <span id="cd-secs">60</span>s</div>
|
| 426 |
|
| 427 |
<script>
|
| 428 |
+
var FRAMES = __FRAMES__;
|
| 429 |
+
var MESES = __MESES__;
|
| 430 |
+
|
| 431 |
+
var REC_SLOTS = ['r1_0','r1_1','r1_2','r1_3','r2_0','r2_1','r2_2','r2_3'];
|
| 432 |
+
var LINE_SLOTS = ['r1_0','r1_1','r1_2','r2_0','r2_2','e1_0','e1_1','e1_2'];
|
| 433 |
+
var DONUT_SLOTS = ['r1_3','es_0'];
|
| 434 |
+
var BARH_SLOTS = ['r2_1','es_1'];
|
| 435 |
+
var BARV_SLOTS = ['r2_3','es_2'];
|
| 436 |
+
var EXP_LINE = ['e1_0','e1_1','e1_2'];
|
| 437 |
+
|
| 438 |
+
var METRIC_IDS = {
|
| 439 |
+
desc:'m_desc', peso:'m_peso', vol:'m_vol', unid:'m_unid', itens:'m_itens',
|
| 440 |
+
req:'m_req', uem:'m_uem', itot:'m_itot'
|
| 441 |
+
};
|
| 442 |
+
var METRIC_SUF = { peso:' kg' };
|
| 443 |
+
|
| 444 |
+
var TITLES = {
|
| 445 |
+
r1_0:'Descarregamentos', r1_1:'Peso Recebido (kg)', r1_2:'Volumes Recebidos',
|
| 446 |
+
r1_3:'Volumes por Sistema',
|
| 447 |
+
r2_0:'Unidades Recebidas', r2_1:'Unidades Itens por Sistema',
|
| 448 |
+
r2_2:'Itens por Unidade', r2_3:'Itens/Unidade por Sistema',
|
| 449 |
+
e1_0:'Requisições', e1_1:'Unidades Emitidas', e1_2:'Total de Itens',
|
| 450 |
+
es_0:'Requisições por Sistema', es_1:'Unidades Emitidas por Sistema', es_2:'Itens por Sistema'
|
| 451 |
+
};
|
| 452 |
+
|
| 453 |
+
var inited = {};
|
| 454 |
+
var FRAME_INTERVAL_MS = 600;
|
| 455 |
+
var PAUSE_AFTER_MS = 60000;
|
| 456 |
+
var frameIdx = 0;
|
| 457 |
+
var animTimer = null;
|
| 458 |
+
var cdTimer = null;
|
| 459 |
+
var badge = null;
|
| 460 |
+
var cdSecs = null;
|
| 461 |
+
|
| 462 |
+
function gsize(id) {
|
| 463 |
var el = document.getElementById(id);
|
| 464 |
if (!el) return [200,150];
|
| 465 |
+
return [el.clientWidth||200, el.clientHeight||150];
|
| 466 |
+
}
|
| 467 |
|
| 468 |
var ticktext = Object.values(MESES);
|
| 469 |
var tickvals = Object.keys(MESES).map(Number);
|
| 470 |
|
| 471 |
+
function fmtNum(v) {
|
| 472 |
var n = parseFloat(v);
|
| 473 |
if (isNaN(n)) return v+'';
|
| 474 |
+
var i = Math.round(n);
|
| 475 |
+
return i.toString().replace(/\\B(?=(\\d{3})+(?!\\d))/g,'.');
|
| 476 |
+
}
|
| 477 |
+
|
| 478 |
+
function addTitle(layout, id, isRec) {
|
| 479 |
+
var col = isRec ? '#003a70' : '#064e3b';
|
| 480 |
+
layout.title = {
|
| 481 |
+
text:'<b>'+(TITLES[id]||id)+'</b>',
|
| 482 |
+
font:{family:'Sora',size:11,color:col},
|
| 483 |
+
x:0.5, xanchor:'center'
|
| 484 |
+
};
|
| 485 |
+
}
|
| 486 |
+
|
| 487 |
+
// ── CORRECTED makeLine ────────────────────────────────────────────────
|
| 488 |
+
// Uses per-series yshift annotations (like makeLineExp) to prevent
|
| 489 |
+
// value labels from overlapping when multiple years share the same x points.
|
| 490 |
+
function makeLine(traces, id) {
|
| 491 |
+
var wh = gsize(id);
|
| 492 |
+
var isRec = REC_SLOTS.indexOf(id) >= 0;
|
| 493 |
+
var grid = isRec ? 'rgba(0,86,163,0.10)' : 'rgba(16,185,129,0.10)';
|
| 494 |
+
var bg = isRec ? 'rgba(240,246,255,0.4)' : 'rgba(240,250,246,0.4)';
|
| 495 |
+
|
| 496 |
+
// Alternate label offsets per series so they don't stack on top of each other
|
| 497 |
+
var yShifts = [16, 30, 44, 58];
|
| 498 |
+
|
| 499 |
var data = [], anns = [];
|
| 500 |
+
|
| 501 |
+
traces.forEach(function(t, i) {
|
| 502 |
+
data.push({
|
| 503 |
+
type:'scatter', mode:'lines+markers',
|
| 504 |
+
x:t.x, y:t.y,
|
| 505 |
+
line:{width:2.2, color:t.line_color, shape:'spline'},
|
| 506 |
+
marker:{size:6, color:t.line_color, line:{width:1.5, color:'white'}},
|
|
|
|
| 507 |
cliponaxis:false, showlegend:false, name:t.ano
|
| 508 |
+
});
|
| 509 |
+
|
| 510 |
+
// Legend row for this series (stacked vertically, one per line)
|
| 511 |
+
anns.push({
|
| 512 |
+
x: i * 0.28, y: 1.13,
|
| 513 |
+
xref:'paper', yref:'paper',
|
| 514 |
showarrow:false, xanchor:'left', yanchor:'top',
|
| 515 |
+
font:{size:9, family:'DM Sans', color:t.line_color},
|
| 516 |
+
text:'<b>■ ' + t.ano + '</b> ' + t.total
|
| 517 |
+
});
|
| 518 |
+
|
| 519 |
+
// Per-point value annotations with staggered yshift to avoid overlap
|
| 520 |
+
var sh = yShifts[i % yShifts.length];
|
| 521 |
+
t.x.forEach(function(mx, j) {
|
| 522 |
+
if (!t.text[j]) return;
|
| 523 |
+
anns.push({
|
| 524 |
+
x: mx, y: t.y[j],
|
| 525 |
+
xref:'x', yref:'y',
|
| 526 |
+
showarrow: false,
|
| 527 |
+
yshift: sh,
|
| 528 |
+
font:{size:8, family:'DM Sans', color: t.line_color},
|
| 529 |
+
bgcolor:'rgba(0,0,0,0)',
|
| 530 |
+
xanchor:'center', yanchor:'bottom',
|
| 531 |
+
text: '<b>' + t.text[j] + '</b>'
|
| 532 |
+
});
|
| 533 |
+
});
|
| 534 |
+
});
|
| 535 |
+
|
| 536 |
+
return {
|
| 537 |
data: data,
|
| 538 |
+
layout: {
|
| 539 |
width:wh[0], height:wh[1], autosize:false,
|
| 540 |
+
margin:{l:38, r:12, t:52, b:24},
|
| 541 |
+
paper_bgcolor:'white', plot_bgcolor:bg,
|
| 542 |
showlegend:false, annotations:anns,
|
| 543 |
+
xaxis:{
|
| 544 |
+
tickmode:'array', tickvals:tickvals, ticktext:ticktext,
|
| 545 |
+
tickfont:{size:8, family:'DM Sans'},
|
| 546 |
+
gridcolor:grid, zeroline:false
|
| 547 |
+
},
|
| 548 |
+
yaxis:{
|
| 549 |
+
tickfont:{size:8, family:'DM Sans'},
|
| 550 |
+
rangemode:'tozero',
|
| 551 |
+
gridcolor:grid, zeroline:false
|
| 552 |
+
}
|
| 553 |
+
}
|
| 554 |
+
};
|
| 555 |
+
}
|
| 556 |
+
// ─────────────────────────────────────────────────────────────────────
|
| 557 |
+
|
| 558 |
+
function makeLineExp(traces, id) {
|
| 559 |
+
var wh = gsize(id);
|
| 560 |
+
var data = [], anns = [];
|
| 561 |
+
var shifts = [18,32,46,60];
|
| 562 |
+
traces.forEach(function(t, i) {
|
| 563 |
+
data.push({
|
| 564 |
+
type:'scatter', mode:'lines+markers',
|
| 565 |
+
x:t.x, y:t.y,
|
| 566 |
+
line:{width:2.2,color:t.line_color,shape:'spline'},
|
| 567 |
+
marker:{size:6,color:t.line_color,line:{width:1.5,color:'white'}},
|
| 568 |
+
cliponaxis:false, showlegend:false, name:t.ano
|
| 569 |
+
});
|
| 570 |
+
anns.push({
|
| 571 |
+
x:i*0.28, y:1.13, xref:'paper', yref:'paper',
|
| 572 |
+
showarrow:false, xanchor:'left', yanchor:'top',
|
| 573 |
+
font:{size:9,family:'DM Sans',color:t.line_color},
|
| 574 |
+
text:'<b>■ '+t.ano+'</b> '+t.total
|
| 575 |
+
});
|
| 576 |
+
var sh = shifts[i % shifts.length];
|
| 577 |
+
t.x.forEach(function(mx,j) {
|
| 578 |
+
if (!t.text[j]) return;
|
| 579 |
+
anns.push({x:mx,y:t.y[j],xref:'x',yref:'y',showarrow:false,yshift:sh,
|
| 580 |
+
font:{size:8,family:'DM Sans',color:'#1a1a1a'},bgcolor:'rgba(0,0,0,0)',
|
| 581 |
+
xanchor:'center',yanchor:'bottom',text:t.text[j]});
|
| 582 |
+
});
|
| 583 |
+
});
|
| 584 |
+
return {
|
| 585 |
+
data:data,
|
| 586 |
+
layout:{
|
| 587 |
+
width:wh[0], height:wh[1], autosize:false,
|
| 588 |
+
margin:{l:38,r:12,t:52,b:36},
|
| 589 |
+
paper_bgcolor:'white', plot_bgcolor:'rgba(240,250,246,0.4)',
|
| 590 |
+
showlegend:false, annotations:anns,
|
| 591 |
+
xaxis:{tickmode:'array',tickvals:tickvals,ticktext:ticktext,
|
| 592 |
+
tickfont:{size:8,family:'DM Sans'},
|
| 593 |
+
gridcolor:'rgba(16,185,129,0.10)',zeroline:false,ticklabelstandoff:8},
|
| 594 |
+
yaxis:{tickfont:{size:8,family:'DM Sans'},rangemode:'tozero',
|
| 595 |
+
gridcolor:'rgba(16,185,129,0.10)',zeroline:false}
|
| 596 |
+
}
|
| 597 |
+
};
|
| 598 |
+
}
|
| 599 |
+
|
| 600 |
+
function makePie(slices, id) {
|
| 601 |
+
var wh = gsize(id);
|
| 602 |
+
var total = slices.reduce(function(s,x){ return s+x.value; }, 0);
|
| 603 |
+
var labels = slices.map(function(x) {
|
| 604 |
+
return x.label+': '+fmtNum(x.value)+' ('+(x.value/total*100).toFixed(1)+'%)';
|
| 605 |
+
});
|
| 606 |
+
return {
|
| 607 |
+
data:[{
|
| 608 |
+
type:'pie', labels:labels, values:slices.map(function(x){ return x.value; }),
|
| 609 |
+
hole:0.38,
|
| 610 |
+
marker:{colors:slices.map(function(x){ return x.color; }), line:{color:'white',width:2}},
|
| 611 |
+
textfont:{size:8,family:'DM Sans'}, textposition:'inside'
|
| 612 |
+
}],
|
| 613 |
+
layout:{
|
| 614 |
+
width:wh[0], height:wh[1], autosize:false,
|
| 615 |
+
margin:{l:6,r:90,t:36,b:8}, paper_bgcolor:'white',
|
| 616 |
+
showlegend:true,
|
| 617 |
+
legend:{orientation:'v',y:0.5,x:1.01,font:{size:7,family:'DM Sans'}}
|
| 618 |
+
}
|
| 619 |
+
};
|
| 620 |
+
}
|
| 621 |
+
|
| 622 |
+
function makeBarH(bars, id) {
|
| 623 |
+
var wh = gsize(id);
|
| 624 |
+
var isRec = REC_SLOTS.indexOf(id) >= 0;
|
| 625 |
+
var grid = isRec ? 'rgba(0,86,163,0.08)' : 'rgba(16,185,129,0.08)';
|
| 626 |
+
var maxVal = Math.max.apply(null, bars.map(function(b){ return b.value; }));
|
| 627 |
var thresh = maxVal * 0.15;
|
| 628 |
+
var anns = bars.map(function(b) {
|
| 629 |
+
var ins = b.value >= thresh;
|
| 630 |
+
return {
|
| 631 |
+
x: ins ? b.value*0.96 : b.value+maxVal*0.02,
|
| 632 |
y: b.label,
|
| 633 |
xref:'x', yref:'y', showarrow:false,
|
| 634 |
+
xanchor: ins?'right':'left', yanchor:'middle',
|
|
|
|
| 635 |
text:'<b>'+fmtNum(b.value)+'</b>',
|
| 636 |
+
font:{size:9,family:'DM Sans',color:ins?'white':'#374151'}
|
| 637 |
+
};
|
| 638 |
+
});
|
| 639 |
+
return {
|
| 640 |
+
data:[{
|
| 641 |
type:'bar', orientation:'h',
|
| 642 |
+
x:bars.map(function(b){ return b.value; }),
|
| 643 |
+
y:bars.map(function(b){ return b.label; }),
|
| 644 |
+
text:bars.map(function(){ return ''; }),
|
| 645 |
+
marker:{color:bars.map(function(b){ return b.color; }),opacity:0.90,line:{color:'rgba(255,255,255,0)',width:0}}
|
| 646 |
+
}],
|
| 647 |
+
layout:{
|
|
|
|
|
|
|
|
|
|
|
|
|
| 648 |
width:wh[0], height:wh[1], autosize:false,
|
| 649 |
+
margin:{l:96,r:60,t:36,b:12},
|
| 650 |
paper_bgcolor:'white', plot_bgcolor:'rgba(248,252,250,0.6)',
|
| 651 |
+
showlegend:false, bargap:0.28, annotations:anns,
|
| 652 |
+
yaxis:{tickfont:{size:8,family:'DM Sans',color:'#4b5563'},automargin:true,
|
| 653 |
+
gridcolor:'rgba(0,0,0,0)',zeroline:false},
|
| 654 |
+
xaxis:{tickfont:{size:8,family:'DM Sans',color:'#9ca3af'},
|
| 655 |
+
gridcolor:grid,zeroline:false,range:[0,maxVal*1.25]}
|
| 656 |
+
},
|
| 657 |
+
_orient:'h'
|
| 658 |
+
};
|
| 659 |
+
}
|
| 660 |
+
|
| 661 |
+
function makeBarV(bars, id) {
|
| 662 |
+
var wh = gsize(id);
|
| 663 |
+
var isRec = REC_SLOTS.indexOf(id) >= 0;
|
| 664 |
+
var grid = isRec ? 'rgba(0,86,163,0.08)' : 'rgba(16,185,129,0.08)';
|
| 665 |
+
var maxVal = Math.max.apply(null, bars.map(function(b){ return b.value; }));
|
| 666 |
+
var anns = bars.map(function(b) {
|
| 667 |
+
return {x:b.label,y:b.value,xref:'x',yref:'y',showarrow:false,
|
| 668 |
+
xanchor:'center',yanchor:'bottom',yshift:4,
|
|
|
|
|
|
|
| 669 |
text:'<b>'+fmtNum(b.value)+'</b>',
|
| 670 |
+
font:{size:9,family:'DM Sans',color:'#374151'}};
|
| 671 |
+
});
|
| 672 |
+
return {
|
| 673 |
+
data:[{
|
|
|
|
| 674 |
type:'bar',
|
| 675 |
+
x:bars.map(function(b){ return b.label; }),
|
| 676 |
+
y:bars.map(function(b){ return b.value; }),
|
| 677 |
+
text:bars.map(function(){ return ''; }),
|
| 678 |
+
marker:{color:bars.map(function(b){ return b.color; }),opacity:0.90,line:{color:'rgba(255,255,255,0)',width:0}}
|
| 679 |
+
}],
|
| 680 |
+
layout:{
|
|
|
|
|
|
|
|
|
|
|
|
|
| 681 |
width:wh[0], height:wh[1], autosize:false,
|
| 682 |
+
margin:{l:38,r:12,t:42,b:100},
|
| 683 |
paper_bgcolor:'white', plot_bgcolor:'rgba(248,252,250,0.6)',
|
| 684 |
+
showlegend:false, bargap:0.30, annotations:anns,
|
| 685 |
+
xaxis:{tickfont:{size:7,family:'DM Sans',color:'#4b5563'},
|
| 686 |
+
tickangle:-45,zeroline:false,automargin:true,ticklabelstandoff:8},
|
| 687 |
+
yaxis:{tickfont:{size:8,family:'DM Sans',color:'#9ca3af'},
|
| 688 |
+
gridcolor:grid,zeroline:false,range:[0,maxVal*1.30]}
|
| 689 |
+
},
|
| 690 |
+
_orient:'v'
|
| 691 |
+
};
|
| 692 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 693 |
|
| 694 |
+
function roundBars(boxEl, r, orient) {
|
| 695 |
+
setTimeout(function() {
|
| 696 |
+
boxEl.querySelectorAll('.bars path').forEach(function(p) {
|
| 697 |
+
var d = p.getAttribute('d');
|
| 698 |
+
if (!d) return;
|
| 699 |
+
var tok = d.match(/[A-Za-z][^A-Za-z]*/g)||[];
|
| 700 |
+
var cx=0,cy=0,aX=[],aY=[];
|
| 701 |
+
tok.forEach(function(t) {
|
| 702 |
+
var cmd = t[0];
|
| 703 |
+
var pts = t.slice(1).trim().split(/[\\s,]+/).map(Number).filter(function(n){ return !isNaN(n); });
|
| 704 |
+
if (cmd==='M'||cmd==='L') { aX.push(pts[0]); aY.push(pts[1]); cx=pts[0]; cy=pts[1]; }
|
| 705 |
+
else if (cmd==='H') { aX.push(pts[0]); aY.push(cy); cx=pts[0]; }
|
| 706 |
+
else if (cmd==='V') { aX.push(cx); aY.push(pts[0]); cy=pts[0]; }
|
| 707 |
+
});
|
| 708 |
+
if (aX.length<2) return;
|
| 709 |
+
var x0=Math.min.apply(null,aX), x1=Math.max.apply(null,aX);
|
| 710 |
+
var y0=Math.min.apply(null,aY), y1=Math.max.apply(null,aY);
|
| 711 |
+
var w=x1-x0, h=y1-y0;
|
| 712 |
+
if (w<=0||h<=0) return;
|
| 713 |
+
var rr=Math.min(r,w/2,h/2), nd;
|
| 714 |
+
if (orient==='h') {
|
| 715 |
+
nd='M '+x0+','+y0+' H '+(x1-rr)+' Q '+x1+','+y0+' '+x1+','+(y0+rr)
|
| 716 |
+
+' V '+(y1-rr)+' Q '+x1+','+y1+' '+(x1-rr)+','+y1+' H '+x0+' V '+y0+' Z';
|
| 717 |
+
} else {
|
| 718 |
+
nd='M '+x0+','+y1+' H '+x1+' V '+(y0+rr)
|
| 719 |
+
+' Q '+x1+','+y0+' '+(x1-rr)+','+y0
|
| 720 |
+
+' H '+(x0+rr)+' Q '+x0+','+y0+' '+x0+','+(y0+rr)+' V '+y1+' Z';
|
| 721 |
+
}
|
| 722 |
+
p.setAttribute('d', nd);
|
| 723 |
+
});
|
| 724 |
+
}, 60);
|
| 725 |
+
}
|
| 726 |
|
| 727 |
+
function renderFrame(f) {
|
| 728 |
+
Object.keys(METRIC_IDS).forEach(function(k) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 729 |
var el = document.getElementById(METRIC_IDS[k]);
|
| 730 |
+
if (el && f.metrics[k] !== undefined) el.textContent = f.metrics[k]+(METRIC_SUF[k]||'');
|
| 731 |
+
});
|
|
|
|
|
|
|
| 732 |
|
| 733 |
+
Object.keys(f.charts).forEach(function(id) {
|
|
|
|
| 734 |
var box = document.getElementById(id);
|
| 735 |
if (!box) return;
|
| 736 |
+
var d = f.charts[id];
|
|
|
|
| 737 |
var isRec = REC_SLOTS.indexOf(id) >= 0;
|
| 738 |
+
var spec;
|
| 739 |
+
|
| 740 |
+
if (LINE_SLOTS.indexOf(id) >= 0) {
|
| 741 |
+
spec = EXP_LINE.indexOf(id) >= 0 ? makeLineExp(d,id) : makeLine(d,id);
|
| 742 |
+
} else if (DONUT_SLOTS.indexOf(id) >= 0) {
|
| 743 |
+
spec = makePie(d, id);
|
| 744 |
+
} else if (BARH_SLOTS.indexOf(id) >= 0) {
|
| 745 |
+
spec = makeBarH(d, id);
|
| 746 |
+
} else if (BARV_SLOTS.indexOf(id) >= 0) {
|
| 747 |
+
spec = makeBarV(d, id);
|
| 748 |
+
}
|
| 749 |
if (!spec) return;
|
| 750 |
|
| 751 |
+
addTitle(spec.layout, id, isRec);
|
| 752 |
var isBar = (BARH_SLOTS.indexOf(id)>=0 || BARV_SLOTS.indexOf(id)>=0);
|
| 753 |
+
var orient = spec._orient || 'v';
|
| 754 |
|
| 755 |
+
if (!inited[id]) {
|
| 756 |
+
Plotly.newPlot(box, spec.data, spec.layout, {displayModeBar:false, responsive:false});
|
| 757 |
+
inited[id] = true;
|
| 758 |
+
} else {
|
| 759 |
Plotly.react(box, spec.data, spec.layout);
|
| 760 |
+
}
|
| 761 |
+
if (isBar) roundBars(box, 7, orient);
|
| 762 |
+
});
|
| 763 |
+
}
|
| 764 |
|
| 765 |
+
function showCountdown(secs) {
|
| 766 |
badge.classList.add('visible');
|
| 767 |
+
var rem = secs;
|
| 768 |
+
cdSecs.textContent = rem;
|
| 769 |
+
cdTimer = setInterval(function() {
|
| 770 |
+
rem--;
|
| 771 |
+
cdSecs.textContent = rem;
|
| 772 |
+
if (rem <= 0) {
|
| 773 |
+
clearInterval(cdTimer);
|
| 774 |
badge.classList.remove('visible');
|
| 775 |
frameIdx = 0;
|
| 776 |
startAnimation();
|
| 777 |
+
}
|
| 778 |
+
}, 1000);
|
| 779 |
+
}
|
| 780 |
|
| 781 |
+
function stepFrame() {
|
| 782 |
renderFrame(FRAMES[frameIdx]);
|
| 783 |
frameIdx++;
|
| 784 |
+
if (frameIdx >= FRAMES.length) {
|
| 785 |
clearTimeout(animTimer);
|
| 786 |
showCountdown(PAUSE_AFTER_MS / 1000);
|
| 787 |
+
} else {
|
| 788 |
animTimer = setTimeout(stepFrame, FRAME_INTERVAL_MS);
|
| 789 |
+
}
|
| 790 |
+
}
|
| 791 |
|
| 792 |
+
function startAnimation() {
|
| 793 |
clearTimeout(animTimer);
|
| 794 |
+
clearInterval(cdTimer);
|
| 795 |
animTimer = setTimeout(stepFrame, FRAME_INTERVAL_MS);
|
| 796 |
+
}
|
| 797 |
|
| 798 |
+
window.addEventListener('load', function() {
|
| 799 |
+
badge = document.getElementById('countdown-badge');
|
| 800 |
+
cdSecs = document.getElementById('cd-secs');
|
| 801 |
+
setTimeout(function(){ frameIdx=0; startAnimation(); }, 300);
|
| 802 |
+
});
|
|
|
|
|
|
|
|
|
|
| 803 |
|
| 804 |
var _rt;
|
| 805 |
+
window.addEventListener('resize', function() {
|
| 806 |
clearTimeout(_rt);
|
| 807 |
+
_rt = setTimeout(function() {
|
| 808 |
+
document.querySelectorAll('.cb').forEach(function(box) {
|
| 809 |
+
if (box.data) Plotly.relayout(box, {width:box.clientWidth, height:box.clientHeight});
|
| 810 |
+
});
|
| 811 |
+
}, 200);
|
| 812 |
+
});
|
|
|
|
|
|
|
| 813 |
</script>
|
| 814 |
+
</body>
|
| 815 |
+
</html>"""
|
| 816 |
+
|
| 817 |
+
html = (html
|
| 818 |
+
.replace('__FRAMES__', frames_json)
|
| 819 |
+
.replace('__MESES__', meses_json)
|
| 820 |
+
)
|
| 821 |
|
| 822 |
components.html(html, height=8000, scrolling=False)
|
| 823 |
|
| 824 |
else:
|
| 825 |
+
components.html("""<!DOCTYPE html><html><body style="display:flex;align-items:center;
|
| 826 |
+
justify-content:center;height:100dvh;font-family:sans-serif;color:#064e3b;background:#f0faf6;">
|
| 827 |
+
<div style="text-align:center"><div style="font-size:2em">📊</div>
|
| 828 |
+
<p>Aguardando dados... Recarregue a página.</p></div></body></html>""",
|
| 829 |
+
height=4000, scrolling=False)
|
| 830 |
|
| 831 |
time.sleep(60)
|
| 832 |
+
st.rerun()
|
pages/rec.py
CHANGED
|
@@ -7,7 +7,23 @@ import streamlit.components.v1 as components
|
|
| 7 |
from gs_client import read_ws
|
| 8 |
|
| 9 |
st.set_page_config(layout="wide", initial_sidebar_state="collapsed", page_title="PMJA - Dashboard Recebimento")
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
# ── SESSION ───────────────────────────────────────────────────────────
|
| 12 |
_cc = CookieController()
|
| 13 |
COOKIE_NAME = "pmja_session"
|
|
@@ -38,21 +54,63 @@ if time.time() - st.session_state.inicio_exibicao >= 120:
|
|
| 38 |
st.session_state.inicio_exibicao = time.time()
|
| 39 |
st.switch_page("pages/exp.py")
|
| 40 |
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
.
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
}
|
| 55 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
|
| 57 |
# ── HELPERS ───────────────────────────────────────────────────────────
|
| 58 |
def fmt(n):
|
|
@@ -72,24 +130,15 @@ def extrair_sistemas(df):
|
|
| 72 |
break
|
| 73 |
return sorted(list(sistemas_set))
|
| 74 |
|
| 75 |
-
|
| 76 |
-
if pd.isna(num): return "0"
|
| 77 |
-
return f"{int(num):,}".replace(',', '.')
|
| 78 |
-
|
| 79 |
-
# ── BUILD ALL FRAMES ──────────────────────────────────────────────────
|
| 80 |
def build_frames(df_plot, df_rec, sistemas, cores_sistemas, anos_unicos,
|
| 81 |
mes_max_dados, mes_max_rec, metricas_linha1, metricas_linha2,
|
| 82 |
metricas_rec, mapeamentos_rec):
|
| 83 |
-
|
| 84 |
meses_pt = {1:'Jan',2:'Fev',3:'Mar',4:'Abr',5:'Mai',6:'Jun',
|
| 85 |
7:'Jul',8:'Ago',9:'Set',10:'Out',11:'Nov',12:'Dez'}
|
| 86 |
-
|
| 87 |
frames = []
|
| 88 |
-
|
| 89 |
for mes_atual in range(1, mes_max_dados + 1):
|
| 90 |
frame = {"mes": mes_atual, "cards": {}, "linha1": [], "linha2": [], "linha3": []}
|
| 91 |
-
|
| 92 |
-
# ── cards ─────────────────────────────────────────────────────
|
| 93 |
df_ate = df_plot[df_plot['mes'] <= mes_atual]
|
| 94 |
for coluna, titulo, cor, sufixo in [
|
| 95 |
('qtd_descarregamentos', 'Descarregamentos', '#003a70', ''),
|
|
@@ -98,12 +147,8 @@ def build_frames(df_plot, df_rec, sistemas, cores_sistemas, anos_unicos,
|
|
| 98 |
('qtd_unidades_recebidos','Unidades Recebidas','#4d9fd6',''),
|
| 99 |
('qtd_itens_por_unidade','Itens por Unidade','#80b9e5', ''),
|
| 100 |
]:
|
| 101 |
-
frame["cards"][coluna] = {
|
| 102 |
-
|
| 103 |
-
"sufixo": sufixo, "cor": cor, "titulo": titulo
|
| 104 |
-
}
|
| 105 |
-
|
| 106 |
-
# ── linha 1: 3 line charts ────────────────────────────────────
|
| 107 |
for config in metricas_linha1:
|
| 108 |
col = config['coluna']
|
| 109 |
traces = []
|
|
@@ -112,16 +157,13 @@ def build_frames(df_plot, df_rec, sistemas, cores_sistemas, anos_unicos,
|
|
| 112 |
if d.empty: continue
|
| 113 |
cor = config['cores'][idx_ano % len(config['cores'])]
|
| 114 |
traces.append({
|
| 115 |
-
"ano": str(ano),
|
| 116 |
-
"cor": cor,
|
| 117 |
"x": [int(x) for x in d['mes']],
|
| 118 |
"y": [float(v) for v in d[col]],
|
| 119 |
-
"text": [
|
| 120 |
-
"fill_rgba":
|
| 121 |
})
|
| 122 |
frame["linha1"].append({"titulo": config['titulo'], "sufixo": config['sufixo'], "traces": traces})
|
| 123 |
-
|
| 124 |
-
# ── linha 2: 2 line charts ────────────────────────────────────
|
| 125 |
for config in metricas_linha2:
|
| 126 |
col = config['coluna']
|
| 127 |
traces = []
|
|
@@ -130,16 +172,13 @@ def build_frames(df_plot, df_rec, sistemas, cores_sistemas, anos_unicos,
|
|
| 130 |
if d.empty: continue
|
| 131 |
cor = config['cores'][idx_ano % len(config['cores'])]
|
| 132 |
traces.append({
|
| 133 |
-
"ano": str(ano),
|
| 134 |
-
"cor": cor,
|
| 135 |
"x": [int(x) for x in d['mes']],
|
| 136 |
"y": [float(v) for v in d[col]],
|
| 137 |
-
"text": [
|
| 138 |
-
"fill_rgba":
|
| 139 |
})
|
| 140 |
frame["linha2"].append({"titulo": config['titulo'], "sufixo": config['sufixo'], "traces": traces})
|
| 141 |
-
|
| 142 |
-
# ── linha 3: pizza + barH + barV ─────────────────────────────
|
| 143 |
if df_rec is not None and mes_atual <= mes_max_rec:
|
| 144 |
df_ate_rec = df_rec[df_rec['mes'] <= mes_atual]
|
| 145 |
for idx_m, metrica_info in enumerate(metricas_rec):
|
|
@@ -149,105 +188,17 @@ def build_frames(df_plot, df_rec, sistemas, cores_sistemas, anos_unicos,
|
|
| 149 |
col_nome = next((c for c, s in mapeamento.items() if s == sistema), None)
|
| 150 |
if col_nome and col_nome in df_rec.columns:
|
| 151 |
total = float(df_ate_rec[col_nome].fillna(0).sum())
|
| 152 |
-
valores.append(total)
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
frame["linha3"].append({
|
| 156 |
-
"titulo": metrica_info['titulo'],
|
| 157 |
-
"tipo": idx_m,
|
| 158 |
-
"valores": valores, "labels": labels, "cores": cores
|
| 159 |
-
})
|
| 160 |
else:
|
| 161 |
-
for metrica_info in metricas_rec:
|
| 162 |
-
frame["linha3"].append({"titulo": metrica_info['titulo'], "tipo":
|
| 163 |
-
|
| 164 |
frames.append(frame)
|
| 165 |
-
|
| 166 |
return frames, meses_pt
|
| 167 |
|
| 168 |
-
# ──
|
| 169 |
-
# ── LOADING SCREEN ─────────────────────────────────────────────────
|
| 170 |
-
loading_html = """
|
| 171 |
-
<style>
|
| 172 |
-
*{margin:0;padding:0;box-sizing:border-box}
|
| 173 |
-
.overlay{
|
| 174 |
-
position:fixed;inset:0;
|
| 175 |
-
background:linear-gradient(135deg,#003a70 0%,#0056A3 100%);
|
| 176 |
-
display:flex;flex-direction:column;align-items:center;justify-content:center;
|
| 177 |
-
z-index:99999;font-family:'DM Sans',sans-serif;
|
| 178 |
-
transition:opacity .5s;
|
| 179 |
-
}
|
| 180 |
-
.logos{display:flex;align-items:center;gap:32px;margin-bottom:48px}
|
| 181 |
-
.logo-img{height:40px;filter:brightness(0) invert(1);opacity:.9}
|
| 182 |
-
.divider{width:1px;height:40px;background:rgba(255,255,255,.25)}
|
| 183 |
-
.title{font-size:22px;color:#fff;font-weight:800;letter-spacing:-.5px;
|
| 184 |
-
margin-bottom:4px;text-align:center;font-family:'Sora',sans-serif}
|
| 185 |
-
.subtitle{font-size:13px;color:rgba(255,255,255,.7);font-weight:600;
|
| 186 |
-
margin-bottom:52px;text-align:center}
|
| 187 |
-
.bar-wrap{width:280px;height:4px;background:rgba(255,255,255,.15);
|
| 188 |
-
border-radius:99px;overflow:hidden;margin-bottom:20px}
|
| 189 |
-
.bar-fill{height:100%;width:0%;background:#fff;border-radius:99px;transition:width .4s ease}
|
| 190 |
-
.status{font-size:12px;color:rgba(255,255,255,.65);font-weight:600;
|
| 191 |
-
letter-spacing:.3px;min-height:18px;text-align:center}
|
| 192 |
-
.dots span{animation:blink 1.2s infinite;opacity:0}
|
| 193 |
-
.dots span:nth-child(2){animation-delay:.2s}
|
| 194 |
-
.dots span:nth-child(3){animation-delay:.4s}
|
| 195 |
-
@keyframes blink{0%,100%{opacity:0}50%{opacity:1}}
|
| 196 |
-
.spinner{width:36px;height:36px;border:3px solid rgba(255,255,255,.2);
|
| 197 |
-
border-top-color:#fff;border-radius:50%;animation:spin .8s linear infinite;
|
| 198 |
-
margin-bottom:32px}
|
| 199 |
-
@keyframes spin{to{transform:rotate(360deg)}}
|
| 200 |
-
</style>
|
| 201 |
-
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;600;700;800&family=Sora:wght@700;800&display=swap" rel="stylesheet">
|
| 202 |
-
|
| 203 |
-
<div class="overlay" id="overlay">
|
| 204 |
-
<div class="logos">
|
| 205 |
-
<img class="logo-img" src="https://companieslogo.com/img/orig/AZ2.F-d26946db.png?t=1720244490" alt="AZ">
|
| 206 |
-
<div class="divider"></div>
|
| 207 |
-
<img class="logo-img" src="https://upload.wikimedia.org/wikipedia/commons/8/8f/Logo-engie.svg" alt="Engie">
|
| 208 |
-
</div>
|
| 209 |
-
<div class="spinner"></div>
|
| 210 |
-
<div class="title">PMJA — Dashboard Recebimento</div>
|
| 211 |
-
<div class="subtitle">Gestão de Materiais</div>
|
| 212 |
-
<div class="bar-wrap"><div class="bar-fill" id="bar"></div></div>
|
| 213 |
-
<div class="status" id="status">
|
| 214 |
-
Conectando ao Google Sheets
|
| 215 |
-
<span class="dots"><span>.</span><span>.</span><span>.</span></span>
|
| 216 |
-
</div>
|
| 217 |
-
</div>
|
| 218 |
-
|
| 219 |
-
<script>
|
| 220 |
-
var steps=[
|
| 221 |
-
{p:18, msg:'Conectando ao Google Sheets'},
|
| 222 |
-
{p:40, msg:'Carregando dados de recebimento'},
|
| 223 |
-
{p:62, msg:'Processando métricas'},
|
| 224 |
-
{p:80, msg:'Montando gráficos'},
|
| 225 |
-
{p:95, msg:'Quase pronto'}
|
| 226 |
-
];
|
| 227 |
-
var i=0;
|
| 228 |
-
function next(){
|
| 229 |
-
if(i>=steps.length) return;
|
| 230 |
-
var s=steps[i++];
|
| 231 |
-
document.getElementById('bar').style.width=s.p+'%';
|
| 232 |
-
document.getElementById('status').innerHTML=
|
| 233 |
-
s.msg+'<span class="dots"><span>.</span><span>.</span><span>.</span></span>';
|
| 234 |
-
setTimeout(next, 950+Math.random()*550);
|
| 235 |
-
}
|
| 236 |
-
setTimeout(next, 300);
|
| 237 |
-
</script>
|
| 238 |
-
"""
|
| 239 |
-
|
| 240 |
-
loading_placeholder = st.empty()
|
| 241 |
-
|
| 242 |
-
with loading_placeholder:
|
| 243 |
-
components.html(loading_html, height=0)
|
| 244 |
-
|
| 245 |
-
df_recebimento = read_ws("recebimento")
|
| 246 |
-
df_rec_dados = read_ws("rec_dados")
|
| 247 |
-
|
| 248 |
-
loading_placeholder.empty()
|
| 249 |
-
|
| 250 |
-
|
| 251 |
if df_recebimento is not None and not df_recebimento.empty:
|
| 252 |
df_plot = df_recebimento.copy()
|
| 253 |
colunas = ['mes_ano','qtd_descarregamentos','qtd_volumes_recebidos',
|
|
@@ -267,9 +218,9 @@ if df_recebimento is not None and not df_recebimento.empty:
|
|
| 267 |
df_rec, sistemas, cores_sistemas, mes_max_rec = None, [], [], 0
|
| 268 |
mapeamentos_rec = [{}, {}, {}]
|
| 269 |
metricas_rec = [
|
| 270 |
-
{'palavra_chave': 'Volumes', 'titulo': 'Volumes Recebidos por Sistema',
|
| 271 |
-
{'palavra_chave': 'Unid. Itens', 'titulo': 'Unidades de Itens por Sistema',
|
| 272 |
-
{'palavra_chave': 'Itens por unidade', 'titulo': 'Itens por Unidade por Sistema',
|
| 273 |
]
|
| 274 |
if df_rec_dados is not None and not df_rec_dados.empty:
|
| 275 |
df_rec = df_rec_dados.copy()
|
|
@@ -295,25 +246,25 @@ if df_recebimento is not None and not df_recebimento.empty:
|
|
| 295 |
|
| 296 |
metricas_linha1 = [
|
| 297 |
{'coluna':'qtd_descarregamentos','titulo':'Descarregamentos',
|
| 298 |
-
'cores':['#
|
| 299 |
{'coluna':'peso_recebido','titulo':'Peso Recebido',
|
| 300 |
-
'cores':['#
|
| 301 |
{'coluna':'qtd_volumes_recebidos','titulo':'Volumes Recebidos',
|
| 302 |
-
'cores':['#
|
| 303 |
]
|
| 304 |
metricas_linha2 = [
|
| 305 |
{'coluna':'qtd_unidades_recebidos','titulo':'Unidades Recebidas',
|
| 306 |
-
'cores':['#
|
| 307 |
{'coluna':'qtd_itens_por_unidade','titulo':'Itens por Unidade',
|
| 308 |
-
'cores':['#
|
| 309 |
]
|
| 310 |
|
| 311 |
def titulo_anos(config):
|
| 312 |
parts = []
|
| 313 |
for idx_ano, ano in enumerate(anos_unicos):
|
| 314 |
cor = config['cores'][idx_ano % len(config['cores'])]
|
| 315 |
-
total =
|
| 316 |
-
parts.append(
|
| 317 |
return " <span style='color:#666'>|</span> ".join(parts)
|
| 318 |
|
| 319 |
def titulo_sistemas(idx_m):
|
|
@@ -323,9 +274,9 @@ if df_recebimento is not None and not df_recebimento.empty:
|
|
| 323 |
for cat_idx, sistema in enumerate(sistemas):
|
| 324 |
col_nome = next((c for c,s in mapa.items() if s==sistema), None)
|
| 325 |
if col_nome:
|
| 326 |
-
total =
|
| 327 |
cor = cores_sistemas[cat_idx]
|
| 328 |
-
parts.append(
|
| 329 |
return " <span style='color:#666'>|</span> ".join(parts)
|
| 330 |
|
| 331 |
frames, meses_pt = build_frames(
|
|
@@ -334,69 +285,77 @@ if df_recebimento is not None and not df_recebimento.empty:
|
|
| 334 |
metricas_rec, mapeamentos_rec
|
| 335 |
)
|
| 336 |
|
| 337 |
-
frames_json
|
| 338 |
-
meses_json
|
| 339 |
-
anos_json
|
| 340 |
-
|
| 341 |
sub_l1 = json.dumps([titulo_anos(c) for c in metricas_linha1])
|
| 342 |
sub_l2 = json.dumps([titulo_anos(c) for c in metricas_linha2])
|
| 343 |
sub_l3 = json.dumps([titulo_sistemas(i) for i in range(3)])
|
| 344 |
|
| 345 |
-
html =
|
|
|
|
|
|
|
| 346 |
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
| 347 |
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;600;700;800&family=Sora:wght@700;800;900&display=swap" rel="stylesheet">
|
| 348 |
<script src="https://cdn.plot.ly/plotly-2.27.0.min.js" charset="utf-8"></script>
|
| 349 |
<style>
|
| 350 |
-
*{
|
| 351 |
-
html,body{
|
| 352 |
background:linear-gradient(135deg,#f5f7fa 0%,#e8eef5 100%);
|
| 353 |
-
font-family:'DM Sans',sans-serif;}
|
| 354 |
-
.root{
|
| 355 |
-
grid-template-rows:auto auto 1fr 1fr 1fr;}
|
| 356 |
-
.hdr{
|
| 357 |
-
border-radius:8px;padding:4px 14px;display:flex;align-items:center;
|
| 358 |
-
box-shadow:0 4px 12px rgba(0,58,112,.12);}
|
| 359 |
-
.logo{
|
| 360 |
-
.ht{
|
| 361 |
-
|
| 362 |
-
.
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
.
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
.
|
| 372 |
-
|
| 373 |
-
.
|
| 374 |
-
|
| 375 |
-
.
|
| 376 |
-
|
| 377 |
-
.ct
|
| 378 |
-
|
| 379 |
-
.
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
.
|
| 387 |
-
.
|
| 388 |
-
}
|
| 389 |
-
|
| 390 |
-
.
|
| 391 |
-
.
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 396 |
</style>
|
| 397 |
-
</head>
|
|
|
|
| 398 |
<div class="root">
|
| 399 |
-
|
| 400 |
<div class="hdr">
|
| 401 |
<img class="logo" src="https://companieslogo.com/img/orig/AZ2.F-d26946db.png?t=1720244490" alt="AZ">
|
| 402 |
<div style="text-align:center;flex:1">
|
|
@@ -405,509 +364,431 @@ html,body{{width:100%;height:100%;overflow:hidden;
|
|
| 405 |
</div>
|
| 406 |
<img class="logo" src="https://upload.wikimedia.org/wikipedia/commons/8/8f/Logo-engie.svg" alt="Engie">
|
| 407 |
</div>
|
| 408 |
-
|
| 409 |
<div class="cards" id="cards-row"></div>
|
| 410 |
<div class="row3" id="row1"></div>
|
| 411 |
<div class="row2" id="row2"></div>
|
| 412 |
<div class="row3" id="row3"></div>
|
| 413 |
-
|
| 414 |
</div>
|
| 415 |
-
|
| 416 |
<script>
|
| 417 |
-
var FRAMES =
|
| 418 |
-
var MESES =
|
| 419 |
-
var ANOS =
|
| 420 |
-
var SUB_L1 =
|
| 421 |
-
var SUB_L2 =
|
| 422 |
-
var SUB_L3 =
|
| 423 |
|
| 424 |
var CARDS_CFG = [
|
| 425 |
-
{
|
| 426 |
-
{
|
| 427 |
-
{
|
| 428 |
-
{
|
| 429 |
-
{
|
| 430 |
];
|
| 431 |
|
| 432 |
var TITULOS_L1 = ['Descarregamentos','Peso Recebido','Volumes Recebidos'];
|
| 433 |
var TITULOS_L2 = ['Unidades Recebidas','Itens por Unidade'];
|
| 434 |
var TITULOS_L3 = ['Volumes Recebidos por Sistema','Unidades de Itens por Sistema','Itens por Unidade por Sistema'];
|
| 435 |
|
| 436 |
-
var FRAME_MS
|
| 437 |
-
var PAUSE_MS
|
| 438 |
-
var frameIdx
|
| 439 |
-
var
|
|
|
|
|
|
|
| 440 |
|
| 441 |
-
function buildDOM() {
|
| 442 |
var cr = document.getElementById('cards-row');
|
| 443 |
-
CARDS_CFG.forEach(function(c) {
|
| 444 |
-
cr.innerHTML += '<div class="mc" style="border-left-color:'+c.cor+'"
|
| 445 |
-
+'<div class="ml">'+c.titulo+'</div>'
|
| 446 |
-
+'<div class="mv" id="mv_'+c.col+'" style="color:'+c.cor+'">0'+c.sufixo+'</div></div>';
|
| 447 |
-
}
|
| 448 |
-
|
| 449 |
var r1 = document.getElementById('row1');
|
| 450 |
-
for(var i=0;i<3;i++) {
|
| 451 |
r1.innerHTML += '<div class="cb"><div class="ct">'
|
| 452 |
-
+'<div class="ct-title">'+TITULOS_L1[i]+'</div>'
|
| 453 |
-
+'<div class="ct-sub" id="sub_l1_'+i+'">'+SUB_L1[i]+'</div></div>'
|
| 454 |
-
+'<div class="cp" id="l1_'+i+'"></div></div>';
|
| 455 |
-
}
|
| 456 |
-
|
| 457 |
var r2 = document.getElementById('row2');
|
| 458 |
-
for(var i=0;i<2;i++) {
|
| 459 |
r2.innerHTML += '<div class="cb"><div class="ct">'
|
| 460 |
-
+'<div class="ct-title">'+TITULOS_L2[i]+'</div>'
|
| 461 |
-
+'<div class="ct-sub" id="sub_l2_'+i+'">'+SUB_L2[i]+'</div></div>'
|
| 462 |
-
+'<div class="cp" id="l2_'+i+'"></div></div>';
|
| 463 |
-
}
|
| 464 |
-
|
| 465 |
var r3 = document.getElementById('row3');
|
| 466 |
-
for(var i=0;i<3;i++) {
|
| 467 |
r3.innerHTML += '<div class="cb"><div class="ct">'
|
| 468 |
-
+'<div class="ct-title">'+TITULOS_L3[i]+'</div>'
|
| 469 |
-
+'<div class="ct-sub" id="sub_l3_'+i+'">'+SUB_L3[i]+'</div></div>'
|
| 470 |
-
+'<div class="cp" id="l3_'+i+'"></div></div>';
|
| 471 |
-
}
|
| 472 |
-
}
|
| 473 |
|
| 474 |
-
function gsize(id) {
|
| 475 |
var el = document.getElementById(id);
|
| 476 |
-
if(!el) return [200,150];
|
| 477 |
return [el.clientWidth||200, el.clientHeight||150];
|
| 478 |
-
}
|
| 479 |
-
|
| 480 |
-
var
|
| 481 |
-
|
| 482 |
-
// ── Line chart ─────────────────────────────────────────────────────
|
| 483 |
-
// Labels em coordenadas Y de dados — anti-colisão real por mês.
|
| 484 |
-
// Para cada mês agrupa todos os pontos, ordena por Y,
|
| 485 |
-
// e empilha as annotations com offset mínimo garantido em unidades de dados.
|
| 486 |
-
function renderLine(traces, elId, sufixo) {{
|
| 487 |
-
var wh = gsize(elId), data = [];
|
| 488 |
-
var n = traces.length;
|
| 489 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 490 |
var allY = [];
|
| 491 |
-
traces.forEach(function(t){
|
| 492 |
var yMax = allY.length ? Math.max.apply(null,allY) : 1;
|
| 493 |
-
|
| 494 |
var allX = [];
|
| 495 |
-
traces.forEach(function(t){
|
| 496 |
var xMin = allX.length ? Math.min.apply(null,allX) : 1;
|
| 497 |
var xMax2 = allX.length ? Math.max.apply(null,allX) : 12;
|
| 498 |
-
|
| 499 |
-
// headroom: espaço acima do yMax para acomodar labels empilhados
|
| 500 |
-
// cada ano extra adiciona 8% — 1 ano=40%, 7 anos=88%
|
| 501 |
var headroom = 1.40 + Math.max(0, n-1) * 0.08;
|
| 502 |
var yTop = yMax * headroom;
|
| 503 |
-
|
| 504 |
-
// offset mínimo entre labels adjacentes = 5% do range total visível
|
| 505 |
var MIN_OFFSET = yTop * 0.055;
|
| 506 |
|
| 507 |
-
traces.forEach(function(t, i) {
|
| 508 |
-
|
|
|
|
| 509 |
type:'scatter', mode:'lines+markers',
|
| 510 |
x:t.x, y:t.y,
|
| 511 |
cliponaxis:false, showlegend:false, name:t.ano,
|
| 512 |
-
line:{
|
| 513 |
-
marker:{
|
| 514 |
fill: i>0 ? 'tonexty' : null,
|
| 515 |
-
fillcolor:
|
| 516 |
-
hovertemplate:'<b>'+t.ano+'</b> %{
|
| 517 |
-
customdata: t.x.map(function(m){
|
| 518 |
text: t.text
|
| 519 |
-
}
|
| 520 |
-
}
|
| 521 |
-
|
| 522 |
-
|
| 523 |
-
|
| 524 |
-
|
| 525 |
-
|
| 526 |
-
if(!
|
| 527 |
-
|
| 528 |
-
|
| 529 |
-
|
| 530 |
-
}});
|
| 531 |
|
| 532 |
var annotations = [];
|
| 533 |
-
|
| 534 |
-
Object.keys(byX).forEach(function(mxStr) {{
|
| 535 |
var mx = parseInt(mxStr);
|
| 536 |
var pts = byX[mx];
|
| 537 |
-
|
| 538 |
-
// Ordena crescente por valor Y
|
| 539 |
-
pts.sort(function(a,b){{ return a.yv - b.yv; }});
|
| 540 |
-
|
| 541 |
-
// Calcula posição Y da annotation para cada ponto:
|
| 542 |
-
// começa em yv + MIN_OFFSET e garante que cada próximo
|
| 543 |
-
// está pelo menos MIN_OFFSET acima do anterior
|
| 544 |
var labelY = [];
|
| 545 |
-
pts.forEach(function(p, k) {
|
| 546 |
var candidate = p.yv + MIN_OFFSET;
|
| 547 |
-
if(k > 0 && candidate < labelY[k-1] + MIN_OFFSET)
|
| 548 |
-
candidate = labelY[k-1] + MIN_OFFSET;
|
| 549 |
-
}}
|
| 550 |
labelY.push(candidate);
|
| 551 |
-
}
|
| 552 |
-
|
| 553 |
-
|
| 554 |
-
|
| 555 |
-
|
| 556 |
-
|
| 557 |
-
|
| 558 |
-
|
| 559 |
-
|
| 560 |
-
|
| 561 |
-
|
| 562 |
-
|
| 563 |
-
|
| 564 |
-
|
| 565 |
-
|
| 566 |
-
borderpad: 0
|
| 567 |
-
}});
|
| 568 |
-
}});
|
| 569 |
-
}});
|
| 570 |
-
|
| 571 |
-
var layout = {{
|
| 572 |
-
width:wh[0], height:wh[1], autosize:false,
|
| 573 |
hovermode:'x unified', template:'plotly_white',
|
| 574 |
-
font:{
|
| 575 |
-
margin:{
|
| 576 |
plot_bgcolor:'rgba(248,249,250,0.5)',
|
| 577 |
-
xaxis:{
|
| 578 |
tickmode:'array', tickvals:tickvals, ticktext:ticktext,
|
| 579 |
-
tickangle:0, tickfont:{
|
| 580 |
showgrid:true, gridwidth:1, gridcolor:'rgba(200,200,200,0.25)',
|
| 581 |
fixedrange:true, range:[xMin-0.4, xMax2+0.4]
|
| 582 |
-
}
|
| 583 |
-
yaxis:{
|
| 584 |
-
tickfont:{
|
| 585 |
showgrid:true, gridwidth:1, gridcolor:'rgba(200,200,200,0.25)',
|
| 586 |
range:[0, yTop], fixedrange:true,
|
| 587 |
-
tickformat: yMax
|
| 588 |
-
}
|
| 589 |
-
annotations:
|
| 590 |
-
hoverlabel:{
|
| 591 |
-
}
|
|
|
|
| 592 |
var el = document.getElementById(elId);
|
| 593 |
-
if(!
|
| 594 |
-
Plotly.newPlot(el, data, layout, {
|
| 595 |
-
|
| 596 |
-
}
|
| 597 |
Plotly.react(el, data, layout);
|
| 598 |
-
}
|
| 599 |
-
}
|
| 600 |
-
|
| 601 |
|
| 602 |
-
|
| 603 |
-
|
| 604 |
-
|
| 605 |
-
var
|
| 606 |
-
var
|
| 607 |
-
var labels = item.labels.map(function(l,i){{
|
| 608 |
var pct = total>0 ? (item.valores[i]/total*100).toFixed(1) : '0.0';
|
| 609 |
-
var vf = item.valores[i].toString().replace(/\\B(?=(\\d{
|
| 610 |
return l+': '+vf+' ('+pct+'%)';
|
| 611 |
-
}
|
| 612 |
-
var data = [{
|
| 613 |
type:'pie', labels:labels, values:item.valores,
|
| 614 |
-
marker:{
|
| 615 |
hole:0.3, textposition:'inside',
|
| 616 |
-
hovertemplate:'<b>%{
|
| 617 |
-
}
|
| 618 |
-
var layout = {
|
| 619 |
-
|
| 620 |
-
template:'plotly_white',
|
| 621 |
-
|
| 622 |
-
|
| 623 |
-
|
| 624 |
-
|
| 625 |
-
}};
|
| 626 |
var el = document.getElementById(elId);
|
| 627 |
-
if(!
|
| 628 |
-
Plotly.newPlot(el, data, layout, {
|
| 629 |
-
|
| 630 |
-
}
|
| 631 |
Plotly.react(el, data, layout);
|
| 632 |
-
}
|
| 633 |
-
}
|
| 634 |
-
|
| 635 |
-
|
| 636 |
-
|
| 637 |
-
|
| 638 |
-
|
| 639 |
-
|
| 640 |
-
var
|
| 641 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 642 |
|
| 643 |
-
|
| 644 |
-
|
| 645 |
-
|
| 646 |
-
|
| 647 |
-
|
| 648 |
-
|
| 649 |
-
}
|
| 650 |
-
|
| 651 |
-
var
|
| 652 |
-
|
| 653 |
-
var
|
| 654 |
-
|
| 655 |
-
|
| 656 |
-
|
| 657 |
-
|
|
|
|
| 658 |
type:'bar', orientation:'h',
|
| 659 |
-
x:
|
| 660 |
-
marker:{
|
| 661 |
-
var r=parseInt(c.slice(1,3),16),g=parseInt(c.slice(3,5),16),b=parseInt(c.slice(5,7),16);
|
| 662 |
-
return 'rgba('+r+','+g+','+b+',0.12)';
|
| 663 |
-
}}),line:{{width:0}}}},
|
| 664 |
hoverinfo:'skip', showlegend:false,
|
| 665 |
-
text:sL.map(function(){
|
| 666 |
-
}
|
| 667 |
-
|
| 668 |
-
var barData = {{
|
| 669 |
type:'bar', orientation:'h',
|
| 670 |
x:sV, y:sL,
|
| 671 |
-
marker:{
|
| 672 |
-
hovertemplate:'<b>%{
|
| 673 |
showlegend:false,
|
| 674 |
-
text:sL.map(function(){
|
| 675 |
-
}
|
| 676 |
|
| 677 |
var threshold = maxVal * 0.20;
|
| 678 |
-
var annotations = sV.map(function(v,i){
|
| 679 |
var isShort = v < threshold;
|
| 680 |
-
return {
|
| 681 |
-
x: isShort ? v
|
| 682 |
y: sL[i],
|
| 683 |
-
text:
|
| 684 |
-
xref:'x', yref:'y',
|
| 685 |
-
showarrow:false,
|
| 686 |
xanchor: isShort ? 'left' : 'right',
|
| 687 |
yanchor:'middle',
|
| 688 |
-
font:{
|
| 689 |
-
}
|
| 690 |
-
}
|
| 691 |
-
|
| 692 |
-
var layout = {
|
| 693 |
-
|
| 694 |
-
barmode:'overlay',
|
| 695 |
-
|
| 696 |
-
|
| 697 |
-
|
| 698 |
-
|
| 699 |
-
|
| 700 |
-
|
| 701 |
-
|
| 702 |
-
|
| 703 |
-
|
| 704 |
-
|
| 705 |
-
|
| 706 |
-
|
| 707 |
-
zeroline:false,
|
| 708 |
-
ticksuffix:' ',
|
| 709 |
-
fixedrange:true,
|
| 710 |
-
automargin:true
|
| 711 |
-
}},
|
| 712 |
-
annotations: annotations,
|
| 713 |
-
hoverlabel:{{bgcolor:'white',font_size:11,font_family:'DM Sans',bordercolor:'#e0e0e0'}}
|
| 714 |
-
}};
|
| 715 |
|
| 716 |
var el = document.getElementById(elId);
|
| 717 |
-
if(!
|
| 718 |
-
Plotly.newPlot(el, [trackData, barData], layout, {
|
| 719 |
-
|
| 720 |
-
}
|
| 721 |
Plotly.react(el, [trackData, barData], layout);
|
| 722 |
-
}
|
| 723 |
-
|
| 724 |
-
|
| 725 |
-
el.querySelectorAll('.bars path').forEach(function(p) {{
|
| 726 |
-
var d=p.getAttribute('d'); if(!d) return;
|
| 727 |
-
var toks=d.match(/[A-Za-z][^A-Za-z]*/g)||[],cx=0,cy=0,aX=[],aY=[];
|
| 728 |
-
toks.forEach(function(tok){{
|
| 729 |
-
var cmd=tok[0];
|
| 730 |
-
var pts=tok.slice(1).trim().split(/[\\s,]+/).map(Number).filter(function(n){{return !isNaN(n);}});
|
| 731 |
-
if(cmd==='M'||cmd==='L'){{aX.push(pts[0]);aY.push(pts[1]);cx=pts[0];cy=pts[1];}}
|
| 732 |
-
else if(cmd==='H'){{aX.push(pts[0]);aY.push(cy);cx=pts[0];}}
|
| 733 |
-
else if(cmd==='V'){{aX.push(cx);aY.push(pts[0]);cy=pts[0];}}
|
| 734 |
-
}});
|
| 735 |
-
if(aX.length<2) return;
|
| 736 |
-
var x0=Math.min.apply(null,aX),x1=Math.max.apply(null,aX);
|
| 737 |
-
var y0=Math.min.apply(null,aY),y1=Math.max.apply(null,aY);
|
| 738 |
-
var w=x1-x0,h=y1-y0; if(w<=0||h<=0) return;
|
| 739 |
-
var r=Math.min(7,h/2);
|
| 740 |
-
var nd='M '+x0+','+y0
|
| 741 |
-
+' H '+(x1-r)+' Q '+x1+','+y0+' '+x1+','+(y0+r)
|
| 742 |
-
+' V '+(y1-r)+' Q '+x1+','+y1+' '+(x1-r)+','+y1
|
| 743 |
-
+' H '+x0+' Z';
|
| 744 |
-
p.setAttribute('d',nd);
|
| 745 |
-
}});
|
| 746 |
-
}},80);
|
| 747 |
-
}}
|
| 748 |
-
|
| 749 |
-
// ── Bar V ──────────────────────────────────────────────────────────
|
| 750 |
-
// FIX: margem esquerda e inferior maiores para labels não grudarem
|
| 751 |
-
function renderBarV(item, elId) {{
|
| 752 |
-
if(!item.valores.length) return;
|
| 753 |
-
var el = document.getElementById(elId);
|
| 754 |
-
var wh = gsize(elId);
|
| 755 |
-
var maxVal = Math.max.apply(null, item.valores);
|
| 756 |
-
|
| 757 |
-
function fmtDot(v) {{
|
| 758 |
-
if(!v || v===0) return '';
|
| 759 |
-
var s = Math.round(v).toString(), r='', c=0;
|
| 760 |
-
for(var i=s.length-1;i>=0;i--){{ if(c>0&&c%3===0) r='.'+r; r=s[i]+r; c++; }}
|
| 761 |
-
return r;
|
| 762 |
-
}}
|
| 763 |
|
| 764 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 765 |
|
| 766 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 767 |
type:'bar',
|
| 768 |
x:item.labels, y:item.valores,
|
| 769 |
-
text:texts,
|
| 770 |
-
|
| 771 |
-
|
| 772 |
-
|
| 773 |
-
|
| 774 |
-
|
| 775 |
-
|
| 776 |
-
|
| 777 |
-
var layout = {{
|
| 778 |
-
width:wh[0], height:wh[1], autosize:false,
|
| 779 |
template:'plotly_white', showlegend:false,
|
| 780 |
-
font:{
|
| 781 |
-
|
| 782 |
-
|
| 783 |
-
|
| 784 |
-
|
| 785 |
-
|
| 786 |
-
|
| 787 |
-
|
| 788 |
-
|
| 789 |
-
|
| 790 |
-
|
| 791 |
-
|
| 792 |
-
|
| 793 |
-
|
| 794 |
-
|
| 795 |
-
yaxis:{{
|
| 796 |
-
tickfont:{{size:9,family:'DM Sans'}},
|
| 797 |
-
showgrid:true, gridwidth:1,
|
| 798 |
-
gridcolor:'rgba(200,200,200,0.2)',
|
| 799 |
-
range:[0, maxVal*1.35],
|
| 800 |
-
fixedrange:true
|
| 801 |
-
}},
|
| 802 |
-
hoverlabel:{{bgcolor:'white',font_size:11,font_family:'DM Sans',bordercolor:'#e0e0e0'}}
|
| 803 |
-
}};
|
| 804 |
-
|
| 805 |
el.style.overflow = 'visible';
|
| 806 |
-
if(el.parentElement) el.parentElement.style.overflow = 'visible';
|
| 807 |
-
|
| 808 |
-
|
| 809 |
-
|
| 810 |
-
|
| 811 |
-
}} else {{
|
| 812 |
Plotly.react(el, data, layout);
|
| 813 |
-
}
|
| 814 |
roundBars(el, 8, 'v');
|
| 815 |
-
}
|
| 816 |
-
|
| 817 |
-
|
| 818 |
-
function
|
| 819 |
-
setTimeout(function() {{
|
| 820 |
-
boxEl.querySelectorAll('.bars path').forEach(function(p) {{
|
| 821 |
-
var d=p.getAttribute('d'); if(!d) return;
|
| 822 |
-
var tokens=d.match(/[A-Za-z][^A-Za-z]*/g)||[];
|
| 823 |
-
var cx=0,cy=0,aX=[],aY=[];
|
| 824 |
-
tokens.forEach(function(tok) {{
|
| 825 |
-
var cmd=tok[0];
|
| 826 |
-
var pts=tok.slice(1).trim().split(/[\\s,]+/).map(Number).filter(function(n){{return !isNaN(n);}});
|
| 827 |
-
if(cmd==='M'||cmd==='L'){{aX.push(pts[0]);aY.push(pts[1]);cx=pts[0];cy=pts[1];}}
|
| 828 |
-
else if(cmd==='H'){{aX.push(pts[0]);aY.push(cy);cx=pts[0];}}
|
| 829 |
-
else if(cmd==='V'){{aX.push(cx);aY.push(pts[0]);cy=pts[0];}}
|
| 830 |
-
}});
|
| 831 |
-
if(aX.length<2) return;
|
| 832 |
-
var x0=Math.min.apply(null,aX),x1=Math.max.apply(null,aX);
|
| 833 |
-
var y0=Math.min.apply(null,aY),y1=Math.max.apply(null,aY);
|
| 834 |
-
var w=x1-x0,h=y1-y0; if(w<=0||h<=0) return;
|
| 835 |
-
var rr=Math.min(r,w/2,h/2), nd;
|
| 836 |
-
if(orientation==='h') {{
|
| 837 |
-
nd='M '+x0+','+(y0+rr)+' Q '+x0+','+y0+' '+(x0+rr)+','+y0
|
| 838 |
-
+' H '+(x1-rr)+' Q '+x1+','+y0+' '+x1+','+(y0+rr)
|
| 839 |
-
+' V '+(y1-rr)+' Q '+x1+','+y1+' '+(x1-rr)+','+y1
|
| 840 |
-
+' H '+(x0+rr)+' Q '+x0+','+y1+' '+x0+','+(y1-rr)+' Z';
|
| 841 |
-
}} else {{
|
| 842 |
-
nd='M '+x0+','+y1+' V '+(y0+rr)
|
| 843 |
-
+' Q '+x0+','+y0+' '+(x0+rr)+','+y0
|
| 844 |
-
+' H '+(x1-rr)+' Q '+x1+','+y0+' '+x1+','+(y0+rr)
|
| 845 |
-
+' V '+y1+' Z';
|
| 846 |
-
}}
|
| 847 |
-
p.setAttribute('d',nd);
|
| 848 |
-
}});
|
| 849 |
-
}},60);
|
| 850 |
-
}}
|
| 851 |
-
|
| 852 |
-
// ── Render frame ──────────────────────────────────────────────────
|
| 853 |
-
function renderFrame(f) {{
|
| 854 |
-
CARDS_CFG.forEach(function(c) {{
|
| 855 |
var cd = f.cards[c.col];
|
| 856 |
-
if(cd) document.getElementById('mv_'+c.col).textContent = cd.valor + cd.sufixo;
|
| 857 |
-
}
|
| 858 |
-
f.linha1.forEach(function(item, i) {
|
| 859 |
-
f.linha2.forEach(function(item, i) {
|
| 860 |
-
f.linha3.forEach(function(item, i) {
|
| 861 |
-
if(item.tipo===0)
|
| 862 |
-
else if(item.tipo===1) renderBarH(item,'l3_'+i);
|
| 863 |
-
else if(item.tipo===2) renderBarV(item,'l3_'+i);
|
| 864 |
-
}
|
| 865 |
-
}
|
| 866 |
-
|
| 867 |
-
|
| 868 |
-
function stepFrame() {{
|
| 869 |
renderFrame(FRAMES[frameIdx]);
|
| 870 |
frameIdx++;
|
| 871 |
-
if(frameIdx >= FRAMES.length) {
|
| 872 |
clearTimeout(animTimer);
|
| 873 |
-
cdownTimer = setTimeout(function()
|
| 874 |
-
|
| 875 |
-
startAnimation();
|
| 876 |
-
}}, PAUSE_MS);
|
| 877 |
-
}} else {{
|
| 878 |
animTimer = setTimeout(stepFrame, FRAME_MS);
|
| 879 |
-
}
|
| 880 |
-
}
|
| 881 |
|
| 882 |
-
function startAnimation() {
|
| 883 |
clearTimeout(animTimer);
|
| 884 |
clearTimeout(cdownTimer);
|
| 885 |
animTimer = setTimeout(stepFrame, FRAME_MS);
|
| 886 |
-
}
|
| 887 |
|
| 888 |
var _rt;
|
| 889 |
-
window.addEventListener('resize', function() {
|
| 890 |
clearTimeout(_rt);
|
| 891 |
-
_rt = setTimeout(function() {
|
| 892 |
-
document.querySelectorAll('.cp').forEach(function(box) {
|
| 893 |
-
if(box.data) Plotly.relayout(box, {
|
| 894 |
-
}
|
| 895 |
-
}
|
| 896 |
-
}
|
| 897 |
-
|
| 898 |
-
window.addEventListener('load', function() {
|
| 899 |
buildDOM();
|
| 900 |
-
setTimeout(function()
|
| 901 |
-
}
|
| 902 |
</script>
|
| 903 |
-
</body>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 904 |
|
| 905 |
-
components.html(html, height=8000, scrolling=
|
| 906 |
|
| 907 |
else:
|
| 908 |
-
components.html("""<!DOCTYPE html><html><body style=
|
| 909 |
-
justify-content:center;height:100dvh;font-family:sans-serif;color:#003a70;background:#f5f7fa;
|
| 910 |
-
<div style=
|
| 911 |
<p>Aguardando dados... Recarregue a página.</p></div></body></html>""",
|
| 912 |
height=4000, scrolling=False)
|
| 913 |
|
|
|
|
| 7 |
from gs_client import read_ws
|
| 8 |
|
| 9 |
st.set_page_config(layout="wide", initial_sidebar_state="collapsed", page_title="PMJA - Dashboard Recebimento")
|
| 10 |
+
|
| 11 |
+
st.markdown("""<style>
|
| 12 |
+
#MainMenu,footer,header,[data-testid="stToolbar"],[data-testid="stDecoration"],
|
| 13 |
+
[data-testid="stStatusWidget"]{display:none!important}
|
| 14 |
+
.block-container,.element-container,.stMarkdown{padding:0!important;margin:0!important;max-width:100%!important}
|
| 15 |
+
iframe{position:fixed!important;top:0!important;left:0!important;
|
| 16 |
+
width:100vw!important;height:100dvh!important;border:none!important;z-index:9999!important}
|
| 17 |
+
[data-testid="stHorizontalBlock"]{gap:0!important;margin:0!important;padding:0!important;
|
| 18 |
+
height:0!important;overflow:visible!important}
|
| 19 |
+
.main,[data-testid="stAppViewContainer"]{background:#f5f7fa!important}
|
| 20 |
+
@media(max-width:900px){
|
| 21 |
+
iframe{position:static!important;width:100%!important;height:7500px!important;z-index:1!important}
|
| 22 |
+
.main,[data-testid="stAppViewContainer"]{overflow-y:auto!important;height:auto!important}
|
| 23 |
+
.block-container{height:auto!important}
|
| 24 |
+
}
|
| 25 |
+
</style>""", unsafe_allow_html=True)
|
| 26 |
+
|
| 27 |
# ── SESSION ───────────────────────────────────────────────────────────
|
| 28 |
_cc = CookieController()
|
| 29 |
COOKIE_NAME = "pmja_session"
|
|
|
|
| 54 |
st.session_state.inicio_exibicao = time.time()
|
| 55 |
st.switch_page("pages/exp.py")
|
| 56 |
|
| 57 |
+
# ── LOADING ───────────────────────────────────────────────────────────
|
| 58 |
+
loading_placeholder = st.empty()
|
| 59 |
+
loading_placeholder.markdown("""
|
| 60 |
+
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;700;800&family=Sora:wght@800&display=swap" rel="stylesheet">
|
| 61 |
+
<style>
|
| 62 |
+
#ld{position:fixed;inset:0;z-index:999999;background:linear-gradient(135deg,#003a70,#0056A3);
|
| 63 |
+
display:flex;flex-direction:column;align-items:center;justify-content:center;font-family:'DM Sans',sans-serif}
|
| 64 |
+
#ld .lg{display:flex;align-items:center;gap:24px;margin-bottom:36px}
|
| 65 |
+
#ld img{height:34px;filter:brightness(0) invert(1);opacity:.9}
|
| 66 |
+
#ld .dv{width:1px;height:32px;background:rgba(255,255,255,.25)}
|
| 67 |
+
#ld h2{font-family:'Sora',sans-serif;font-size:clamp(15px,4vw,21px);color:#fff;font-weight:800;margin-bottom:4px;text-align:center}
|
| 68 |
+
#ld p{font-size:clamp(11px,3vw,13px);color:rgba(255,255,255,.7);font-weight:600;margin-bottom:40px;text-align:center}
|
| 69 |
+
#ld .sp{width:32px;height:32px;border:3px solid rgba(255,255,255,.2);border-top-color:#fff;
|
| 70 |
+
border-radius:50%;animation:sp .8s linear infinite;margin-bottom:24px}
|
| 71 |
+
@keyframes sp{to{transform:rotate(360deg)}}
|
| 72 |
+
#ld .bw{width:min(260px,70vw);height:4px;background:rgba(255,255,255,.15);border-radius:99px;overflow:hidden;margin-bottom:14px}
|
| 73 |
+
#ld .bf{height:100%;width:0%;background:#fff;border-radius:99px;transition:width .4s ease}
|
| 74 |
+
#ld .st{font-size:11px;color:rgba(255,255,255,.6);font-weight:600;text-align:center}
|
| 75 |
+
#ld .dt span{animation:dt 1.2s infinite;opacity:0}
|
| 76 |
+
#ld .dt span:nth-child(2){animation-delay:.2s}
|
| 77 |
+
#ld .dt span:nth-child(3){animation-delay:.4s}
|
| 78 |
+
@keyframes dt{0%,100%{opacity:0}50%{opacity:1}}
|
| 79 |
+
</style>
|
| 80 |
+
<div id="ld">
|
| 81 |
+
<div class="lg">
|
| 82 |
+
<img src="https://companieslogo.com/img/orig/AZ2.F-d26946db.png?t=1720244490">
|
| 83 |
+
<div class="dv"></div>
|
| 84 |
+
<img src="https://upload.wikimedia.org/wikipedia/commons/8/8f/Logo-engie.svg">
|
| 85 |
+
</div>
|
| 86 |
+
<div class="sp"></div>
|
| 87 |
+
<h2>PMJA — Dashboard Recebimento</h2>
|
| 88 |
+
<p>Gestão de Materiais</p>
|
| 89 |
+
<div class="bw"><div class="bf" id="ldbf"></div></div>
|
| 90 |
+
<div class="st" id="ldst">Conectando ao Google Sheets<span class="dt"><span>.</span><span>.</span><span>.</span></span></div>
|
| 91 |
+
</div>
|
| 92 |
+
<script>
|
| 93 |
+
(function(){
|
| 94 |
+
var s=[{p:18,m:'Conectando ao Google Sheets'},{p:40,m:'Carregando dados de recebimento'},
|
| 95 |
+
{p:62,m:'Processando métricas'},{p:80,m:'Montando gráficos'},{p:95,m:'Quase pronto'}];
|
| 96 |
+
var i=0;
|
| 97 |
+
function n(){
|
| 98 |
+
if(i>=s.length)return;
|
| 99 |
+
var x=s[i++];
|
| 100 |
+
var b=document.getElementById('ldbf'),t=document.getElementById('ldst');
|
| 101 |
+
if(b)b.style.width=x.p+'%';
|
| 102 |
+
if(t)t.innerHTML=x.m+'<span class="dt"><span>.</span><span>.</span><span>.</span></span>';
|
| 103 |
+
setTimeout(n,950+Math.random()*550);
|
| 104 |
+
}
|
| 105 |
+
setTimeout(n,300);
|
| 106 |
+
})();
|
| 107 |
+
</script>
|
| 108 |
+
""", unsafe_allow_html=True)
|
| 109 |
+
|
| 110 |
+
# ── LOAD DATA ─────────────────────────────────────────────────────────
|
| 111 |
+
df_recebimento = read_ws("recebimento")
|
| 112 |
+
df_rec_dados = read_ws("rec_dados")
|
| 113 |
+
loading_placeholder.empty()
|
| 114 |
|
| 115 |
# ── HELPERS ───────────────────────────────────────────────────────────
|
| 116 |
def fmt(n):
|
|
|
|
| 130 |
break
|
| 131 |
return sorted(list(sistemas_set))
|
| 132 |
|
| 133 |
+
# ── BUILD FRAMES ──────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
def build_frames(df_plot, df_rec, sistemas, cores_sistemas, anos_unicos,
|
| 135 |
mes_max_dados, mes_max_rec, metricas_linha1, metricas_linha2,
|
| 136 |
metricas_rec, mapeamentos_rec):
|
|
|
|
| 137 |
meses_pt = {1:'Jan',2:'Fev',3:'Mar',4:'Abr',5:'Mai',6:'Jun',
|
| 138 |
7:'Jul',8:'Ago',9:'Set',10:'Out',11:'Nov',12:'Dez'}
|
|
|
|
| 139 |
frames = []
|
|
|
|
| 140 |
for mes_atual in range(1, mes_max_dados + 1):
|
| 141 |
frame = {"mes": mes_atual, "cards": {}, "linha1": [], "linha2": [], "linha3": []}
|
|
|
|
|
|
|
| 142 |
df_ate = df_plot[df_plot['mes'] <= mes_atual]
|
| 143 |
for coluna, titulo, cor, sufixo in [
|
| 144 |
('qtd_descarregamentos', 'Descarregamentos', '#003a70', ''),
|
|
|
|
| 147 |
('qtd_unidades_recebidos','Unidades Recebidas','#4d9fd6',''),
|
| 148 |
('qtd_itens_por_unidade','Itens por Unidade','#80b9e5', ''),
|
| 149 |
]:
|
| 150 |
+
frame["cards"][coluna] = {"valor": fmt(df_ate[coluna].sum()),
|
| 151 |
+
"sufixo": sufixo, "cor": cor, "titulo": titulo}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 152 |
for config in metricas_linha1:
|
| 153 |
col = config['coluna']
|
| 154 |
traces = []
|
|
|
|
| 157 |
if d.empty: continue
|
| 158 |
cor = config['cores'][idx_ano % len(config['cores'])]
|
| 159 |
traces.append({
|
| 160 |
+
"ano": str(ano), "cor": cor,
|
|
|
|
| 161 |
"x": [int(x) for x in d['mes']],
|
| 162 |
"y": [float(v) for v in d[col]],
|
| 163 |
+
"text": [fmt(v) if not pd.isna(v) and v > 0 else "" for v in d[col]],
|
| 164 |
+
"fill_rgba": "rgba({},{},{},0.1)".format(int(cor[1:3],16),int(cor[3:5],16),int(cor[5:7],16))
|
| 165 |
})
|
| 166 |
frame["linha1"].append({"titulo": config['titulo'], "sufixo": config['sufixo'], "traces": traces})
|
|
|
|
|
|
|
| 167 |
for config in metricas_linha2:
|
| 168 |
col = config['coluna']
|
| 169 |
traces = []
|
|
|
|
| 172 |
if d.empty: continue
|
| 173 |
cor = config['cores'][idx_ano % len(config['cores'])]
|
| 174 |
traces.append({
|
| 175 |
+
"ano": str(ano), "cor": cor,
|
|
|
|
| 176 |
"x": [int(x) for x in d['mes']],
|
| 177 |
"y": [float(v) for v in d[col]],
|
| 178 |
+
"text": [fmt(v) if not pd.isna(v) and v > 0 else "" for v in d[col]],
|
| 179 |
+
"fill_rgba": "rgba({},{},{},0.1)".format(int(cor[1:3],16),int(cor[3:5],16),int(cor[5:7],16))
|
| 180 |
})
|
| 181 |
frame["linha2"].append({"titulo": config['titulo'], "sufixo": config['sufixo'], "traces": traces})
|
|
|
|
|
|
|
| 182 |
if df_rec is not None and mes_atual <= mes_max_rec:
|
| 183 |
df_ate_rec = df_rec[df_rec['mes'] <= mes_atual]
|
| 184 |
for idx_m, metrica_info in enumerate(metricas_rec):
|
|
|
|
| 188 |
col_nome = next((c for c, s in mapeamento.items() if s == sistema), None)
|
| 189 |
if col_nome and col_nome in df_rec.columns:
|
| 190 |
total = float(df_ate_rec[col_nome].fillna(0).sum())
|
| 191 |
+
valores.append(total); labels.append(sistema); cores.append(cores_sistemas[cat_idx])
|
| 192 |
+
frame["linha3"].append({"titulo": metrica_info['titulo'], "tipo": idx_m,
|
| 193 |
+
"valores": valores, "labels": labels, "cores": cores})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 194 |
else:
|
| 195 |
+
for idx_m, metrica_info in enumerate(metricas_rec):
|
| 196 |
+
frame["linha3"].append({"titulo": metrica_info['titulo'], "tipo": idx_m,
|
| 197 |
+
"valores": [], "labels": [], "cores": []})
|
| 198 |
frames.append(frame)
|
|
|
|
| 199 |
return frames, meses_pt
|
| 200 |
|
| 201 |
+
# ── MAIN ──────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 202 |
if df_recebimento is not None and not df_recebimento.empty:
|
| 203 |
df_plot = df_recebimento.copy()
|
| 204 |
colunas = ['mes_ano','qtd_descarregamentos','qtd_volumes_recebidos',
|
|
|
|
| 218 |
df_rec, sistemas, cores_sistemas, mes_max_rec = None, [], [], 0
|
| 219 |
mapeamentos_rec = [{}, {}, {}]
|
| 220 |
metricas_rec = [
|
| 221 |
+
{'palavra_chave': 'Volumes', 'titulo': 'Volumes Recebidos por Sistema', 'excluir': ['Unid.','unidade']},
|
| 222 |
+
{'palavra_chave': 'Unid. Itens', 'titulo': 'Unidades de Itens por Sistema', 'excluir': ['por unidade']},
|
| 223 |
+
{'palavra_chave': 'Itens por unidade', 'titulo': 'Itens por Unidade por Sistema', 'excluir': ['Unid. Itens']}
|
| 224 |
]
|
| 225 |
if df_rec_dados is not None and not df_rec_dados.empty:
|
| 226 |
df_rec = df_rec_dados.copy()
|
|
|
|
| 246 |
|
| 247 |
metricas_linha1 = [
|
| 248 |
{'coluna':'qtd_descarregamentos','titulo':'Descarregamentos',
|
| 249 |
+
'cores':['#001f3f','#0056A3','#00a8e8','#7ecfef'],'sufixo':''},
|
| 250 |
{'coluna':'peso_recebido','titulo':'Peso Recebido',
|
| 251 |
+
'cores':['#001f3f','#0056A3','#00a8e8','#7ecfef'],'sufixo':' kg'},
|
| 252 |
{'coluna':'qtd_volumes_recebidos','titulo':'Volumes Recebidos',
|
| 253 |
+
'cores':['#001f3f','#0056A3','#00a8e8','#7ecfef'],'sufixo':''}
|
| 254 |
]
|
| 255 |
metricas_linha2 = [
|
| 256 |
{'coluna':'qtd_unidades_recebidos','titulo':'Unidades Recebidas',
|
| 257 |
+
'cores':['#001f3f','#0056A3','#00a8e8','#7ecfef'],'sufixo':''},
|
| 258 |
{'coluna':'qtd_itens_por_unidade','titulo':'Itens por Unidade',
|
| 259 |
+
'cores':['#001f3f','#0056A3','#00a8e8','#7ecfef'],'sufixo':''}
|
| 260 |
]
|
| 261 |
|
| 262 |
def titulo_anos(config):
|
| 263 |
parts = []
|
| 264 |
for idx_ano, ano in enumerate(anos_unicos):
|
| 265 |
cor = config['cores'][idx_ano % len(config['cores'])]
|
| 266 |
+
total = fmt(df_plot[df_plot['ano']==ano][config['coluna']].sum())
|
| 267 |
+
parts.append("<span style='color:{};font-weight:700'>{}: {}{}</span>".format(cor, ano, total, config['sufixo']))
|
| 268 |
return " <span style='color:#666'>|</span> ".join(parts)
|
| 269 |
|
| 270 |
def titulo_sistemas(idx_m):
|
|
|
|
| 274 |
for cat_idx, sistema in enumerate(sistemas):
|
| 275 |
col_nome = next((c for c,s in mapa.items() if s==sistema), None)
|
| 276 |
if col_nome:
|
| 277 |
+
total = fmt(df_rec[col_nome].sum())
|
| 278 |
cor = cores_sistemas[cat_idx]
|
| 279 |
+
parts.append("<span style='color:{};font-weight:700'>{}: {}</span>".format(cor, sistema, total))
|
| 280 |
return " <span style='color:#666'>|</span> ".join(parts)
|
| 281 |
|
| 282 |
frames, meses_pt = build_frames(
|
|
|
|
| 285 |
metricas_rec, mapeamentos_rec
|
| 286 |
)
|
| 287 |
|
| 288 |
+
frames_json = json.dumps(frames, ensure_ascii=False)
|
| 289 |
+
meses_json = json.dumps(meses_pt)
|
| 290 |
+
anos_json = json.dumps([int(a) for a in anos_unicos])
|
|
|
|
| 291 |
sub_l1 = json.dumps([titulo_anos(c) for c in metricas_linha1])
|
| 292 |
sub_l2 = json.dumps([titulo_anos(c) for c in metricas_linha2])
|
| 293 |
sub_l3 = json.dumps([titulo_sistemas(i) for i in range(3)])
|
| 294 |
|
| 295 |
+
html = """<!DOCTYPE html>
|
| 296 |
+
<html>
|
| 297 |
+
<head>
|
| 298 |
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
| 299 |
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;600;700;800&family=Sora:wght@700;800;900&display=swap" rel="stylesheet">
|
| 300 |
<script src="https://cdn.plot.ly/plotly-2.27.0.min.js" charset="utf-8"></script>
|
| 301 |
<style>
|
| 302 |
+
* { margin:0; padding:0; box-sizing:border-box; }
|
| 303 |
+
html, body { width:100%; height:100%; overflow:hidden;
|
| 304 |
background:linear-gradient(135deg,#f5f7fa 0%,#e8eef5 100%);
|
| 305 |
+
font-family:'DM Sans',sans-serif; }
|
| 306 |
+
.root { display:grid; width:100%; height:100dvh; gap:3px; padding:4px; overflow:hidden;
|
| 307 |
+
grid-template-rows:auto auto 1fr 1fr 1fr; }
|
| 308 |
+
.hdr { background:linear-gradient(135deg,#003a70 0%,#0056A3 100%);
|
| 309 |
+
border-radius:8px; padding:4px 14px; display:flex; align-items:center;
|
| 310 |
+
justify-content:space-between; box-shadow:0 4px 12px rgba(0,58,112,.12); }
|
| 311 |
+
.logo { height:clamp(22px,3.2dvh,36px); filter:brightness(0) invert(1); opacity:.92; }
|
| 312 |
+
.ht { font-family:'Sora',sans-serif; font-size:clamp(12px,1.8dvh,20px);
|
| 313 |
+
color:#fff; font-weight:800; letter-spacing:-.5px; }
|
| 314 |
+
.hs { font-family:'DM Sans',sans-serif; font-size:clamp(9px,1.2dvh,14px);
|
| 315 |
+
color:rgba(255,255,255,.9); font-weight:700; }
|
| 316 |
+
.cards { display:grid; grid-template-columns:repeat(5,1fr); gap:3px; }
|
| 317 |
+
.mc { background:white; border-radius:8px;
|
| 318 |
+
padding:clamp(3px,.5dvh,7px) clamp(5px,.6vw,12px);
|
| 319 |
+
border-left:3px solid; box-shadow:0 2px 8px rgba(0,58,112,.06); overflow:hidden; }
|
| 320 |
+
.ml { font-family:'DM Sans',sans-serif; font-size:clamp(7px,.85dvh,11px);
|
| 321 |
+
color:#6b7280; font-weight:700; text-transform:uppercase;
|
| 322 |
+
letter-spacing:.4px; margin-bottom:2px; white-space:nowrap; }
|
| 323 |
+
.mv { font-family:'Sora',sans-serif; font-size:clamp(12px,1.9dvh,22px);
|
| 324 |
+
font-weight:800; line-height:1; letter-spacing:-.4px; white-space:nowrap; }
|
| 325 |
+
.row3 { display:grid; grid-template-columns:repeat(3,1fr); gap:3px; min-height:0; }
|
| 326 |
+
.row2 { display:grid; grid-template-columns:repeat(2,1fr); gap:3px; min-height:0; }
|
| 327 |
+
.cb { background:white; border-radius:8px; box-shadow:0 2px 8px rgba(0,58,112,.06);
|
| 328 |
+
overflow:visible; min-height:0; display:flex; flex-direction:column; }
|
| 329 |
+
.ct { background:white; padding:3px 6px 0; border-radius:8px 8px 0 0;
|
| 330 |
+
border-bottom:1px solid #f0f0f0; flex-shrink:0; }
|
| 331 |
+
.ct-title { font-family:'Sora',sans-serif; font-size:clamp(9px,1.1dvh,14px);
|
| 332 |
+
color:#1a1a1a; font-weight:800; text-align:center; }
|
| 333 |
+
.ct-sub { text-align:center; font-size:clamp(7px,.8dvh,10px);
|
| 334 |
+
font-weight:700; margin-top:1px; line-height:1.3; }
|
| 335 |
+
.cp { flex:1; min-height:0; overflow:visible; }
|
| 336 |
+
@media(max-width:900px){
|
| 337 |
+
html,body { overflow:hidden!important; }
|
| 338 |
+
.root { height:100dvh!important; overflow:hidden!important; }
|
| 339 |
+
.cards { grid-template-columns:repeat(3,1fr)!important; }
|
| 340 |
+
.row3 { grid-template-columns:repeat(2,1fr)!important; }
|
| 341 |
+
.row2 { grid-template-columns:1fr 1fr!important; }
|
| 342 |
+
.cb { min-height:0!important; }
|
| 343 |
+
.cp { min-height:0!important; }
|
| 344 |
+
}
|
| 345 |
+
@media(max-width:600px){
|
| 346 |
+
html,body { overflow-y:auto!important; height:auto!important; }
|
| 347 |
+
.root { height:auto!important; overflow:visible!important; grid-template-rows:auto!important; }
|
| 348 |
+
.cards { grid-template-columns:repeat(2,1fr)!important; }
|
| 349 |
+
.row3,.row2 { grid-template-columns:1fr!important; }
|
| 350 |
+
.cb { height:260px!important; }
|
| 351 |
+
.cp { height:210px!important; flex:none!important; }
|
| 352 |
+
.hs { display:none; }
|
| 353 |
+
.hdr { padding:4px 8px!important; }
|
| 354 |
+
}
|
| 355 |
</style>
|
| 356 |
+
</head>
|
| 357 |
+
<body>
|
| 358 |
<div class="root">
|
|
|
|
| 359 |
<div class="hdr">
|
| 360 |
<img class="logo" src="https://companieslogo.com/img/orig/AZ2.F-d26946db.png?t=1720244490" alt="AZ">
|
| 361 |
<div style="text-align:center;flex:1">
|
|
|
|
| 364 |
</div>
|
| 365 |
<img class="logo" src="https://upload.wikimedia.org/wikipedia/commons/8/8f/Logo-engie.svg" alt="Engie">
|
| 366 |
</div>
|
|
|
|
| 367 |
<div class="cards" id="cards-row"></div>
|
| 368 |
<div class="row3" id="row1"></div>
|
| 369 |
<div class="row2" id="row2"></div>
|
| 370 |
<div class="row3" id="row3"></div>
|
|
|
|
| 371 |
</div>
|
|
|
|
| 372 |
<script>
|
| 373 |
+
var FRAMES = __FRAMES__;
|
| 374 |
+
var MESES = __MESES__;
|
| 375 |
+
var ANOS = __ANOS__;
|
| 376 |
+
var SUB_L1 = __SUB_L1__;
|
| 377 |
+
var SUB_L2 = __SUB_L2__;
|
| 378 |
+
var SUB_L3 = __SUB_L3__;
|
| 379 |
|
| 380 |
var CARDS_CFG = [
|
| 381 |
+
{col:'qtd_descarregamentos', titulo:'Descarregamentos', cor:'#003a70', sufixo:''},
|
| 382 |
+
{col:'peso_recebido', titulo:'Peso Recebido', cor:'#005a9c', sufixo:' kg'},
|
| 383 |
+
{col:'qtd_volumes_recebidos',titulo:'Volumes Recebidos', cor:'#0075be', sufixo:''},
|
| 384 |
+
{col:'qtd_unidades_recebidos',titulo:'Unidades Recebidas',cor:'#4d9fd6', sufixo:''},
|
| 385 |
+
{col:'qtd_itens_por_unidade', titulo:'Itens por Unidade', cor:'#80b9e5', sufixo:''}
|
| 386 |
];
|
| 387 |
|
| 388 |
var TITULOS_L1 = ['Descarregamentos','Peso Recebido','Volumes Recebidos'];
|
| 389 |
var TITULOS_L2 = ['Unidades Recebidas','Itens por Unidade'];
|
| 390 |
var TITULOS_L3 = ['Volumes Recebidos por Sistema','Unidades de Itens por Sistema','Itens por Unidade por Sistema'];
|
| 391 |
|
| 392 |
+
var FRAME_MS = 900;
|
| 393 |
+
var PAUSE_MS = 60000;
|
| 394 |
+
var frameIdx = 0;
|
| 395 |
+
var animTimer = null;
|
| 396 |
+
var cdownTimer= null;
|
| 397 |
+
var inited = {};
|
| 398 |
|
| 399 |
+
function buildDOM() {
|
| 400 |
var cr = document.getElementById('cards-row');
|
| 401 |
+
CARDS_CFG.forEach(function(c) {
|
| 402 |
+
cr.innerHTML += '<div class="mc" style="border-left-color:'+c.cor+'">'
|
| 403 |
+
+ '<div class="ml">'+c.titulo+'</div>'
|
| 404 |
+
+ '<div class="mv" id="mv_'+c.col+'" style="color:'+c.cor+'">0'+c.sufixo+'</div></div>';
|
| 405 |
+
});
|
|
|
|
| 406 |
var r1 = document.getElementById('row1');
|
| 407 |
+
for (var i=0; i<3; i++) {
|
| 408 |
r1.innerHTML += '<div class="cb"><div class="ct">'
|
| 409 |
+
+ '<div class="ct-title">'+TITULOS_L1[i]+'</div>'
|
| 410 |
+
+ '<div class="ct-sub" id="sub_l1_'+i+'">'+SUB_L1[i]+'</div></div>'
|
| 411 |
+
+ '<div class="cp" id="l1_'+i+'"></div></div>';
|
| 412 |
+
}
|
|
|
|
| 413 |
var r2 = document.getElementById('row2');
|
| 414 |
+
for (var i=0; i<2; i++) {
|
| 415 |
r2.innerHTML += '<div class="cb"><div class="ct">'
|
| 416 |
+
+ '<div class="ct-title">'+TITULOS_L2[i]+'</div>'
|
| 417 |
+
+ '<div class="ct-sub" id="sub_l2_'+i+'">'+SUB_L2[i]+'</div></div>'
|
| 418 |
+
+ '<div class="cp" id="l2_'+i+'"></div></div>';
|
| 419 |
+
}
|
|
|
|
| 420 |
var r3 = document.getElementById('row3');
|
| 421 |
+
for (var i=0; i<3; i++) {
|
| 422 |
r3.innerHTML += '<div class="cb"><div class="ct">'
|
| 423 |
+
+ '<div class="ct-title">'+TITULOS_L3[i]+'</div>'
|
| 424 |
+
+ '<div class="ct-sub" id="sub_l3_'+i+'">'+SUB_L3[i]+'</div></div>'
|
| 425 |
+
+ '<div class="cp" id="l3_'+i+'"></div></div>';
|
| 426 |
+
}
|
| 427 |
+
}
|
| 428 |
|
| 429 |
+
function gsize(id) {
|
| 430 |
var el = document.getElementById(id);
|
| 431 |
+
if (!el) return [200,150];
|
| 432 |
return [el.clientWidth||200, el.clientHeight||150];
|
| 433 |
+
}
|
| 434 |
+
|
| 435 |
+
var tickvals = Object.keys(MESES).map(Number);
|
| 436 |
+
var ticktext = Object.values(MESES);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 437 |
|
| 438 |
+
function renderLine(traces, elId, sufixo) {
|
| 439 |
+
var wh = gsize(elId);
|
| 440 |
+
var data = [];
|
| 441 |
+
var n = traces.length;
|
| 442 |
var allY = [];
|
| 443 |
+
traces.forEach(function(t){ t.y.forEach(function(v){ if(v>=0) allY.push(v); }); });
|
| 444 |
var yMax = allY.length ? Math.max.apply(null,allY) : 1;
|
|
|
|
| 445 |
var allX = [];
|
| 446 |
+
traces.forEach(function(t){ t.x.forEach(function(v){ allX.push(v); }); });
|
| 447 |
var xMin = allX.length ? Math.min.apply(null,allX) : 1;
|
| 448 |
var xMax2 = allX.length ? Math.max.apply(null,allX) : 12;
|
|
|
|
|
|
|
|
|
|
| 449 |
var headroom = 1.40 + Math.max(0, n-1) * 0.08;
|
| 450 |
var yTop = yMax * headroom;
|
|
|
|
|
|
|
| 451 |
var MIN_OFFSET = yTop * 0.055;
|
| 452 |
|
| 453 |
+
traces.forEach(function(t, i) {
|
| 454 |
+
var rgba = t.fill_rgba;
|
| 455 |
+
data.push({
|
| 456 |
type:'scatter', mode:'lines+markers',
|
| 457 |
x:t.x, y:t.y,
|
| 458 |
cliponaxis:false, showlegend:false, name:t.ano,
|
| 459 |
+
line:{width:2.5, color:t.cor, shape:'spline'},
|
| 460 |
+
marker:{size:6, color:t.cor, line:{width:1.5, color:'white'}},
|
| 461 |
fill: i>0 ? 'tonexty' : null,
|
| 462 |
+
fillcolor: rgba,
|
| 463 |
+
hovertemplate:'<b>'+t.ano+'</b> %{customdata}: <b>%{text}</b>'+sufixo+'<extra></extra>',
|
| 464 |
+
customdata: t.x.map(function(m){ return MESES[m]; }),
|
| 465 |
text: t.text
|
| 466 |
+
});
|
| 467 |
+
});
|
| 468 |
+
|
| 469 |
+
var byX = {};
|
| 470 |
+
traces.forEach(function(t) {
|
| 471 |
+
t.x.forEach(function(mx, j) {
|
| 472 |
+
if (!t.text[j]) return;
|
| 473 |
+
if (!byX[mx]) byX[mx] = [];
|
| 474 |
+
byX[mx].push({yv:t.y[j], txt:t.text[j], cor:t.cor});
|
| 475 |
+
});
|
| 476 |
+
});
|
|
|
|
| 477 |
|
| 478 |
var annotations = [];
|
| 479 |
+
Object.keys(byX).forEach(function(mxStr) {
|
|
|
|
| 480 |
var mx = parseInt(mxStr);
|
| 481 |
var pts = byX[mx];
|
| 482 |
+
pts.sort(function(a,b){ return a.yv-b.yv; });
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 483 |
var labelY = [];
|
| 484 |
+
pts.forEach(function(p, k) {
|
| 485 |
var candidate = p.yv + MIN_OFFSET;
|
| 486 |
+
if (k > 0 && candidate < labelY[k-1] + MIN_OFFSET) candidate = labelY[k-1] + MIN_OFFSET;
|
|
|
|
|
|
|
| 487 |
labelY.push(candidate);
|
| 488 |
+
});
|
| 489 |
+
var xanchor = mx===xMin ? 'left' : (mx===xMax2 ? 'right' : 'center');
|
| 490 |
+
pts.forEach(function(p, k) {
|
| 491 |
+
annotations.push({
|
| 492 |
+
x:mx, y:labelY[k], xref:'x', yref:'y',
|
| 493 |
+
text:p.txt, showarrow:false,
|
| 494 |
+
xanchor:xanchor, yanchor:'middle',
|
| 495 |
+
font:{size:9, family:'DM Sans', color:'#111827'},
|
| 496 |
+
bgcolor:'rgba(0,0,0,0)', borderpad:0
|
| 497 |
+
});
|
| 498 |
+
});
|
| 499 |
+
});
|
| 500 |
+
|
| 501 |
+
var layout = {
|
| 502 |
+
autosize:true,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 503 |
hovermode:'x unified', template:'plotly_white',
|
| 504 |
+
font:{family:'DM Sans', size:10}, showlegend:false,
|
| 505 |
+
margin:{l:42,r:18,t:24,b:36},
|
| 506 |
plot_bgcolor:'rgba(248,249,250,0.5)',
|
| 507 |
+
xaxis:{
|
| 508 |
tickmode:'array', tickvals:tickvals, ticktext:ticktext,
|
| 509 |
+
tickangle:0, tickfont:{size:9,family:'DM Sans',color:'#555'},
|
| 510 |
showgrid:true, gridwidth:1, gridcolor:'rgba(200,200,200,0.25)',
|
| 511 |
fixedrange:true, range:[xMin-0.4, xMax2+0.4]
|
| 512 |
+
},
|
| 513 |
+
yaxis:{
|
| 514 |
+
tickfont:{size:9,family:'DM Sans'},
|
| 515 |
showgrid:true, gridwidth:1, gridcolor:'rgba(200,200,200,0.25)',
|
| 516 |
range:[0, yTop], fixedrange:true,
|
| 517 |
+
tickformat: yMax>=100000 ? '.2s' : null
|
| 518 |
+
},
|
| 519 |
+
annotations:annotations,
|
| 520 |
+
hoverlabel:{bgcolor:'white',font_size:11,font_family:'DM Sans',bordercolor:'#ddd',namelength:-1}
|
| 521 |
+
};
|
| 522 |
+
|
| 523 |
var el = document.getElementById(elId);
|
| 524 |
+
if (!inited[elId]) {
|
| 525 |
+
Plotly.newPlot(el, data, layout, {displayModeBar:false, responsive:true});
|
| 526 |
+
inited[elId] = true;
|
| 527 |
+
} else {
|
| 528 |
Plotly.react(el, data, layout);
|
| 529 |
+
}
|
| 530 |
+
}
|
|
|
|
| 531 |
|
| 532 |
+
function renderPizza(item, elId) {
|
| 533 |
+
if (!item.valores.length) return;
|
| 534 |
+
var wh = gsize(elId);
|
| 535 |
+
var total = item.valores.reduce(function(s,v){ return s+v; }, 0);
|
| 536 |
+
var labels = item.labels.map(function(l,i) {
|
|
|
|
| 537 |
var pct = total>0 ? (item.valores[i]/total*100).toFixed(1) : '0.0';
|
| 538 |
+
var vf = item.valores[i].toString().replace(/\\B(?=(\\d{3})+(?!\\d))/g,'.');
|
| 539 |
return l+': '+vf+' ('+pct+'%)';
|
| 540 |
+
});
|
| 541 |
+
var data = [{
|
| 542 |
type:'pie', labels:labels, values:item.valores,
|
| 543 |
+
marker:{colors:item.cores, line:{color:'white',width:2}},
|
| 544 |
hole:0.3, textposition:'inside',
|
| 545 |
+
hovertemplate:'<b>%{label}</b><br>Volumes: %{value:,.0f}<br>Percentual: %{percent}<extra></extra>'
|
| 546 |
+
}];
|
| 547 |
+
var layout = {
|
| 548 |
+
autosize:true,
|
| 549 |
+
template:'plotly_white', font:{family:'DM Sans',size:9}, showlegend:true,
|
| 550 |
+
legend:{orientation:'v',yanchor:'middle',y:0.5,xanchor:'left',x:1.02,font:{size:8.5}},
|
| 551 |
+
margin:{l:20,r:150,t:8,b:8},
|
| 552 |
+
hoverlabel:{bgcolor:'white',font_size:11,font_family:'DM Sans',bordercolor:'#e0e0e0'}
|
| 553 |
+
};
|
|
|
|
| 554 |
var el = document.getElementById(elId);
|
| 555 |
+
if (!inited[elId]) {
|
| 556 |
+
Plotly.newPlot(el, data, layout, {displayModeBar:false, responsive:true});
|
| 557 |
+
inited[elId] = true;
|
| 558 |
+
} else {
|
| 559 |
Plotly.react(el, data, layout);
|
| 560 |
+
}
|
| 561 |
+
}
|
| 562 |
+
|
| 563 |
+
function fmtDot(v) {
|
| 564 |
+
if (v===null||v===undefined) return '0';
|
| 565 |
+
var n = Math.round(v);
|
| 566 |
+
if (n===0) return '0';
|
| 567 |
+
var s=n.toString(), r='', c=0;
|
| 568 |
+
for (var i=s.length-1; i>=0; i--) {
|
| 569 |
+
if (c>0 && c%3===0) r='.'+r;
|
| 570 |
+
r=s[i]+r; c++;
|
| 571 |
+
}
|
| 572 |
+
return r;
|
| 573 |
+
}
|
| 574 |
|
| 575 |
+
function renderBarH(item, elId) {
|
| 576 |
+
if (!item.valores.length) return;
|
| 577 |
+
var wh = gsize(elId);
|
| 578 |
+
var maxVal = Math.max.apply(null, item.valores);
|
| 579 |
+
var pairs = item.labels.map(function(l,i){ return {l:l,v:item.valores[i],c:item.cores[i]}; });
|
| 580 |
+
pairs.sort(function(a,b){ return b.v-a.v; });
|
| 581 |
+
var sL = pairs.map(function(p){ return p.l; });
|
| 582 |
+
var sV = pairs.map(function(p){ return p.v; });
|
| 583 |
+
var sC = pairs.map(function(p){ return p.c; });
|
| 584 |
+
|
| 585 |
+
var trackColors = sC.map(function(c) {
|
| 586 |
+
var r=parseInt(c.slice(1,3),16), g=parseInt(c.slice(3,5),16), b=parseInt(c.slice(5,7),16);
|
| 587 |
+
return 'rgba('+r+','+g+','+b+',0.12)';
|
| 588 |
+
});
|
| 589 |
+
|
| 590 |
+
var trackData = {
|
| 591 |
type:'bar', orientation:'h',
|
| 592 |
+
x:sV.map(function(){ return maxVal; }), y:sL,
|
| 593 |
+
marker:{color:trackColors, line:{width:0}},
|
|
|
|
|
|
|
|
|
|
| 594 |
hoverinfo:'skip', showlegend:false,
|
| 595 |
+
text:sL.map(function(){ return ''; })
|
| 596 |
+
};
|
| 597 |
+
var barData = {
|
|
|
|
| 598 |
type:'bar', orientation:'h',
|
| 599 |
x:sV, y:sL,
|
| 600 |
+
marker:{color:sC, line:{width:0}},
|
| 601 |
+
hovertemplate:'<b>%{y}</b><br>%{x:,.0f}<extra></extra>',
|
| 602 |
showlegend:false,
|
| 603 |
+
text:sL.map(function(){ return ''; })
|
| 604 |
+
};
|
| 605 |
|
| 606 |
var threshold = maxVal * 0.20;
|
| 607 |
+
var annotations = sV.map(function(v,i) {
|
| 608 |
var isShort = v < threshold;
|
| 609 |
+
return {
|
| 610 |
+
x: isShort ? v+maxVal*0.015 : v-maxVal*0.015,
|
| 611 |
y: sL[i],
|
| 612 |
+
text:'<b>'+fmtDot(v)+'</b>',
|
| 613 |
+
xref:'x', yref:'y', showarrow:false,
|
|
|
|
| 614 |
xanchor: isShort ? 'left' : 'right',
|
| 615 |
yanchor:'middle',
|
| 616 |
+
font:{size:11, family:'DM Sans', color: isShort?'#003a70':'white'}
|
| 617 |
+
};
|
| 618 |
+
});
|
| 619 |
+
|
| 620 |
+
var layout = {
|
| 621 |
+
autosize:true,
|
| 622 |
+
barmode:'overlay', template:'plotly_white', showlegend:false,
|
| 623 |
+
font:{family:'DM Sans',size:9},
|
| 624 |
+
margin:{l:150,r:70,t:6,b:20},
|
| 625 |
+
plot_bgcolor:'rgba(0,0,0,0)', paper_bgcolor:'rgba(0,0,0,0)',
|
| 626 |
+
xaxis:{visible:false, range:[0,maxVal*1.20], fixedrange:true},
|
| 627 |
+
yaxis:{
|
| 628 |
+
tickfont:{size:10,family:'DM Sans',color:'#333'},
|
| 629 |
+
showgrid:false, showline:false, zeroline:false,
|
| 630 |
+
ticksuffix:' ', fixedrange:true, automargin:true
|
| 631 |
+
},
|
| 632 |
+
annotations:annotations,
|
| 633 |
+
hoverlabel:{bgcolor:'white',font_size:11,font_family:'DM Sans',bordercolor:'#e0e0e0'}
|
| 634 |
+
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 635 |
|
| 636 |
var el = document.getElementById(elId);
|
| 637 |
+
if (!inited[elId]) {
|
| 638 |
+
Plotly.newPlot(el, [trackData, barData], layout, {displayModeBar:false, responsive:true});
|
| 639 |
+
inited[elId] = true;
|
| 640 |
+
} else {
|
| 641 |
Plotly.react(el, [trackData, barData], layout);
|
| 642 |
+
}
|
| 643 |
+
roundBars(el, 8, 'h');
|
| 644 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 645 |
|
| 646 |
+
function roundBars(boxEl, r, orientation) {
|
| 647 |
+
setTimeout(function() {
|
| 648 |
+
boxEl.querySelectorAll('.bars path').forEach(function(p) {
|
| 649 |
+
var d = p.getAttribute('d');
|
| 650 |
+
if (!d) return;
|
| 651 |
+
var tok = d.match(/[A-Za-z][^A-Za-z]*/g) || [];
|
| 652 |
+
var cx=0, cy=0, aX=[], aY=[];
|
| 653 |
+
tok.forEach(function(t) {
|
| 654 |
+
var cmd = t[0];
|
| 655 |
+
var pts = t.slice(1).trim().split(/[\\s,]+/).map(Number).filter(function(n){ return !isNaN(n); });
|
| 656 |
+
if (cmd==='M'||cmd==='L') { aX.push(pts[0]); aY.push(pts[1]); cx=pts[0]; cy=pts[1]; }
|
| 657 |
+
else if (cmd==='H') { aX.push(pts[0]); aY.push(cy); cx=pts[0]; }
|
| 658 |
+
else if (cmd==='V') { aX.push(cx); aY.push(pts[0]); cy=pts[0]; }
|
| 659 |
+
});
|
| 660 |
+
if (aX.length < 2) return;
|
| 661 |
+
var x0=Math.min.apply(null,aX), x1=Math.max.apply(null,aX);
|
| 662 |
+
var y0=Math.min.apply(null,aY), y1=Math.max.apply(null,aY);
|
| 663 |
+
var w=x1-x0, h=y1-y0;
|
| 664 |
+
if (w<=0 || h<=0) return;
|
| 665 |
+
var rr = Math.min(r, w/2, h/2);
|
| 666 |
+
var nd;
|
| 667 |
+
if (orientation === 'h') {
|
| 668 |
+
nd = 'M '+x0+','+y0
|
| 669 |
+
+ ' H '+(x1-rr)+' Q '+x1+','+y0+' '+x1+','+(y0+rr)
|
| 670 |
+
+ ' V '+(y1-rr)+' Q '+x1+','+y1+' '+(x1-rr)+','+y1
|
| 671 |
+
+ ' H '+x0+' V '+y0+' Z';
|
| 672 |
+
} else {
|
| 673 |
+
nd = 'M '+x0+','+y1
|
| 674 |
+
+ ' H '+x1
|
| 675 |
+
+ ' V '+(y0+rr)
|
| 676 |
+
+ ' Q '+x1+','+y0+' '+(x1-rr)+','+y0
|
| 677 |
+
+ ' H '+(x0+rr)+' Q '+x0+','+y0+' '+x0+','+(y0+rr)
|
| 678 |
+
+ ' V '+y1+' Z';
|
| 679 |
+
}
|
| 680 |
+
p.setAttribute('d', nd);
|
| 681 |
+
});
|
| 682 |
+
}, 80);
|
| 683 |
+
}
|
| 684 |
|
| 685 |
+
function renderBarV(item, elId) {
|
| 686 |
+
if (!item.valores.length) return;
|
| 687 |
+
var el = document.getElementById(elId);
|
| 688 |
+
var wh = gsize(elId);
|
| 689 |
+
var maxVal = Math.max.apply(null, item.valores);
|
| 690 |
+
var texts = item.valores.map(function(v){ return v>0 ? fmtDot(v) : ''; });
|
| 691 |
+
var data = [{
|
| 692 |
type:'bar',
|
| 693 |
x:item.labels, y:item.valores,
|
| 694 |
+
text:texts, textposition:'outside', cliponaxis:false,
|
| 695 |
+
textfont:{size:10,family:'DM Sans',color:'#333'},
|
| 696 |
+
marker:{color:item.cores, line:{width:0}},
|
| 697 |
+
hovertemplate:'<b>%{x}</b><br>%{y:,.0f}<extra></extra>'
|
| 698 |
+
}];
|
| 699 |
+
var layout = {
|
| 700 |
+
autosize:true,
|
|
|
|
|
|
|
|
|
|
| 701 |
template:'plotly_white', showlegend:false,
|
| 702 |
+
font:{family:'DM Sans',size:9},
|
| 703 |
+
margin:{l:30,r:20,t:40,b:90},
|
| 704 |
+
plot_bgcolor:'rgba(248,249,250,0.5)', paper_bgcolor:'white',
|
| 705 |
+
xaxis:{
|
| 706 |
+
tickfont:{size:8,family:'DM Sans'},
|
| 707 |
+
tickangle:-35, showgrid:false, fixedrange:true,
|
| 708 |
+
automargin:true, ticklabelstandoff:8
|
| 709 |
+
},
|
| 710 |
+
yaxis:{
|
| 711 |
+
tickfont:{size:9,family:'DM Sans'},
|
| 712 |
+
showgrid:true, gridwidth:1, gridcolor:'rgba(200,200,200,0.2)',
|
| 713 |
+
range:[0, maxVal*1.35], fixedrange:true
|
| 714 |
+
},
|
| 715 |
+
hoverlabel:{bgcolor:'white',font_size:11,font_family:'DM Sans',bordercolor:'#e0e0e0'}
|
| 716 |
+
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 717 |
el.style.overflow = 'visible';
|
| 718 |
+
if (el.parentElement) el.parentElement.style.overflow = 'visible';
|
| 719 |
+
if (!inited[elId]) {
|
| 720 |
+
Plotly.newPlot(el, data, layout, {displayModeBar:false, responsive:true});
|
| 721 |
+
inited[elId] = true;
|
| 722 |
+
} else {
|
|
|
|
| 723 |
Plotly.react(el, data, layout);
|
| 724 |
+
}
|
| 725 |
roundBars(el, 8, 'v');
|
| 726 |
+
}
|
| 727 |
+
|
| 728 |
+
function renderFrame(f) {
|
| 729 |
+
CARDS_CFG.forEach(function(c) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 730 |
var cd = f.cards[c.col];
|
| 731 |
+
if (cd) document.getElementById('mv_'+c.col).textContent = cd.valor + cd.sufixo;
|
| 732 |
+
});
|
| 733 |
+
f.linha1.forEach(function(item, i) { renderLine(item.traces, 'l1_'+i, item.sufixo); });
|
| 734 |
+
f.linha2.forEach(function(item, i) { renderLine(item.traces, 'l2_'+i, item.sufixo); });
|
| 735 |
+
f.linha3.forEach(function(item, i) {
|
| 736 |
+
if (item.tipo===0) renderPizza(item, 'l3_'+i);
|
| 737 |
+
else if (item.tipo===1) renderBarH(item, 'l3_'+i);
|
| 738 |
+
else if (item.tipo===2) renderBarV(item, 'l3_'+i);
|
| 739 |
+
});
|
| 740 |
+
}
|
| 741 |
+
|
| 742 |
+
function stepFrame() {
|
|
|
|
| 743 |
renderFrame(FRAMES[frameIdx]);
|
| 744 |
frameIdx++;
|
| 745 |
+
if (frameIdx >= FRAMES.length) {
|
| 746 |
clearTimeout(animTimer);
|
| 747 |
+
cdownTimer = setTimeout(function(){ frameIdx=0; startAnimation(); }, PAUSE_MS);
|
| 748 |
+
} else {
|
|
|
|
|
|
|
|
|
|
| 749 |
animTimer = setTimeout(stepFrame, FRAME_MS);
|
| 750 |
+
}
|
| 751 |
+
}
|
| 752 |
|
| 753 |
+
function startAnimation() {
|
| 754 |
clearTimeout(animTimer);
|
| 755 |
clearTimeout(cdownTimer);
|
| 756 |
animTimer = setTimeout(stepFrame, FRAME_MS);
|
| 757 |
+
}
|
| 758 |
|
| 759 |
var _rt;
|
| 760 |
+
window.addEventListener('resize', function() {
|
| 761 |
clearTimeout(_rt);
|
| 762 |
+
_rt = setTimeout(function() {
|
| 763 |
+
document.querySelectorAll('.cp').forEach(function(box) {
|
| 764 |
+
if (box.data) Plotly.relayout(box, {width:box.clientWidth, height:box.clientHeight});
|
| 765 |
+
});
|
| 766 |
+
}, 200);
|
| 767 |
+
});
|
| 768 |
+
|
| 769 |
+
window.addEventListener('load', function() {
|
| 770 |
buildDOM();
|
| 771 |
+
setTimeout(function(){ frameIdx=0; startAnimation(); }, 400);
|
| 772 |
+
});
|
| 773 |
</script>
|
| 774 |
+
</body>
|
| 775 |
+
</html>"""
|
| 776 |
+
|
| 777 |
+
html = (html
|
| 778 |
+
.replace('__FRAMES__', frames_json)
|
| 779 |
+
.replace('__MESES__', meses_json)
|
| 780 |
+
.replace('__ANOS__', anos_json)
|
| 781 |
+
.replace('__SUB_L1__', sub_l1)
|
| 782 |
+
.replace('__SUB_L2__', sub_l2)
|
| 783 |
+
.replace('__SUB_L3__', sub_l3)
|
| 784 |
+
)
|
| 785 |
|
| 786 |
+
components.html(html, height=8000, scrolling=True)
|
| 787 |
|
| 788 |
else:
|
| 789 |
+
components.html("""<!DOCTYPE html><html><body style="display:flex;align-items:center;
|
| 790 |
+
justify-content:center;height:100dvh;font-family:sans-serif;color:#003a70;background:#f5f7fa;">
|
| 791 |
+
<div style="text-align:center"><div style="font-size:2em">📊</div>
|
| 792 |
<p>Aguardando dados... Recarregue a página.</p></div></body></html>""",
|
| 793 |
height=4000, scrolling=False)
|
| 794 |
|