Update app.py
Browse files
app.py
CHANGED
|
@@ -19,7 +19,6 @@ Funcionalidades:
|
|
| 19 |
"""
|
| 20 |
|
| 21 |
import os
|
| 22 |
-
import io
|
| 23 |
import datetime
|
| 24 |
import pandas as pd
|
| 25 |
import numpy as np
|
|
@@ -70,14 +69,12 @@ TIPO_LABEL = {
|
|
| 70 |
'DISSIM' : 'DISSIM',
|
| 71 |
}
|
| 72 |
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
# ββ Cores por categoria (header da tabela) ββββββββββββββββββββββββββββββββββββ
|
| 76 |
CAT_CORES = {
|
| 77 |
-
'EM CURSO' : ('#0D47A1', '#1565C0'),
|
| 78 |
-
'LICENCIAMENTO': ('#4A148C', '#6A1B9A'),
|
| 79 |
-
'FINALIZADO' : ('#1B5E20', '#2E7D32'),
|
| 80 |
-
'GLOBAL' : ('#212121', '#37474F'),
|
| 81 |
}
|
| 82 |
|
| 83 |
# ββ Leitura dos CSVs de categoria βββββββββββββββββββββββββββββββββββββββββββββ
|
|
@@ -217,13 +214,12 @@ def get_stats(categoria: str) -> dict:
|
|
| 217 |
def render_html_table(pivot: pd.DataFrame, categoria: str) -> str:
|
| 218 |
cor_dark, cor_mid = CAT_CORES.get(categoria, ('#212121', '#37474F'))
|
| 219 |
|
| 220 |
-
# Cores das faixas β header e cΓ©lulas
|
| 221 |
faixa_header_bg = ['#1B5E20', '#E65100', '#BF360C', '#B71C1C']
|
| 222 |
faixa_cell = [
|
| 223 |
-
('#E8F5E9', '#1B5E20'),
|
| 224 |
-
('#FFF8E1', '#E65100'),
|
| 225 |
-
('#FBE9E7', '#BF360C'),
|
| 226 |
-
('#FFEBEE', '#B71C1C'),
|
| 227 |
]
|
| 228 |
faixa_cols = [
|
| 229 |
'< 50 % [uni]',
|
|
@@ -259,24 +255,25 @@ def render_html_table(pivot: pd.DataFrame, categoria: str) -> str:
|
|
| 259 |
font-size: 12px;
|
| 260 |
text-transform: uppercase;
|
| 261 |
border-bottom: 2px solid rgba(255,255,255,0.18);
|
|
|
|
| 262 |
}}
|
| 263 |
.sla-table th.th-tipo {{
|
| 264 |
background: {cor_dark};
|
| 265 |
-
color: #
|
| 266 |
text-align: left;
|
| 267 |
min-width: 150px;
|
| 268 |
border-right: 1px solid rgba(255,255,255,0.15);
|
| 269 |
}}
|
| 270 |
.sla-table th.th-sla {{
|
| 271 |
background: {cor_mid};
|
| 272 |
-
color: #
|
| 273 |
text-align: center;
|
| 274 |
width: 80px;
|
| 275 |
border-right: 1px solid rgba(255,255,255,0.15);
|
| 276 |
}}
|
| 277 |
.sla-table th.th-total {{
|
| 278 |
background: {cor_dark};
|
| 279 |
-
color: #
|
| 280 |
text-align: center;
|
| 281 |
width: 70px;
|
| 282 |
}}
|
|
@@ -346,6 +343,7 @@ def render_html_table(pivot: pd.DataFrame, categoria: str) -> str:
|
|
| 346 |
margin-top: 2px;
|
| 347 |
text-transform: none;
|
| 348 |
letter-spacing: 0;
|
|
|
|
| 349 |
}}
|
| 350 |
</style>
|
| 351 |
<div class="sla-wrap">
|
|
@@ -356,7 +354,9 @@ def render_html_table(pivot: pd.DataFrame, categoria: str) -> str:
|
|
| 356 |
<th class="th-sla">SLA<span class="sub-label">[dias]</span></th>
|
| 357 |
"""
|
| 358 |
for label, bg in zip(faixa_labels, faixa_header_bg):
|
| 359 |
-
html += f' <th style="background:{bg};color:#
|
|
|
|
|
|
|
| 360 |
|
| 361 |
html += ' <th class="th-total">TOTAL</th>\n </tr>\n </thead>\n <tbody>\n'
|
| 362 |
|
|
@@ -369,7 +369,7 @@ def render_html_table(pivot: pd.DataFrame, categoria: str) -> str:
|
|
| 369 |
for col, (bg, fg) in zip(faixa_cols, faixa_cell):
|
| 370 |
val = int(row[col])
|
| 371 |
if val == 0:
|
| 372 |
-
html +=
|
| 373 |
else:
|
| 374 |
html += (f' <td class="td-faixa">'
|
| 375 |
f'<span class="badge" style="background:{bg};color:{fg};">{val}</span>'
|
|
@@ -385,16 +385,12 @@ def render_html_table(pivot: pd.DataFrame, categoria: str) -> str:
|
|
| 385 |
def render_kpi_html(stats: dict, categoria: str) -> str:
|
| 386 |
cor_dark, cor_mid = CAT_CORES.get(categoria, ('#212121', '#37474F'))
|
| 387 |
pct = stats['pct_ok']
|
| 388 |
-
# Cor da taxa: verde β₯ 80, Γ’mbar 60-79, vermelho < 60
|
| 389 |
if pct >= 80:
|
| 390 |
-
taxa_cor = '#1B5E20'
|
| 391 |
-
taxa_bg = '#E8F5E9'
|
| 392 |
elif pct >= 60:
|
| 393 |
-
taxa_cor = '#E65100'
|
| 394 |
-
taxa_bg = '#FFF8E1'
|
| 395 |
else:
|
| 396 |
-
taxa_cor = '#B71C1C'
|
| 397 |
-
taxa_bg = '#FFEBEE'
|
| 398 |
|
| 399 |
html = f"""
|
| 400 |
<style>
|
|
@@ -458,21 +454,15 @@ def render_kpi_html(stats: dict, categoria: str) -> str:
|
|
| 458 |
# ββ ExportaΓ§Γ΅es ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 459 |
|
| 460 |
def exportar_csv_pivot(categoria: str) -> str:
|
| 461 |
-
"""Exporta a tabela pivot (distribuiΓ§Γ£o por faixas) da categoria seleccionada."""
|
| 462 |
pivot = build_pivot(DF_GLOBAL, categoria)
|
| 463 |
-
ts
|
| 464 |
-
nome
|
| 465 |
-
path
|
| 466 |
pivot.to_csv(path, index=False, encoding='utf-8-sig', sep=';')
|
| 467 |
return path
|
| 468 |
|
| 469 |
def exportar_csv_fact(categoria: str) -> str:
|
| 470 |
-
|
| 471 |
-
if categoria == 'GLOBAL':
|
| 472 |
-
sub = DF_GLOBAL.copy()
|
| 473 |
-
else:
|
| 474 |
-
sub = DF_GLOBAL[DF_GLOBAL['CATEGORIA'] == categoria].copy()
|
| 475 |
-
|
| 476 |
fact = sub[[
|
| 477 |
'SUB-CIP', 'PROJETO', 'TIPO', 'TIPO_LABEL', 'RB STATUS', 'CATEGORIA',
|
| 478 |
'DATA_ADJ_CLIENTE', 'DATA_PREVISTA', 'TEMPO_EXECUCAO',
|
|
@@ -480,20 +470,17 @@ def exportar_csv_fact(categoria: str) -> str:
|
|
| 480 |
]].copy()
|
| 481 |
fact['DATA_ADJ_CLIENTE'] = fact['DATA_ADJ_CLIENTE'].dt.strftime('%d/%m/%Y')
|
| 482 |
fact['DATA_PREVISTA'] = fact['DATA_PREVISTA'].dt.strftime('%d/%m/%Y')
|
| 483 |
-
|
| 484 |
-
ts = datetime.datetime.now().strftime('%Y%m%d_%H%M%S')
|
| 485 |
nome = f"sla_fact_{categoria.lower().replace(' ', '_')}_{ts}.csv"
|
| 486 |
path = os.path.join(OUTPUT_DIR, nome)
|
| 487 |
fact.to_csv(path, index=False, encoding='utf-8-sig', sep=';')
|
| 488 |
return path
|
| 489 |
|
| 490 |
def exportar_excel_powerbi() -> str:
|
| 491 |
-
|
| 492 |
-
ts = datetime.datetime.now().strftime('%Y%m%d_%H%M%S')
|
| 493 |
path = os.path.join(OUTPUT_DIR, f'modelo_sla_powerbi_{ts}.xlsx')
|
| 494 |
|
| 495 |
with pd.ExcelWriter(path, engine='openpyxl') as writer:
|
| 496 |
-
# Fact table completa
|
| 497 |
fact = DF_GLOBAL[[
|
| 498 |
'SUB-CIP', 'PROJETO', 'TIPO', 'TIPO_LABEL', 'RB STATUS', 'CATEGORIA',
|
| 499 |
'DATA_ADJ_CLIENTE', 'DATA_PREVISTA', 'TEMPO_EXECUCAO',
|
|
@@ -503,40 +490,33 @@ def exportar_excel_powerbi() -> str:
|
|
| 503 |
fact['DATA_PREVISTA'] = fact['DATA_PREVISTA'].dt.strftime('%d/%m/%Y')
|
| 504 |
fact.to_excel(writer, sheet_name='Fact_Tarefas', index=False)
|
| 505 |
|
| 506 |
-
# Pivot por categoria
|
| 507 |
for cat in ['EM CURSO', 'LICENCIAMENTO', 'FINALIZADO', 'GLOBAL']:
|
| 508 |
pivot = build_pivot(DF_GLOBAL, cat)
|
| 509 |
pivot['CATEGORIA'] = cat
|
| 510 |
-
|
| 511 |
-
pivot.to_excel(writer, sheet_name=sheet, index=False)
|
| 512 |
|
| 513 |
-
|
| 514 |
-
dim_tipo = pd.DataFrame([
|
| 515 |
{'TIPO': t, 'TIPO_LABEL': TIPO_LABEL.get(t, t),
|
| 516 |
'SLA_DIAS': SLA_MAP.get(t, 0), 'ORDEM': i + 1}
|
| 517 |
for i, t in enumerate(TIPO_ORDER)
|
| 518 |
-
])
|
| 519 |
-
dim_tipo.to_excel(writer, sheet_name='Dim_Tipo', index=False)
|
| 520 |
|
| 521 |
all_status = sorted(DF_GLOBAL['RB STATUS'].dropna().unique())
|
| 522 |
-
|
| 523 |
'RB_STATUS': all_status,
|
| 524 |
'CATEGORIA': [get_categoria(s) for s in all_status],
|
| 525 |
-
})
|
| 526 |
-
dim_status.to_excel(writer, sheet_name='Dim_Status', index=False)
|
| 527 |
|
| 528 |
-
|
| 529 |
'CATEGORIA': ['EM CURSO', 'LICENCIAMENTO', 'FINALIZADO', 'GLOBAL'],
|
| 530 |
'ORDEM' : [1, 2, 3, 4],
|
| 531 |
-
})
|
| 532 |
-
dim_cat.to_excel(writer, sheet_name='Dim_Categoria', index=False)
|
| 533 |
|
| 534 |
-
|
| 535 |
'FAIXA_SLA': ['< 50 %', '50 % < X β€ 75 %', '75 % < X β€ 100 %', '> 100 %'],
|
| 536 |
'ORDEM' : [1, 2, 3, 4],
|
| 537 |
'COR_HEX' : ['#2ECC71', '#F1C40F', '#E67E22', '#E74C3C'],
|
| 538 |
-
})
|
| 539 |
-
dim_faixa.to_excel(writer, sheet_name='Dim_Faixa_SLA', index=False)
|
| 540 |
|
| 541 |
return path
|
| 542 |
|
|
@@ -548,7 +528,7 @@ def atualizar_vista(categoria: str):
|
|
| 548 |
kpi_html = render_kpi_html(stats, categoria)
|
| 549 |
return tabela_html, kpi_html
|
| 550 |
|
| 551 |
-
# ββ CSS global
|
| 552 |
CSS = """
|
| 553 |
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap');
|
| 554 |
|
|
@@ -561,28 +541,30 @@ body, .gradio-container {
|
|
| 561 |
margin: 0 auto !important;
|
| 562 |
padding: 0 !important;
|
| 563 |
}
|
| 564 |
-
/* Header */
|
| 565 |
.app-header {
|
| 566 |
background: linear-gradient(135deg, #0D47A1 0%, #1565C0 50%, #1976D2 100%);
|
| 567 |
-
color: white;
|
| 568 |
padding: 28px 36px 22px;
|
| 569 |
border-radius: 0 0 16px 16px;
|
| 570 |
margin-bottom: 24px;
|
| 571 |
box-shadow: 0 4px 20px rgba(13,71,161,0.25);
|
| 572 |
}
|
|
|
|
|
|
|
|
|
|
| 573 |
.app-header h1 {
|
| 574 |
margin: 0 0 6px;
|
| 575 |
font-size: 26px;
|
| 576 |
font-weight: 800;
|
| 577 |
letter-spacing: -0.3px;
|
|
|
|
| 578 |
}
|
| 579 |
.app-header p {
|
| 580 |
margin: 0;
|
| 581 |
font-size: 13px;
|
| 582 |
-
opacity: 0.82;
|
| 583 |
font-weight: 400;
|
|
|
|
|
|
|
| 584 |
}
|
| 585 |
-
/* Selector de categoria */
|
| 586 |
.cat-selector label {
|
| 587 |
font-weight: 700 !important;
|
| 588 |
font-size: 12px !important;
|
|
@@ -591,9 +573,7 @@ body, .gradio-container {
|
|
| 591 |
color: #546e7a !important;
|
| 592 |
margin-bottom: 8px !important;
|
| 593 |
}
|
| 594 |
-
.cat-selector .wrap {
|
| 595 |
-
gap: 10px !important;
|
| 596 |
-
}
|
| 597 |
.cat-selector input[type=radio] + span {
|
| 598 |
border-radius: 8px !important;
|
| 599 |
padding: 9px 22px !important;
|
|
@@ -611,7 +591,6 @@ body, .gradio-container {
|
|
| 611 |
border-color: #0D47A1 !important;
|
| 612 |
box-shadow: 0 4px 12px rgba(13,71,161,0.3) !important;
|
| 613 |
}
|
| 614 |
-
/* BotΓ΅es de exportaΓ§Γ£o */
|
| 615 |
.btn-export {
|
| 616 |
border-radius: 8px !important;
|
| 617 |
font-weight: 600 !important;
|
|
@@ -619,7 +598,6 @@ body, .gradio-container {
|
|
| 619 |
padding: 10px 20px !important;
|
| 620 |
transition: all 0.2s !important;
|
| 621 |
}
|
| 622 |
-
/* SecΓ§Γ£o de exportaΓ§Γ£o */
|
| 623 |
.export-section {
|
| 624 |
background: white;
|
| 625 |
border-radius: 12px;
|
|
@@ -627,7 +605,6 @@ body, .gradio-container {
|
|
| 627 |
box-shadow: 0 2px 10px rgba(0,0,0,0.06);
|
| 628 |
margin-top: 16px;
|
| 629 |
}
|
| 630 |
-
/* Legenda */
|
| 631 |
.legenda-bar {
|
| 632 |
display: flex;
|
| 633 |
gap: 20px;
|
|
@@ -639,9 +616,7 @@ body, .gradio-container {
|
|
| 639 |
margin-top: 16px;
|
| 640 |
flex-wrap: wrap;
|
| 641 |
}
|
| 642 |
-
/* Remover footer Gradio */
|
| 643 |
footer { display: none !important; }
|
| 644 |
-
/* PainΓ©is */
|
| 645 |
.gr-panel, .gr-box { border-radius: 12px !important; }
|
| 646 |
"""
|
| 647 |
|
|
@@ -652,13 +627,24 @@ N_TOTAL = len(DF_GLOBAL)
|
|
| 652 |
|
| 653 |
with gr.Blocks(title="Dashboard SLA") as demo:
|
| 654 |
|
| 655 |
-
# ββ Header ββββββββββββββββββββββββ
|
| 656 |
gr.HTML(f"""
|
| 657 |
-
<div class="app-header"
|
| 658 |
-
|
| 659 |
-
|
| 660 |
-
|
| 661 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 662 |
</p>
|
| 663 |
</div>
|
| 664 |
""")
|
|
@@ -681,22 +667,23 @@ with gr.Blocks(title="Dashboard SLA") as demo:
|
|
| 681 |
kpi_out = gr.HTML()
|
| 682 |
|
| 683 |
# ββ SecΓ§Γ£o de exportaΓ§Γ£o βββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 684 |
-
gr.HTML('<div class="export-section"><b style="font-size:13px;color:#37474F;
|
|
|
|
| 685 |
|
| 686 |
with gr.Row():
|
| 687 |
with gr.Column(scale=1):
|
| 688 |
gr.Markdown("**Pivot da categoria** β distribuiΓ§Γ£o por faixas SLA")
|
| 689 |
-
btn_pivot
|
| 690 |
file_pivot = gr.File(label="", show_label=False)
|
| 691 |
|
| 692 |
with gr.Column(scale=1):
|
| 693 |
gr.Markdown("**Dados calculados completos** β todos os campos do dash.R")
|
| 694 |
-
btn_fact
|
| 695 |
file_fact = gr.File(label="", show_label=False)
|
| 696 |
|
| 697 |
with gr.Column(scale=1):
|
| 698 |
gr.Markdown("**Modelo Power BI** β Excel com 6 sheets (Fact + Dims + Pivots)")
|
| 699 |
-
btn_excel
|
| 700 |
file_excel = gr.File(label="", show_label=False)
|
| 701 |
|
| 702 |
# ββ Legenda ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
@@ -723,29 +710,11 @@ with gr.Blocks(title="Dashboard SLA") as demo:
|
|
| 723 |
""")
|
| 724 |
|
| 725 |
# ββ Eventos ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 726 |
-
cat_selector.change(
|
| 727 |
-
|
| 728 |
-
|
| 729 |
-
|
| 730 |
-
)
|
| 731 |
-
demo.load(
|
| 732 |
-
fn=lambda: atualizar_vista('EM CURSO'),
|
| 733 |
-
outputs=[tabela_out, kpi_out],
|
| 734 |
-
)
|
| 735 |
-
btn_pivot.click(
|
| 736 |
-
fn=exportar_csv_pivot,
|
| 737 |
-
inputs=cat_selector,
|
| 738 |
-
outputs=file_pivot,
|
| 739 |
-
)
|
| 740 |
-
btn_fact.click(
|
| 741 |
-
fn=exportar_csv_fact,
|
| 742 |
-
inputs=cat_selector,
|
| 743 |
-
outputs=file_fact,
|
| 744 |
-
)
|
| 745 |
-
btn_excel.click(
|
| 746 |
-
fn=exportar_excel_powerbi,
|
| 747 |
-
outputs=file_excel,
|
| 748 |
-
)
|
| 749 |
|
| 750 |
if __name__ == '__main__':
|
| 751 |
demo.launch(
|
|
|
|
| 19 |
"""
|
| 20 |
|
| 21 |
import os
|
|
|
|
| 22 |
import datetime
|
| 23 |
import pandas as pd
|
| 24 |
import numpy as np
|
|
|
|
| 69 |
'DISSIM' : 'DISSIM',
|
| 70 |
}
|
| 71 |
|
| 72 |
+
# ββ Cores por categoria ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
|
|
|
| 73 |
CAT_CORES = {
|
| 74 |
+
'EM CURSO' : ('#0D47A1', '#1565C0'),
|
| 75 |
+
'LICENCIAMENTO': ('#4A148C', '#6A1B9A'),
|
| 76 |
+
'FINALIZADO' : ('#1B5E20', '#2E7D32'),
|
| 77 |
+
'GLOBAL' : ('#212121', '#37474F'),
|
| 78 |
}
|
| 79 |
|
| 80 |
# ββ Leitura dos CSVs de categoria βββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 214 |
def render_html_table(pivot: pd.DataFrame, categoria: str) -> str:
|
| 215 |
cor_dark, cor_mid = CAT_CORES.get(categoria, ('#212121', '#37474F'))
|
| 216 |
|
|
|
|
| 217 |
faixa_header_bg = ['#1B5E20', '#E65100', '#BF360C', '#B71C1C']
|
| 218 |
faixa_cell = [
|
| 219 |
+
('#E8F5E9', '#1B5E20'),
|
| 220 |
+
('#FFF8E1', '#E65100'),
|
| 221 |
+
('#FBE9E7', '#BF360C'),
|
| 222 |
+
('#FFEBEE', '#B71C1C'),
|
| 223 |
]
|
| 224 |
faixa_cols = [
|
| 225 |
'< 50 % [uni]',
|
|
|
|
| 255 |
font-size: 12px;
|
| 256 |
text-transform: uppercase;
|
| 257 |
border-bottom: 2px solid rgba(255,255,255,0.18);
|
| 258 |
+
color: #ffffff !important;
|
| 259 |
}}
|
| 260 |
.sla-table th.th-tipo {{
|
| 261 |
background: {cor_dark};
|
| 262 |
+
color: #ffffff !important;
|
| 263 |
text-align: left;
|
| 264 |
min-width: 150px;
|
| 265 |
border-right: 1px solid rgba(255,255,255,0.15);
|
| 266 |
}}
|
| 267 |
.sla-table th.th-sla {{
|
| 268 |
background: {cor_mid};
|
| 269 |
+
color: #ffffff !important;
|
| 270 |
text-align: center;
|
| 271 |
width: 80px;
|
| 272 |
border-right: 1px solid rgba(255,255,255,0.15);
|
| 273 |
}}
|
| 274 |
.sla-table th.th-total {{
|
| 275 |
background: {cor_dark};
|
| 276 |
+
color: #ffffff !important;
|
| 277 |
text-align: center;
|
| 278 |
width: 70px;
|
| 279 |
}}
|
|
|
|
| 343 |
margin-top: 2px;
|
| 344 |
text-transform: none;
|
| 345 |
letter-spacing: 0;
|
| 346 |
+
color: #ffffff !important;
|
| 347 |
}}
|
| 348 |
</style>
|
| 349 |
<div class="sla-wrap">
|
|
|
|
| 354 |
<th class="th-sla">SLA<span class="sub-label">[dias]</span></th>
|
| 355 |
"""
|
| 356 |
for label, bg in zip(faixa_labels, faixa_header_bg):
|
| 357 |
+
html += (f' <th style="background:{bg};color:#ffffff !important;'
|
| 358 |
+
f'text-align:center;min-width:90px;">'
|
| 359 |
+
f'{label}<span class="sub-label">[uni]</span></th>\n')
|
| 360 |
|
| 361 |
html += ' <th class="th-total">TOTAL</th>\n </tr>\n </thead>\n <tbody>\n'
|
| 362 |
|
|
|
|
| 369 |
for col, (bg, fg) in zip(faixa_cols, faixa_cell):
|
| 370 |
val = int(row[col])
|
| 371 |
if val == 0:
|
| 372 |
+
html += ' <td class="td-faixa"><span class="badge badge-zero">β</span></td>\n'
|
| 373 |
else:
|
| 374 |
html += (f' <td class="td-faixa">'
|
| 375 |
f'<span class="badge" style="background:{bg};color:{fg};">{val}</span>'
|
|
|
|
| 385 |
def render_kpi_html(stats: dict, categoria: str) -> str:
|
| 386 |
cor_dark, cor_mid = CAT_CORES.get(categoria, ('#212121', '#37474F'))
|
| 387 |
pct = stats['pct_ok']
|
|
|
|
| 388 |
if pct >= 80:
|
| 389 |
+
taxa_cor, taxa_bg = '#1B5E20', '#E8F5E9'
|
|
|
|
| 390 |
elif pct >= 60:
|
| 391 |
+
taxa_cor, taxa_bg = '#E65100', '#FFF8E1'
|
|
|
|
| 392 |
else:
|
| 393 |
+
taxa_cor, taxa_bg = '#B71C1C', '#FFEBEE'
|
|
|
|
| 394 |
|
| 395 |
html = f"""
|
| 396 |
<style>
|
|
|
|
| 454 |
# ββ ExportaΓ§Γ΅es ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 455 |
|
| 456 |
def exportar_csv_pivot(categoria: str) -> str:
|
|
|
|
| 457 |
pivot = build_pivot(DF_GLOBAL, categoria)
|
| 458 |
+
ts = datetime.datetime.now().strftime('%Y%m%d_%H%M%S')
|
| 459 |
+
nome = f"sla_pivot_{categoria.lower().replace(' ', '_')}_{ts}.csv"
|
| 460 |
+
path = os.path.join(OUTPUT_DIR, nome)
|
| 461 |
pivot.to_csv(path, index=False, encoding='utf-8-sig', sep=';')
|
| 462 |
return path
|
| 463 |
|
| 464 |
def exportar_csv_fact(categoria: str) -> str:
|
| 465 |
+
sub = DF_GLOBAL.copy() if categoria == 'GLOBAL' else DF_GLOBAL[DF_GLOBAL['CATEGORIA'] == categoria].copy()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 466 |
fact = sub[[
|
| 467 |
'SUB-CIP', 'PROJETO', 'TIPO', 'TIPO_LABEL', 'RB STATUS', 'CATEGORIA',
|
| 468 |
'DATA_ADJ_CLIENTE', 'DATA_PREVISTA', 'TEMPO_EXECUCAO',
|
|
|
|
| 470 |
]].copy()
|
| 471 |
fact['DATA_ADJ_CLIENTE'] = fact['DATA_ADJ_CLIENTE'].dt.strftime('%d/%m/%Y')
|
| 472 |
fact['DATA_PREVISTA'] = fact['DATA_PREVISTA'].dt.strftime('%d/%m/%Y')
|
| 473 |
+
ts = datetime.datetime.now().strftime('%Y%m%d_%H%M%S')
|
|
|
|
| 474 |
nome = f"sla_fact_{categoria.lower().replace(' ', '_')}_{ts}.csv"
|
| 475 |
path = os.path.join(OUTPUT_DIR, nome)
|
| 476 |
fact.to_csv(path, index=False, encoding='utf-8-sig', sep=';')
|
| 477 |
return path
|
| 478 |
|
| 479 |
def exportar_excel_powerbi() -> str:
|
| 480 |
+
ts = datetime.datetime.now().strftime('%Y%m%d_%H%M%S')
|
|
|
|
| 481 |
path = os.path.join(OUTPUT_DIR, f'modelo_sla_powerbi_{ts}.xlsx')
|
| 482 |
|
| 483 |
with pd.ExcelWriter(path, engine='openpyxl') as writer:
|
|
|
|
| 484 |
fact = DF_GLOBAL[[
|
| 485 |
'SUB-CIP', 'PROJETO', 'TIPO', 'TIPO_LABEL', 'RB STATUS', 'CATEGORIA',
|
| 486 |
'DATA_ADJ_CLIENTE', 'DATA_PREVISTA', 'TEMPO_EXECUCAO',
|
|
|
|
| 490 |
fact['DATA_PREVISTA'] = fact['DATA_PREVISTA'].dt.strftime('%d/%m/%Y')
|
| 491 |
fact.to_excel(writer, sheet_name='Fact_Tarefas', index=False)
|
| 492 |
|
|
|
|
| 493 |
for cat in ['EM CURSO', 'LICENCIAMENTO', 'FINALIZADO', 'GLOBAL']:
|
| 494 |
pivot = build_pivot(DF_GLOBAL, cat)
|
| 495 |
pivot['CATEGORIA'] = cat
|
| 496 |
+
pivot.to_excel(writer, sheet_name=cat.replace(' ', '_')[:31], index=False)
|
|
|
|
| 497 |
|
| 498 |
+
pd.DataFrame([
|
|
|
|
| 499 |
{'TIPO': t, 'TIPO_LABEL': TIPO_LABEL.get(t, t),
|
| 500 |
'SLA_DIAS': SLA_MAP.get(t, 0), 'ORDEM': i + 1}
|
| 501 |
for i, t in enumerate(TIPO_ORDER)
|
| 502 |
+
]).to_excel(writer, sheet_name='Dim_Tipo', index=False)
|
|
|
|
| 503 |
|
| 504 |
all_status = sorted(DF_GLOBAL['RB STATUS'].dropna().unique())
|
| 505 |
+
pd.DataFrame({
|
| 506 |
'RB_STATUS': all_status,
|
| 507 |
'CATEGORIA': [get_categoria(s) for s in all_status],
|
| 508 |
+
}).to_excel(writer, sheet_name='Dim_Status', index=False)
|
|
|
|
| 509 |
|
| 510 |
+
pd.DataFrame({
|
| 511 |
'CATEGORIA': ['EM CURSO', 'LICENCIAMENTO', 'FINALIZADO', 'GLOBAL'],
|
| 512 |
'ORDEM' : [1, 2, 3, 4],
|
| 513 |
+
}).to_excel(writer, sheet_name='Dim_Categoria', index=False)
|
|
|
|
| 514 |
|
| 515 |
+
pd.DataFrame({
|
| 516 |
'FAIXA_SLA': ['< 50 %', '50 % < X β€ 75 %', '75 % < X β€ 100 %', '> 100 %'],
|
| 517 |
'ORDEM' : [1, 2, 3, 4],
|
| 518 |
'COR_HEX' : ['#2ECC71', '#F1C40F', '#E67E22', '#E74C3C'],
|
| 519 |
+
}).to_excel(writer, sheet_name='Dim_Faixa_SLA', index=False)
|
|
|
|
| 520 |
|
| 521 |
return path
|
| 522 |
|
|
|
|
| 528 |
kpi_html = render_kpi_html(stats, categoria)
|
| 529 |
return tabela_html, kpi_html
|
| 530 |
|
| 531 |
+
# ββ CSS global βββββββββββοΏ½οΏ½οΏ½ββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 532 |
CSS = """
|
| 533 |
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap');
|
| 534 |
|
|
|
|
| 541 |
margin: 0 auto !important;
|
| 542 |
padding: 0 !important;
|
| 543 |
}
|
|
|
|
| 544 |
.app-header {
|
| 545 |
background: linear-gradient(135deg, #0D47A1 0%, #1565C0 50%, #1976D2 100%);
|
|
|
|
| 546 |
padding: 28px 36px 22px;
|
| 547 |
border-radius: 0 0 16px 16px;
|
| 548 |
margin-bottom: 24px;
|
| 549 |
box-shadow: 0 4px 20px rgba(13,71,161,0.25);
|
| 550 |
}
|
| 551 |
+
.app-header * {
|
| 552 |
+
color: #ffffff !important;
|
| 553 |
+
}
|
| 554 |
.app-header h1 {
|
| 555 |
margin: 0 0 6px;
|
| 556 |
font-size: 26px;
|
| 557 |
font-weight: 800;
|
| 558 |
letter-spacing: -0.3px;
|
| 559 |
+
color: #ffffff !important;
|
| 560 |
}
|
| 561 |
.app-header p {
|
| 562 |
margin: 0;
|
| 563 |
font-size: 13px;
|
|
|
|
| 564 |
font-weight: 400;
|
| 565 |
+
color: #ffffff !important;
|
| 566 |
+
opacity: 0.92;
|
| 567 |
}
|
|
|
|
| 568 |
.cat-selector label {
|
| 569 |
font-weight: 700 !important;
|
| 570 |
font-size: 12px !important;
|
|
|
|
| 573 |
color: #546e7a !important;
|
| 574 |
margin-bottom: 8px !important;
|
| 575 |
}
|
| 576 |
+
.cat-selector .wrap { gap: 10px !important; }
|
|
|
|
|
|
|
| 577 |
.cat-selector input[type=radio] + span {
|
| 578 |
border-radius: 8px !important;
|
| 579 |
padding: 9px 22px !important;
|
|
|
|
| 591 |
border-color: #0D47A1 !important;
|
| 592 |
box-shadow: 0 4px 12px rgba(13,71,161,0.3) !important;
|
| 593 |
}
|
|
|
|
| 594 |
.btn-export {
|
| 595 |
border-radius: 8px !important;
|
| 596 |
font-weight: 600 !important;
|
|
|
|
| 598 |
padding: 10px 20px !important;
|
| 599 |
transition: all 0.2s !important;
|
| 600 |
}
|
|
|
|
| 601 |
.export-section {
|
| 602 |
background: white;
|
| 603 |
border-radius: 12px;
|
|
|
|
| 605 |
box-shadow: 0 2px 10px rgba(0,0,0,0.06);
|
| 606 |
margin-top: 16px;
|
| 607 |
}
|
|
|
|
| 608 |
.legenda-bar {
|
| 609 |
display: flex;
|
| 610 |
gap: 20px;
|
|
|
|
| 616 |
margin-top: 16px;
|
| 617 |
flex-wrap: wrap;
|
| 618 |
}
|
|
|
|
| 619 |
footer { display: none !important; }
|
|
|
|
| 620 |
.gr-panel, .gr-box { border-radius: 12px !important; }
|
| 621 |
"""
|
| 622 |
|
|
|
|
| 627 |
|
| 628 |
with gr.Blocks(title="Dashboard SLA") as demo:
|
| 629 |
|
| 630 |
+
# ββ Header β texto sempre branco via inline style ββββββββββββββββββββββββ
|
| 631 |
gr.HTML(f"""
|
| 632 |
+
<div class="app-header" style="
|
| 633 |
+
background: linear-gradient(135deg, #0D47A1 0%, #1565C0 50%, #1976D2 100%);
|
| 634 |
+
padding: 28px 36px 22px;
|
| 635 |
+
border-radius: 0 0 16px 16px;
|
| 636 |
+
margin-bottom: 24px;
|
| 637 |
+
box-shadow: 0 4px 20px rgba(13,71,161,0.25);
|
| 638 |
+
">
|
| 639 |
+
<h1 style="margin:0 0 6px;font-size:26px;font-weight:800;letter-spacing:-0.3px;
|
| 640 |
+
color:#ffffff !important;font-family:'Inter','Segoe UI',Arial,sans-serif;">
|
| 641 |
+
π Dashboard SLA β Acompanhamento de Tarefas
|
| 642 |
+
</h1>
|
| 643 |
+
<p style="margin:0;font-size:13px;font-weight:400;
|
| 644 |
+
color:#ffffff !important;opacity:0.92;
|
| 645 |
+
font-family:'Inter','Segoe UI',Arial,sans-serif;">
|
| 646 |
+
Controlo SLA por tipo de tarefa Β· DistribuiΓ§Γ£o por faixas de percentagem
|
| 647 |
+
Β· {N_TOTAL} registos Β· ReferΓͺncia: {DATA_REF}
|
| 648 |
</p>
|
| 649 |
</div>
|
| 650 |
""")
|
|
|
|
| 667 |
kpi_out = gr.HTML()
|
| 668 |
|
| 669 |
# ββ SecΓ§Γ£o de exportaΓ§Γ£o βββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 670 |
+
gr.HTML('<div class="export-section"><b style="font-size:13px;color:#37474F;'
|
| 671 |
+
'text-transform:uppercase;letter-spacing:0.6px;">β¬ Exportar Dados</b></div>')
|
| 672 |
|
| 673 |
with gr.Row():
|
| 674 |
with gr.Column(scale=1):
|
| 675 |
gr.Markdown("**Pivot da categoria** β distribuiΓ§Γ£o por faixas SLA")
|
| 676 |
+
btn_pivot = gr.Button("β¬ CSV β Tabela Pivot", variant="secondary", elem_classes=["btn-export"])
|
| 677 |
file_pivot = gr.File(label="", show_label=False)
|
| 678 |
|
| 679 |
with gr.Column(scale=1):
|
| 680 |
gr.Markdown("**Dados calculados completos** β todos os campos do dash.R")
|
| 681 |
+
btn_fact = gr.Button("β¬ CSV β Dados Calculados", variant="secondary", elem_classes=["btn-export"])
|
| 682 |
file_fact = gr.File(label="", show_label=False)
|
| 683 |
|
| 684 |
with gr.Column(scale=1):
|
| 685 |
gr.Markdown("**Modelo Power BI** β Excel com 6 sheets (Fact + Dims + Pivots)")
|
| 686 |
+
btn_excel = gr.Button("β¬ Excel β Power BI", variant="primary", elem_classes=["btn-export"])
|
| 687 |
file_excel = gr.File(label="", show_label=False)
|
| 688 |
|
| 689 |
# ββ Legenda ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 710 |
""")
|
| 711 |
|
| 712 |
# ββ Eventos ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 713 |
+
cat_selector.change(fn=atualizar_vista, inputs=cat_selector, outputs=[tabela_out, kpi_out])
|
| 714 |
+
demo.load(fn=lambda: atualizar_vista('EM CURSO'), outputs=[tabela_out, kpi_out])
|
| 715 |
+
btn_pivot.click(fn=exportar_csv_pivot, inputs=cat_selector, outputs=file_pivot)
|
| 716 |
+
btn_fact.click(fn=exportar_csv_fact, inputs=cat_selector, outputs=file_fact)
|
| 717 |
+
btn_excel.click(fn=exportar_excel_powerbi, outputs=file_excel)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 718 |
|
| 719 |
if __name__ == '__main__':
|
| 720 |
demo.launch(
|