roundb commited on
Commit
8f08a72
Β·
verified Β·
1 Parent(s): 728d0d5

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +71 -102
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
- FAIXAS = ['< 50 %', '50 % < X ≀ 75 %', '75 % < X ≀ 100 %', '> 100 %']
74
-
75
- # ── Cores por categoria (header da tabela) ────────────────────────────────────
76
  CAT_CORES = {
77
- 'EM CURSO' : ('#0D47A1', '#1565C0'), # azul escuro, azul mΓ©dio
78
- 'LICENCIAMENTO': ('#4A148C', '#6A1B9A'), # roxo escuro, roxo mΓ©dio
79
- 'FINALIZADO' : ('#1B5E20', '#2E7D32'), # verde escuro, verde mΓ©dio
80
- 'GLOBAL' : ('#212121', '#37474F'), # cinza escuro, cinza mΓ©dio
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'), # verde
224
- ('#FFF8E1', '#E65100'), # Γ’mbar
225
- ('#FBE9E7', '#BF360C'), # laranja escuro
226
- ('#FFEBEE', '#B71C1C'), # vermelho
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: #fff;
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: #fff;
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: #fff;
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:#fff;text-align:center;min-width:90px;">{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,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 += f' <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,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 = datetime.datetime.now().strftime('%Y%m%d_%H%M%S')
464
- nome = f"sla_pivot_{categoria.lower().replace(' ', '_')}_{ts}.csv"
465
- path = os.path.join(OUTPUT_DIR, nome)
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
- """Exporta a tabela de factos completa (todos os campos calculados) filtrada pela categoria."""
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
- """Exporta Excel completo com 6 sheets para Power BI."""
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
- sheet = cat.replace(' ', '_')[:31]
511
- pivot.to_excel(writer, sheet_name=sheet, index=False)
512
 
513
- # DimensΓ΅es
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
- dim_status = pd.DataFrame({
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
- dim_cat = pd.DataFrame({
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
- dim_faixa = pd.DataFrame({
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 da aplicaΓ§Γ£o ────────────────────────────────────────────────────
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
- <h1>πŸ“Š Dashboard SLA β€” Acompanhamento de Tarefas</h1>
659
- <p>
660
- Controlo SLA por tipo de tarefa Β· DistribuiΓ§Γ£o por faixas de percentagem Β·
661
- {N_TOTAL} registos Β· ReferΓͺncia: {DATA_REF}
 
 
 
 
 
 
 
 
 
 
 
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;text-transform:uppercase;letter-spacing:0.6px;">⬇ Exportar Dados</b></div>')
 
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 = gr.Button("⬇ CSV β€” Tabela Pivot", variant="secondary", elem_classes=["btn-export"])
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 = gr.Button("⬇ CSV β€” Dados Calculados", variant="secondary", elem_classes=["btn-export"])
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 = gr.Button("⬇ Excel β€” Power BI", variant="primary", elem_classes=["btn-export"])
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
- fn=atualizar_vista,
728
- inputs=cat_selector,
729
- outputs=[tabela_out, kpi_out],
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 &nbsp;Β·&nbsp; DistribuiΓ§Γ£o por faixas de percentagem
647
+ &nbsp;Β·&nbsp; {N_TOTAL} registos &nbsp;Β·&nbsp; 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(