Radioterapia-AI commited on
Commit
2e3fbd3
·
verified ·
1 Parent(s): b1e51e0

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +116 -25
app.py CHANGED
@@ -100,7 +100,7 @@ TAM_HEADER = Pt(9)
100
  TAM_TINY = Pt(8)
101
 
102
  # Página A4
103
- MARGEM_SUP = Cm(5.0) # v4.1: aumentado para 5.0cm mais respiro header↔corpo
104
  MARGEM_INF = Cm(3.0)
105
  MARGEM_ESQ = Cm(2.5)
106
  MARGEM_DIR = Cm(2.0)
@@ -153,6 +153,35 @@ def fmt(p, text, bold=False, italic=False, size=None, color=None, font=FONTE, ca
153
  if caps: run.font.all_caps = True
154
  return run
155
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
156
  def spacing(p, before=0, after=0, line=1.15):
157
  pf = p.paragraph_format
158
  pf.space_before = Pt(before); pf.space_after = Pt(after); pf.line_spacing = line
@@ -260,17 +289,26 @@ def build_footer(section, meta, pal):
260
  else:
261
  fmt(c.paragraphs[0], "________________", size=TAM_SMALL, color="AAAAAA")
262
 
263
- # Página
264
  pp = footer.add_paragraph()
265
  pp.alignment = WD_ALIGN_PARAGRAPH.RIGHT
266
  spacing(pp, 2, 0)
267
  fmt(pp, "Página ", size=TAM_TINY, color="888888")
 
268
  for x in [f'<w:fldChar {nsdecls("w")} w:fldCharType="begin"/>',
269
  f'<w:instrText {nsdecls("w")} xml:space="preserve"> PAGE </w:instrText>',
270
  f'<w:fldChar {nsdecls("w")} w:fldCharType="separate"/>']:
271
  pp.add_run()._r.append(parse_xml(x))
272
  r = pp.add_run("1"); r.font.size = TAM_TINY; r.font.color.rgb = RGBColor.from_string("888888")
273
  pp.add_run()._r.append(parse_xml(f'<w:fldChar {nsdecls("w")} w:fldCharType="end"/>'))
 
 
 
 
 
 
 
 
274
  fmt(pp, f" | {meta.get('codigo','POP-XXX-001')} | v{meta.get('versao','01')}",
275
  size=TAM_TINY, color="888888")
276
 
@@ -308,13 +346,18 @@ def add_bullet(doc, text, bold_prefix=None, indent=IND_ITEM, pal=None):
308
  else:
309
  fmt(p, text, size=TAM_CORPO, color="1A1A1A")
310
 
311
- def add_num(doc, n, text, indent=IND_SUBITEM, pal=None):
312
  c = pal["primary"] if pal else DEFAULT_PRIMARY
313
  p = doc.add_paragraph(); p.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
314
  spacing(p, 1, 4, 1.15)
315
  p.paragraph_format.left_indent = Cm(indent); p.paragraph_format.first_line_indent = Cm(-0.5)
316
- fmt(p, f"{n}. ", bold=True, size=TAM_CORPO, color=c)
317
- fmt(p, text, size=TAM_CORPO, color="1A1A1A")
 
 
 
 
 
318
 
319
  def add_def_item(doc, termo, defn, pal):
320
  p = doc.add_paragraph(); p.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
@@ -520,27 +563,46 @@ def gerar_diagrama_png(tipo, conteudo, titulo, pal):
520
  edge_style = {'color': f'#{pal["primary"]}', 'penwidth': '1.0', 'arrowsize': '0.6'}
521
 
522
  if tipo in ("processo", "fluxograma"):
523
- etapas = conteudo if isinstance(conteudo, list) else [conteudo]
524
- etapas = etapas[:9] # Max 9 steps (3 rows of 3)
525
- n = len(etapas)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
526
  if n <= 3:
527
- # Simple horizontal
528
  dot.attr(rankdir='LR', size='8,2!')
529
- for i, et in enumerate(etapas):
530
- st = node_style if i % 2 == 0 else node_light
531
- dot.node(f'n{i}', et[:30], **st)
 
532
  if i > 0:
533
  dot.edge(f'n{i-1}', f'n{i}', **edge_style)
534
  else:
535
- # Zigzag: rows of 3, TB layout
536
  dot.attr(rankdir='TB', size='8,6!')
537
  per_row = 3
538
- for i, et in enumerate(etapas):
539
- st = node_style if i % 2 == 0 else node_light
540
- dot.node(f'n{i}', et[:30], **st)
 
541
  if i > 0:
542
  dot.edge(f'n{i-1}', f'n{i}', **edge_style)
543
- # Group nodes into ranks (rows of 3)
544
  for row_start in range(0, n, per_row):
545
  row_nodes = ' '.join(f'n{i}' for i in range(row_start, min(row_start + per_row, n)))
546
  dot.body.append(f' {{ rank=same; {row_nodes} }}')
@@ -631,19 +693,29 @@ def _inserir_diagrama_no_corpo(doc, img_path, legenda="", pal=None):
631
 
632
 
633
  def _render_passo(doc, num, item, pal):
634
- """Renderiza um item de procedimento: string (texto) ou dict (diagrama inline).
635
- Aceita: {"diagrama": {"tipo":..., "titulo":..., "conteudo":...}}
636
- Ou: {"tipo":..., "titulo":..., "conteudo":...}
 
 
 
637
  """
638
  if isinstance(item, str):
639
  add_num(doc, num, item, pal=pal)
640
  elif isinstance(item, dict):
641
- # Extrair dados do diagrama (com ou sem wrapper "diagrama")
 
 
 
 
 
 
 
642
  diag = item.get("diagrama", None)
643
  if diag is None and "tipo" in item:
644
- diag = item # Formato direto sem wrapper
645
  if diag is None:
646
- # Dict sem formato reconhecido — renderizar como texto
647
  add_num(doc, num, str(item), pal=pal)
648
  return
649
  tipo = diag.get("tipo", "processo")
@@ -653,7 +725,6 @@ def _render_passo(doc, num, item, pal):
653
  if img_path:
654
  _inserir_diagrama_no_corpo(doc, img_path, titulo, pal)
655
  else:
656
- # Fallback: texto descritivo
657
  items_txt = ', '.join(str(c) for c in conteudo) if isinstance(conteudo, list) else str(conteudo)
658
  add_num(doc, num, f"[{titulo}]: {items_txt}", pal=pal)
659
 
@@ -961,6 +1032,8 @@ def gerar_pop_docx(json_str, primary_color=None, logo_bytes=None, palette_overri
961
  # Seções
962
  sn = 1
963
  add_h1(doc, sn, "OBJETIVO / FINALIDADE", pal)
 
 
964
  add_body(doc, secoes.get("objetivo", "[Objetivo não fornecido]"))
965
 
966
  sn += 1; add_h1(doc, sn, "CAMPO DE APLICAÇÃO / ÁREA", pal)
@@ -998,7 +1071,25 @@ def gerar_pop_docx(json_str, primary_color=None, logo_bytes=None, palette_overri
998
  if isinstance(it, dict): add_risk(doc, it.get("risco",""), it.get("barreira",""), pal)
999
  else: add_bullet(doc, str(it), pal=pal)
1000
  add_h2(doc, f"{sn}.2", "Plano de Contingência", pal)
1001
- add_body(doc, riscos.get("contingencia","[Contingência não fornecida]"), indent=IND_SUBITEM)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1002
 
1003
  sn += 1; add_h1(doc, sn, "REGISTROS DA QUALIDADE (EVIDÊNCIAS)", pal)
1004
  add_body(doc, "A execução deste procedimento e eventuais intercorrências devem ser "
 
100
  TAM_TINY = Pt(8)
101
 
102
  # Página A4
103
+ MARGEM_SUP = Cm(6.0) # 6cmespaço uniforme header↔corpo em todas as páginas
104
  MARGEM_INF = Cm(3.0)
105
  MARGEM_ESQ = Cm(2.5)
106
  MARGEM_DIR = Cm(2.0)
 
153
  if caps: run.font.all_caps = True
154
  return run
155
 
156
+
157
+ def _add_highlight_to_run(run, highlight_color="cyan"):
158
+ """Aplica highlight (fundo colorido) a um run via XML."""
159
+ rPr = run._r.get_or_add_rPr()
160
+ rPr.append(parse_xml(f'<w:highlight {nsdecls("w")} w:val="{highlight_color}"/>'))
161
+
162
+
163
+ def fmt_with_meta_badges(p, text, size=None, color="1A1A1A", font=FONTE):
164
+ """Renderiza texto com badges visuais para Metas OMS (Meta 1..Meta 6).
165
+ Metas ficam em bold + highlight cyan para destaque visual em auditoria."""
166
+ import re
167
+ pattern = re.compile(r'(Meta\s+[1-6])', re.IGNORECASE)
168
+ parts = pattern.split(text)
169
+ for part in parts:
170
+ if pattern.match(part):
171
+ # Badge: bold + highlight
172
+ run = p.add_run(part)
173
+ run.bold = True
174
+ run.font.name = font
175
+ if size: run.font.size = size
176
+ run.font.color.rgb = RGBColor.from_string("1A3A6E")
177
+ _add_highlight_to_run(run, "cyan")
178
+ else:
179
+ if part:
180
+ run = p.add_run(part)
181
+ run.font.name = font
182
+ if size: run.font.size = size
183
+ if color: run.font.color.rgb = RGBColor.from_string(color)
184
+
185
  def spacing(p, before=0, after=0, line=1.15):
186
  pf = p.paragraph_format
187
  pf.space_before = Pt(before); pf.space_after = Pt(after); pf.line_spacing = line
 
289
  else:
290
  fmt(c.paragraphs[0], "________________", size=TAM_SMALL, color="AAAAAA")
291
 
292
+ # Página X de Y
293
  pp = footer.add_paragraph()
294
  pp.alignment = WD_ALIGN_PARAGRAPH.RIGHT
295
  spacing(pp, 2, 0)
296
  fmt(pp, "Página ", size=TAM_TINY, color="888888")
297
+ # Campo PAGE (número atual)
298
  for x in [f'<w:fldChar {nsdecls("w")} w:fldCharType="begin"/>',
299
  f'<w:instrText {nsdecls("w")} xml:space="preserve"> PAGE </w:instrText>',
300
  f'<w:fldChar {nsdecls("w")} w:fldCharType="separate"/>']:
301
  pp.add_run()._r.append(parse_xml(x))
302
  r = pp.add_run("1"); r.font.size = TAM_TINY; r.font.color.rgb = RGBColor.from_string("888888")
303
  pp.add_run()._r.append(parse_xml(f'<w:fldChar {nsdecls("w")} w:fldCharType="end"/>'))
304
+ # " de " + NUMPAGES (total)
305
+ fmt(pp, " de ", size=TAM_TINY, color="888888")
306
+ for x in [f'<w:fldChar {nsdecls("w")} w:fldCharType="begin"/>',
307
+ f'<w:instrText {nsdecls("w")} xml:space="preserve"> NUMPAGES </w:instrText>',
308
+ f'<w:fldChar {nsdecls("w")} w:fldCharType="separate"/>']:
309
+ pp.add_run()._r.append(parse_xml(x))
310
+ r2 = pp.add_run("1"); r2.font.size = TAM_TINY; r2.font.color.rgb = RGBColor.from_string("888888")
311
+ pp.add_run()._r.append(parse_xml(f'<w:fldChar {nsdecls("w")} w:fldCharType="end"/>'))
312
  fmt(pp, f" | {meta.get('codigo','POP-XXX-001')} | v{meta.get('versao','01')}",
313
  size=TAM_TINY, color="888888")
314
 
 
346
  else:
347
  fmt(p, text, size=TAM_CORPO, color="1A1A1A")
348
 
349
+ def add_num(doc, n, text, indent=IND_SUBITEM, pal=None, critico=False):
350
  c = pal["primary"] if pal else DEFAULT_PRIMARY
351
  p = doc.add_paragraph(); p.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
352
  spacing(p, 1, 4, 1.15)
353
  p.paragraph_format.left_indent = Cm(indent); p.paragraph_format.first_line_indent = Cm(-0.5)
354
+ if critico:
355
+ fmt(p, f"\u26a0 ", bold=True, size=TAM_CORPO, color="C05000")
356
+ fmt(p, f"{n}. [PASSO CR\u00cdTICO] ", bold=True, size=TAM_CORPO, color="C05000")
357
+ else:
358
+ fmt(p, f"{n}. ", bold=True, size=TAM_CORPO, color=c)
359
+ # Texto com badges de Meta OMS
360
+ fmt_with_meta_badges(p, text, size=TAM_CORPO)
361
 
362
  def add_def_item(doc, termo, defn, pal):
363
  p = doc.add_paragraph(); p.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
 
563
  edge_style = {'color': f'#{pal["primary"]}', 'penwidth': '1.0', 'arrowsize': '0.6'}
564
 
565
  if tipo in ("processo", "fluxograma"):
566
+ etapas_raw = conteudo if isinstance(conteudo, list) else [conteudo]
567
+ etapas_raw = etapas_raw[:9]
568
+ n = len(etapas_raw)
569
+
570
+ # Mapear formas: elipse→ellipse, losango→diamond, retangulo→box
571
+ forma_map = {'elipse': 'ellipse', 'losango': 'diamond', 'retangulo': 'box',
572
+ 'ellipse': 'ellipse', 'diamond': 'diamond', 'box': 'box'}
573
+
574
+ def _parse_etapa(et, idx, total):
575
+ """Retorna (texto, forma) de um item do fluxograma."""
576
+ if isinstance(et, dict):
577
+ txt = et.get('texto', et.get('titulo', str(et)))[:30]
578
+ frm = forma_map.get(et.get('forma', 'retangulo'), 'box')
579
+ return txt, frm
580
+ txt = str(et)[:30]
581
+ # Auto-forma: primeiro=elipse, último=elipse, demais=box
582
+ if idx == 0 or idx == total - 1:
583
+ return txt, 'ellipse'
584
+ # Detectar decisões por "?" no texto
585
+ if '?' in str(et):
586
+ return txt, 'diamond'
587
+ return txt, 'box'
588
+
589
  if n <= 3:
 
590
  dot.attr(rankdir='LR', size='8,2!')
591
+ for i, et in enumerate(etapas_raw):
592
+ txt, frm = _parse_etapa(et, i, n)
593
+ st = {**(node_style if i % 2 == 0 else node_light), 'shape': frm}
594
+ dot.node(f'n{i}', txt, **st)
595
  if i > 0:
596
  dot.edge(f'n{i-1}', f'n{i}', **edge_style)
597
  else:
 
598
  dot.attr(rankdir='TB', size='8,6!')
599
  per_row = 3
600
+ for i, et in enumerate(etapas_raw):
601
+ txt, frm = _parse_etapa(et, i, n)
602
+ st = {**(node_style if i % 2 == 0 else node_light), 'shape': frm}
603
+ dot.node(f'n{i}', txt, **st)
604
  if i > 0:
605
  dot.edge(f'n{i-1}', f'n{i}', **edge_style)
 
606
  for row_start in range(0, n, per_row):
607
  row_nodes = ' '.join(f'n{i}' for i in range(row_start, min(row_start + per_row, n)))
608
  dot.body.append(f' {{ rank=same; {row_nodes} }}')
 
693
 
694
 
695
  def _render_passo(doc, num, item, pal):
696
+ """Renderiza um item de procedimento: string, dict com texto/prioridade, ou dict com diagrama.
697
+ Formatos aceitos:
698
+ - "texto simples"
699
+ - {"texto": "...", "prioridade": "critica"}
700
+ - {"diagrama": {"tipo":..., "titulo":..., "conteudo":...}}
701
+ - {"tipo":..., "titulo":..., "conteudo":...}
702
  """
703
  if isinstance(item, str):
704
  add_num(doc, num, item, pal=pal)
705
  elif isinstance(item, dict):
706
+ # Formato 1: {"texto": "...", "prioridade": "critica"}
707
+ if "texto" in item and "diagrama" not in item and "tipo" not in item:
708
+ texto = item.get("texto", "")
709
+ critico = item.get("prioridade", "").lower() == "critica"
710
+ add_num(doc, num, texto, pal=pal, critico=critico)
711
+ return
712
+
713
+ # Formato 2: diagrama (com ou sem wrapper "diagrama")
714
  diag = item.get("diagrama", None)
715
  if diag is None and "tipo" in item:
716
+ diag = item
717
  if diag is None:
718
+ # Dict não reconhecido — texto fallback
719
  add_num(doc, num, str(item), pal=pal)
720
  return
721
  tipo = diag.get("tipo", "processo")
 
725
  if img_path:
726
  _inserir_diagrama_no_corpo(doc, img_path, titulo, pal)
727
  else:
 
728
  items_txt = ', '.join(str(c) for c in conteudo) if isinstance(conteudo, list) else str(conteudo)
729
  add_num(doc, num, f"[{titulo}]: {items_txt}", pal=pal)
730
 
 
1032
  # Seções
1033
  sn = 1
1034
  add_h1(doc, sn, "OBJETIVO / FINALIDADE", pal)
1035
+ # Remover space_before do primeiro H1 (margem superior já cuida do espaço)
1036
+ doc.paragraphs[0].paragraph_format.space_before = Pt(0)
1037
  add_body(doc, secoes.get("objetivo", "[Objetivo não fornecido]"))
1038
 
1039
  sn += 1; add_h1(doc, sn, "CAMPO DE APLICAÇÃO / ÁREA", pal)
 
1071
  if isinstance(it, dict): add_risk(doc, it.get("risco",""), it.get("barreira",""), pal)
1072
  else: add_bullet(doc, str(it), pal=pal)
1073
  add_h2(doc, f"{sn}.2", "Plano de Contingência", pal)
1074
+ # Caixa de destaque com borda lateral vermelha/laranja
1075
+ cont_text = riscos.get("contingencia","[Contingência não fornecida]")
1076
+ cont_tbl = doc.add_table(rows=1, cols=1)
1077
+ cont_tbl.alignment = WD_TABLE_ALIGNMENT.CENTER
1078
+ cc = cont_tbl.cell(0, 0)
1079
+ set_width(cc, LARGURA_DXA - 200)
1080
+ set_margins(cc, 80, 80, 200, 200)
1081
+ # Borda esquerda laranja grossa, demais finas cinza
1082
+ set_borders(cc, ("E87D00","12"), (pal["gray_border"],"1"), (pal["gray_border"],"1"), (pal["gray_border"],"1"))
1083
+ set_shading(cc, "FFF8F0")
1084
+ # Ícone + título
1085
+ pt = cc.paragraphs[0]
1086
+ pt.paragraph_format.space_after = Pt(6)
1087
+ fmt(pt, "\u26a0 ATENÇÃO — Plano de Contingência", bold=True, size=TAM_CORPO, color="C05000")
1088
+ # Texto
1089
+ pb = cc.add_paragraph()
1090
+ pb.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
1091
+ pb.paragraph_format.line_spacing = 1.15
1092
+ fmt(pb, cont_text, size=TAM_SMALL, color="1A1A1A")
1093
 
1094
  sn += 1; add_h1(doc, sn, "REGISTROS DA QUALIDADE (EVIDÊNCIAS)", pal)
1095
  add_body(doc, "A execução deste procedimento e eventuais intercorrências devem ser "