avis de sanction dans fiche apprenti
This commit is contained in:
parent
ef6072112b
commit
610e37d2a1
4 changed files with 329 additions and 76 deletions
|
|
@ -418,9 +418,6 @@ class FicheState(AuthState):
|
|||
cal_next_name: str = ""
|
||||
cal_days: list[dict] = []
|
||||
|
||||
# ── Pending dates (quick excuse) ─────────────────────────────────────────
|
||||
pending_dates: list[dict] = []
|
||||
|
||||
# ── Calendar day edit ─────────────────────────────────────────────────────
|
||||
edit_date: str = ""
|
||||
edit_date_label: str = ""
|
||||
|
|
@ -435,6 +432,19 @@ class FicheState(AuthState):
|
|||
edit_p9: str = "present"
|
||||
edit_p10: str = "present"
|
||||
|
||||
# Snapshot des choix au chargement (pour détecter les modifications non
|
||||
# enregistrées). Mis à jour par _load_day_choices().
|
||||
initial_p1: str = "present"
|
||||
initial_p2: str = "present"
|
||||
initial_p3: str = "present"
|
||||
initial_p4: str = "present"
|
||||
initial_p5: str = "present"
|
||||
initial_p6: str = "present"
|
||||
initial_p7: str = "present"
|
||||
initial_p8: str = "present"
|
||||
initial_p9: str = "present"
|
||||
initial_p10: str = "present"
|
||||
|
||||
# ── Escada fiche ─────────────────────────────────────────────────────────
|
||||
fiche_available: bool = False
|
||||
fiche_adresse: str = ""
|
||||
|
|
@ -531,6 +541,7 @@ class FicheState(AuthState):
|
|||
self.selected_id = self.apprenti_ids[0]
|
||||
self.selected_label = self.apprenti_labels[0]
|
||||
self._reload(reset_email=True)
|
||||
self._select_today()
|
||||
|
||||
def handle_select(self, label: str):
|
||||
self.selected_label = label
|
||||
|
|
@ -543,6 +554,13 @@ class FicheState(AuthState):
|
|||
self.apprenti_select_open = False
|
||||
self.apprenti_search = ""
|
||||
self._reload(reset_email=True)
|
||||
self._select_today()
|
||||
|
||||
def _select_today(self):
|
||||
"""Pré-sélectionne la date du jour dans le panneau d'édition."""
|
||||
today_iso = date.today().isoformat()
|
||||
self._load_day_choices(today_iso)
|
||||
self.edit_date = today_iso
|
||||
|
||||
def set_apprenti_search(self, v: str):
|
||||
self.apprenti_search = v
|
||||
|
|
@ -600,12 +618,8 @@ class FicheState(AuthState):
|
|||
self._rebuild_calendar()
|
||||
|
||||
# ── Calendar day edit ─────────────────────────────────────────────────────
|
||||
def select_day(self, date_str: str):
|
||||
if not date_str:
|
||||
return
|
||||
if self.edit_date == date_str:
|
||||
self.edit_date = ""
|
||||
return
|
||||
def _load_day_choices(self, date_str: str):
|
||||
"""Met à jour edit_p1..p10 + edit_date_label pour la date donnée."""
|
||||
sess = get_session()
|
||||
d = date.fromisoformat(date_str)
|
||||
absences = sess.execute(
|
||||
|
|
@ -632,9 +646,74 @@ class FicheState(AuthState):
|
|||
self.edit_p8 = _choice(8)
|
||||
self.edit_p9 = _choice(9)
|
||||
self.edit_p10 = _choice(10)
|
||||
# Snapshot des choix initiaux (pour détecter les modifs)
|
||||
self.initial_p1 = self.edit_p1
|
||||
self.initial_p2 = self.edit_p2
|
||||
self.initial_p3 = self.edit_p3
|
||||
self.initial_p4 = self.edit_p4
|
||||
self.initial_p5 = self.edit_p5
|
||||
self.initial_p6 = self.edit_p6
|
||||
self.initial_p7 = self.edit_p7
|
||||
self.initial_p8 = self.edit_p8
|
||||
self.initial_p9 = self.edit_p9
|
||||
self.initial_p10 = self.edit_p10
|
||||
self.edit_date_label = d.strftime("%d.%m.%Y")
|
||||
|
||||
def select_day(self, date_str: str):
|
||||
if not date_str:
|
||||
return
|
||||
if self.edit_date == date_str:
|
||||
self.edit_date = ""
|
||||
return
|
||||
self._load_day_choices(date_str)
|
||||
self.edit_date = date_str
|
||||
|
||||
@rx.var
|
||||
def edit_has_changes(self) -> bool:
|
||||
"""True si au moins une période diffère de l'état chargé en DB."""
|
||||
return (
|
||||
self.edit_p1 != self.initial_p1 or
|
||||
self.edit_p2 != self.initial_p2 or
|
||||
self.edit_p3 != self.initial_p3 or
|
||||
self.edit_p4 != self.initial_p4 or
|
||||
self.edit_p5 != self.initial_p5 or
|
||||
self.edit_p6 != self.initial_p6 or
|
||||
self.edit_p7 != self.initial_p7 or
|
||||
self.edit_p8 != self.initial_p8 or
|
||||
self.edit_p9 != self.initial_p9 or
|
||||
self.edit_p10 != self.initial_p10
|
||||
)
|
||||
|
||||
@rx.var
|
||||
def edit_has_non_excusee(self) -> bool:
|
||||
"""True si au moins une période est en N (non excusée)."""
|
||||
return (
|
||||
self.edit_p1 == "non_excusee" or
|
||||
self.edit_p2 == "non_excusee" or
|
||||
self.edit_p3 == "non_excusee" or
|
||||
self.edit_p4 == "non_excusee" or
|
||||
self.edit_p5 == "non_excusee" or
|
||||
self.edit_p6 == "non_excusee" or
|
||||
self.edit_p7 == "non_excusee" or
|
||||
self.edit_p8 == "non_excusee" or
|
||||
self.edit_p9 == "non_excusee" or
|
||||
self.edit_p10 == "non_excusee"
|
||||
)
|
||||
|
||||
def excuse_all_visual(self):
|
||||
"""Bascule toutes les N → E dans le panneau (sans toucher la DB).
|
||||
L'enregistrement passe par le bouton « Enregistrer »."""
|
||||
if self.edit_p1 == "non_excusee": self.edit_p1 = "excusee"
|
||||
if self.edit_p2 == "non_excusee": self.edit_p2 = "excusee"
|
||||
if self.edit_p3 == "non_excusee": self.edit_p3 = "excusee"
|
||||
if self.edit_p4 == "non_excusee": self.edit_p4 = "excusee"
|
||||
if self.edit_p5 == "non_excusee": self.edit_p5 = "excusee"
|
||||
if self.edit_p6 == "non_excusee": self.edit_p6 = "excusee"
|
||||
if self.edit_p7 == "non_excusee": self.edit_p7 = "excusee"
|
||||
if self.edit_p8 == "non_excusee": self.edit_p8 = "excusee"
|
||||
if self.edit_p9 == "non_excusee": self.edit_p9 = "excusee"
|
||||
if self.edit_p10 == "non_excusee": self.edit_p10 = "excusee"
|
||||
|
||||
def cancel_edit(self):
|
||||
self.edit_date = ""
|
||||
|
||||
|
|
@ -705,6 +784,10 @@ class FicheState(AuthState):
|
|||
)
|
||||
sess.commit()
|
||||
self._reload(reset_email=False)
|
||||
# Resync du snapshot pour que edit_has_changes reparte à False
|
||||
# tant qu'aucune nouvelle modif n'est faite.
|
||||
if self.edit_date:
|
||||
self._load_day_choices(self.edit_date)
|
||||
if nb_changes == 0:
|
||||
return rx.toast.info("Aucune modification")
|
||||
msg = (
|
||||
|
|
@ -745,9 +828,10 @@ class FicheState(AuthState):
|
|||
f"{old_type} → E (excuse rapide)"
|
||||
)
|
||||
sess.commit()
|
||||
if self.edit_date == date_str:
|
||||
self.edit_date = ""
|
||||
self._reload(reset_email=False)
|
||||
# Rester sur la date sélectionnée et rafraîchir les choix du panneau.
|
||||
if self.edit_date == date_str:
|
||||
self._load_day_choices(date_str)
|
||||
if nb == 0:
|
||||
return rx.toast.info("Aucune absence à excuser")
|
||||
msg = (
|
||||
|
|
@ -890,19 +974,6 @@ class FicheState(AuthState):
|
|||
self.kpi_blocs = nb_blocs_absences(sess, self.selected_id)
|
||||
self.quota_atteint = self.kpi_blocs >= QUOTA
|
||||
|
||||
# Pending dates
|
||||
by_date: dict = {}
|
||||
for ab in absences:
|
||||
by_date.setdefault(ab.date, []).append(ab)
|
||||
self.pending_dates = [
|
||||
{
|
||||
"date_str": d.isoformat(),
|
||||
"label": f"{d.strftime('%d.%m')} ({sum(1 for a in al if a.statut == 'a_traiter')})",
|
||||
}
|
||||
for d, al in sorted(by_date.items())
|
||||
if any(a.statut == "a_traiter" for a in al)
|
||||
]
|
||||
|
||||
# Fiche
|
||||
fiche = sess.execute(
|
||||
select(ApprentiFiche).where(ApprentiFiche.apprenti_id == self.selected_id)
|
||||
|
|
@ -1340,18 +1411,33 @@ def _edit_panel() -> rx.Component:
|
|||
flex_wrap="wrap",
|
||||
width="100%",
|
||||
),
|
||||
rx.hstack(
|
||||
# Action rapide : excuser visuellement toutes les N → E.
|
||||
# N'enregistre pas en DB — il faut cliquer sur « Enregistrer ».
|
||||
rx.flex(
|
||||
rx.button(
|
||||
rx.icon("check-check", size=14),
|
||||
"Excuser toutes les périodes",
|
||||
on_click=FicheState.excuse_all_visual,
|
||||
disabled=~FicheState.edit_has_non_excusee,
|
||||
variant="soft", color_scheme="green", size="2",
|
||||
),
|
||||
width="100%",
|
||||
),
|
||||
rx.divider(),
|
||||
rx.flex(
|
||||
rx.button(
|
||||
rx.icon("save", size=14), "Enregistrer",
|
||||
on_click=FicheState.save_day_edit,
|
||||
disabled=~FicheState.edit_has_changes,
|
||||
color_scheme="blue", size="2",
|
||||
),
|
||||
rx.button(
|
||||
"Annuler",
|
||||
on_click=FicheState.cancel_edit,
|
||||
disabled=~FicheState.edit_has_changes,
|
||||
variant="outline", color_scheme="gray", size="2",
|
||||
),
|
||||
spacing="3",
|
||||
gap="0.75rem", flex_wrap="wrap",
|
||||
),
|
||||
spacing="3", width="100%",
|
||||
),
|
||||
|
|
@ -1441,17 +1527,6 @@ def _notice_row(item) -> rx.Component:
|
|||
)
|
||||
|
||||
|
||||
def _pending_btn(item: dict) -> rx.Component:
|
||||
return rx.button(
|
||||
rx.icon("check", size=13),
|
||||
item["label"],
|
||||
on_click=FicheState.excuse_day(item["date_str"]),
|
||||
color_scheme="green",
|
||||
variant="soft",
|
||||
size="1",
|
||||
)
|
||||
|
||||
|
||||
def _email_section() -> rx.Component:
|
||||
return rx.box(
|
||||
rx.vstack(
|
||||
|
|
@ -1849,33 +1924,6 @@ def fiche_page() -> rx.Component:
|
|||
rx.text("Aucune absence enregistrée.", size="2", color="#666"),
|
||||
),
|
||||
|
||||
# ── Actions rapides ───────────────────────────────────────
|
||||
rx.cond(
|
||||
FicheState.pending_dates.length() > 0,
|
||||
rx.box(
|
||||
rx.vstack(
|
||||
rx.hstack(
|
||||
rx.icon("clock", size=15, color="#b45309"),
|
||||
rx.text(
|
||||
"Valider toutes les absences d'une journée",
|
||||
size="2", weight="bold", color="#92400e",
|
||||
),
|
||||
spacing="2", align="center",
|
||||
),
|
||||
rx.flex(
|
||||
rx.foreach(FicheState.pending_dates, _pending_btn),
|
||||
flex_wrap="wrap", gap="0.5rem",
|
||||
),
|
||||
spacing="2", width="100%",
|
||||
),
|
||||
padding="0.75rem 1rem",
|
||||
background_color="#fffbeb",
|
||||
border_radius="8px",
|
||||
border="1px solid #fcd34d",
|
||||
width="100%",
|
||||
),
|
||||
),
|
||||
|
||||
# ── Panneau d'édition ─────────────────────────────────────
|
||||
rx.cond(
|
||||
FicheState.edit_date != "",
|
||||
|
|
|
|||
|
|
@ -156,7 +156,9 @@ class RetenueState(AuthState):
|
|||
self.modal_open = True
|
||||
|
||||
def _detect_existing_notice(self, sess, apprenti_id: int):
|
||||
"""Détecte si une Notice pending existe déjà aujourd'hui pour cet apprenti."""
|
||||
"""Détecte si une Notice de retenue pending existe déjà aujourd'hui
|
||||
pour cet apprenti. Filtre par source pour ne pas confondre avec une
|
||||
notice de sanction."""
|
||||
today = _date.today()
|
||||
existing = sess.execute(
|
||||
select(Notice)
|
||||
|
|
@ -164,6 +166,7 @@ class RetenueState(AuthState):
|
|||
Notice.apprenti_id == apprenti_id,
|
||||
Notice.date_event == today,
|
||||
Notice.status == "pending",
|
||||
Notice.source == "retenue",
|
||||
)
|
||||
.order_by(Notice.created_at.desc())
|
||||
).scalars().first()
|
||||
|
|
|
|||
|
|
@ -10,16 +10,20 @@ from __future__ import annotations
|
|||
import json
|
||||
import os
|
||||
import sys
|
||||
from datetime import date as _date
|
||||
from pathlib import Path
|
||||
|
||||
import reflex as rx
|
||||
from sqlalchemy import select
|
||||
|
||||
_ROOT = Path(__file__).resolve().parent.parent.parent
|
||||
if str(_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(_ROOT))
|
||||
|
||||
from src.db import get_session, Apprenti, ApprentiFiche # noqa: E402
|
||||
from src.sanction_pdf import generate_avis_pdf # noqa: E402
|
||||
from src.db import get_session, Apprenti, ApprentiFiche, Notice # noqa: E402
|
||||
from src.sanction_pdf import ( # noqa: E402
|
||||
generate_avis_pdf, _DEFAULT_TEXTE_SANCTION, _DEFAULT_CHEF_SECTION,
|
||||
)
|
||||
from src.email_sender import send_email # noqa: E402
|
||||
from src.user_access import is_class_allowed # noqa: E402
|
||||
from src.logger import app_log # noqa: E402
|
||||
|
|
@ -52,10 +56,20 @@ class SanctionState(AuthState):
|
|||
sel_fiche_email_appr: str = ""
|
||||
sel_fiche_email_form: str = ""
|
||||
|
||||
# Texte de description et chef de section, pré-remplis depuis Paramètres
|
||||
# (ou valeurs par défaut) à chaque preload — modifiables librement.
|
||||
texte_description: str = ""
|
||||
chef_section: str = ""
|
||||
|
||||
# Email
|
||||
email_dest: str = "apprenti"
|
||||
email_custom: str = ""
|
||||
|
||||
# Détection notice existante (pending) pour cet apprenti à la date du jour
|
||||
has_existing_notice: bool = False
|
||||
existing_notice_label: str = ""
|
||||
create_anyway: bool = False
|
||||
|
||||
form_error: str = ""
|
||||
|
||||
def set_modal_open(self, v: bool):
|
||||
|
|
@ -65,6 +79,9 @@ class SanctionState(AuthState):
|
|||
|
||||
def set_email_dest(self, v: str): self.email_dest = v
|
||||
def set_email_custom(self, v: str): self.email_custom = v
|
||||
def set_texte_description(self, v: str): self.texte_description = v
|
||||
def set_chef_section(self, v: str): self.chef_section = v
|
||||
def set_create_anyway(self, v: bool): self.create_anyway = v
|
||||
|
||||
def preload_apprenti(self, apprenti_id: int, label: str):
|
||||
self.selected_id = apprenti_id
|
||||
|
|
@ -72,6 +89,15 @@ class SanctionState(AuthState):
|
|||
self.form_error = ""
|
||||
self.email_dest = "apprenti"
|
||||
self.email_custom = ""
|
||||
# Pré-remplit texte + chef de section avec Paramètres ou valeurs par défaut.
|
||||
settings = _load_settings()
|
||||
self.texte_description = (
|
||||
(settings.get("texte_sanction") or "").strip() or _DEFAULT_TEXTE_SANCTION
|
||||
)
|
||||
self.chef_section = (
|
||||
(settings.get("chef_section") or "").strip() or _DEFAULT_CHEF_SECTION
|
||||
)
|
||||
self.create_anyway = False
|
||||
sess = get_session()
|
||||
try:
|
||||
ap = sess.get(Apprenti, apprenti_id)
|
||||
|
|
@ -84,10 +110,75 @@ class SanctionState(AuthState):
|
|||
else:
|
||||
self.sel_fiche_email_appr = ""
|
||||
self.sel_fiche_email_form = ""
|
||||
self._detect_existing_notice(sess, apprenti_id)
|
||||
finally:
|
||||
sess.close()
|
||||
self.modal_open = True
|
||||
|
||||
def _detect_existing_notice(self, sess, apprenti_id: int):
|
||||
"""Détecte si une Notice de sanction pending existe déjà aujourd'hui
|
||||
pour cet apprenti. Filtre par source pour ne pas confondre avec une
|
||||
notice de retenue."""
|
||||
today = _date.today()
|
||||
existing = sess.execute(
|
||||
select(Notice)
|
||||
.where(
|
||||
Notice.apprenti_id == apprenti_id,
|
||||
Notice.date_event == today,
|
||||
Notice.status == "pending",
|
||||
Notice.source == "sanction",
|
||||
)
|
||||
.order_by(Notice.created_at.desc())
|
||||
).scalars().first()
|
||||
if existing:
|
||||
self.has_existing_notice = True
|
||||
self.existing_notice_label = (
|
||||
f"{existing.titre or '(sans titre)'} — "
|
||||
f"créée le {existing.created_at.strftime('%d.%m.%Y %H:%M')}"
|
||||
)
|
||||
else:
|
||||
self.has_existing_notice = False
|
||||
self.existing_notice_label = ""
|
||||
|
||||
def _create_notice(self):
|
||||
"""Crée une Notice en DB (push queue Escada).
|
||||
|
||||
Si une notice pending existe déjà pour cet apprenti aujourd'hui et que
|
||||
l'utilisateur n'a pas coché « Créer quand même », on saute la création.
|
||||
"""
|
||||
if not self.selected_id:
|
||||
return
|
||||
if self.has_existing_notice and not self.create_anyway:
|
||||
app_log(
|
||||
f"[notice] {self.username or '?'} : notice doublon évitée pour "
|
||||
f"{self.selected_label} (existante : {self.existing_notice_label})"
|
||||
)
|
||||
return
|
||||
remarque = (self.texte_description or "").strip() or None
|
||||
sess = get_session()
|
||||
try:
|
||||
sess.add(Notice(
|
||||
apprenti_id=self.selected_id,
|
||||
date_event=_date.today(),
|
||||
titre="Avis de sanction",
|
||||
remarque=remarque,
|
||||
type_notice=None,
|
||||
matiere=None,
|
||||
source="sanction",
|
||||
status="pending",
|
||||
created_by=self.username or None,
|
||||
))
|
||||
sess.commit()
|
||||
app_log(
|
||||
f"[notice] {self.username or '?'} : création (sanction) pour "
|
||||
f"{self.selected_label}"
|
||||
)
|
||||
except Exception as e:
|
||||
sess.rollback()
|
||||
app_log(f"[notice] échec création : {e}")
|
||||
finally:
|
||||
sess.close()
|
||||
|
||||
def _build_pdf(self) -> bytes | None:
|
||||
if not self.selected_id:
|
||||
self.form_error = "Aucun apprenti sélectionné."
|
||||
|
|
@ -95,12 +186,18 @@ class SanctionState(AuthState):
|
|||
if not is_class_allowed(self.username, self.sel_classe):
|
||||
self.form_error = "Accès refusé pour cette classe."
|
||||
return None
|
||||
txt = (self.texte_description or "").strip()
|
||||
if not txt:
|
||||
self.form_error = "Le texte de description ne peut pas être vide."
|
||||
return None
|
||||
self.form_error = ""
|
||||
sess = get_session()
|
||||
try:
|
||||
return generate_avis_pdf(
|
||||
sess, self.selected_id,
|
||||
prof_name=self.name or self.username,
|
||||
texte_override=txt,
|
||||
chef_override=(self.chef_section or "").strip() or None,
|
||||
)
|
||||
finally:
|
||||
sess.close()
|
||||
|
|
@ -125,10 +222,11 @@ class SanctionState(AuthState):
|
|||
f"[sanction] {self.username or '?'} : avis téléchargé pour "
|
||||
f"{self.selected_label}"
|
||||
)
|
||||
self._create_notice()
|
||||
self.modal_open = False
|
||||
return [
|
||||
rx.download(data=data, filename=self._filename()),
|
||||
rx.toast.success("Avis de sanction téléchargé"),
|
||||
rx.toast.success("Avis de sanction téléchargé — notice ajoutée à la file Escada"),
|
||||
]
|
||||
|
||||
def send_email_action(self):
|
||||
|
|
@ -174,12 +272,101 @@ class SanctionState(AuthState):
|
|||
f"[sanction] {self.username or '?'} : avis envoyé à {to} pour "
|
||||
f"{self.selected_label}"
|
||||
)
|
||||
self._create_notice()
|
||||
self.modal_open = False
|
||||
return rx.toast.success(f"Avis de sanction envoyé à {to}")
|
||||
return rx.toast.success(
|
||||
f"Avis de sanction envoyé à {to} — notice ajoutée à la file Escada"
|
||||
)
|
||||
|
||||
|
||||
# ── UI ────────────────────────────────────────────────────────────────────────
|
||||
|
||||
def _texte_section() -> rx.Component:
|
||||
"""Texte de description + chef de section, pré-remplis depuis Paramètres
|
||||
(ou fallback). L'utilisateur les modifie librement avant génération."""
|
||||
return rx.box(
|
||||
rx.vstack(
|
||||
rx.flex(
|
||||
rx.icon("file-text", size=16, color="#37474f"),
|
||||
rx.text("Contenu de l'avis", size="3", weight="bold", color="#37474f"),
|
||||
gap="0.5rem", align="center",
|
||||
),
|
||||
rx.divider(),
|
||||
rx.text(
|
||||
"Pré-remplis depuis les Paramètres. Modifiables avant génération.",
|
||||
size="1", color="var(--gray-11)",
|
||||
),
|
||||
rx.vstack(
|
||||
rx.text("Texte de description", size="2", weight="medium", color="var(--gray-11)"),
|
||||
rx.text_area(
|
||||
value=SanctionState.texte_description,
|
||||
on_change=SanctionState.set_texte_description,
|
||||
rows="8",
|
||||
width="100%",
|
||||
),
|
||||
spacing="1", width="100%",
|
||||
),
|
||||
rx.vstack(
|
||||
rx.text("Chef de section", size="2", weight="medium", color="var(--gray-11)"),
|
||||
rx.input(
|
||||
value=SanctionState.chef_section,
|
||||
on_change=SanctionState.set_chef_section,
|
||||
width="100%",
|
||||
),
|
||||
spacing="1", width="100%",
|
||||
),
|
||||
spacing="3", width="100%",
|
||||
),
|
||||
padding="1.25rem",
|
||||
background_color="white",
|
||||
border_radius="8px",
|
||||
border="1px solid #e0e0e0",
|
||||
width="100%",
|
||||
)
|
||||
|
||||
|
||||
def _duplicate_notice_banner() -> rx.Component:
|
||||
"""Bannière jaune si une notice pending existe déjà aujourd'hui (avec checkbox override)."""
|
||||
return rx.cond(
|
||||
SanctionState.has_existing_notice,
|
||||
rx.box(
|
||||
rx.vstack(
|
||||
rx.flex(
|
||||
rx.icon("triangle-alert", size=16, color="#92400e"),
|
||||
rx.text(
|
||||
"Une notice est déjà en attente aujourd'hui pour cet apprenti :",
|
||||
size="2", weight="medium", color="#92400e",
|
||||
),
|
||||
gap="0.5rem", align="center",
|
||||
),
|
||||
rx.text(
|
||||
SanctionState.existing_notice_label,
|
||||
size="1", color="#78350f",
|
||||
),
|
||||
rx.flex(
|
||||
rx.checkbox(
|
||||
checked=SanctionState.create_anyway,
|
||||
on_change=SanctionState.set_create_anyway,
|
||||
size="2",
|
||||
),
|
||||
rx.text(
|
||||
"Créer quand même une nouvelle notice",
|
||||
size="2", color="#78350f",
|
||||
),
|
||||
gap="0.5rem", align="center",
|
||||
),
|
||||
spacing="2", width="100%",
|
||||
),
|
||||
padding="0.75rem 1rem",
|
||||
background_color="#fef3c7",
|
||||
border="1px solid #fcd34d",
|
||||
border_radius="6px",
|
||||
width="100%",
|
||||
),
|
||||
rx.fragment(),
|
||||
)
|
||||
|
||||
|
||||
def _email_section() -> rx.Component:
|
||||
return rx.box(
|
||||
rx.vstack(
|
||||
|
|
@ -278,13 +465,15 @@ def sanction_modal() -> rx.Component:
|
|||
rx.callout.root(
|
||||
rx.callout.icon(rx.icon("info", size=16)),
|
||||
rx.callout.text(
|
||||
"L'avis utilise le texte par défaut configuré dans ",
|
||||
"L'adresse et le nom de l'entreprise proviennent de la fiche "
|
||||
"apprenti Escada. Les valeurs par défaut sont configurables dans ",
|
||||
rx.link("Paramètres", href="/params", color="var(--brand-accent)"),
|
||||
" (motif et chef de section). L'adresse et le nom de l'entreprise "
|
||||
"proviennent de la fiche apprenti Escada.",
|
||||
".",
|
||||
),
|
||||
color_scheme="blue", variant="soft", size="1",
|
||||
),
|
||||
_texte_section(),
|
||||
_duplicate_notice_banner(),
|
||||
rx.cond(
|
||||
SanctionState.form_error != "",
|
||||
rx.callout.root(
|
||||
|
|
|
|||
|
|
@ -44,6 +44,8 @@ def generate_avis_pdf(
|
|||
sess: Session,
|
||||
apprenti_id: int,
|
||||
prof_name: str = "",
|
||||
texte_override: Optional[str] = None,
|
||||
chef_override: Optional[str] = None,
|
||||
) -> Optional[bytes]:
|
||||
"""Renvoie les bytes d'un PDF d'avis de sanction pré-rempli pour l'apprenti.
|
||||
|
||||
|
|
@ -51,6 +53,9 @@ def generate_avis_pdf(
|
|||
NomParents = nom entreprise) puisque les parents ne sont pas stockés.
|
||||
Texte de description et chef de section depuis data/settings.json.
|
||||
|
||||
Si `texte_override` ou `chef_override` est fourni (non vide), il remplace
|
||||
la valeur issue des paramètres.
|
||||
|
||||
Renvoie None si le template est introuvable ou l'apprenti n'existe pas.
|
||||
"""
|
||||
if not _TEMPLATE_PATH.exists():
|
||||
|
|
@ -77,9 +82,17 @@ def generate_avis_pdf(
|
|||
"Adresse": (fiche.entreprise_adresse if fiche else "") or "",
|
||||
"NPA-Ville": npa_ville,
|
||||
"Date": date.today().strftime("%d.%m.%Y"),
|
||||
"TexteDescription": settings.get("texte_sanction") or _DEFAULT_TEXTE_SANCTION,
|
||||
"TexteDescription": (
|
||||
(texte_override or "").strip()
|
||||
or settings.get("texte_sanction")
|
||||
or _DEFAULT_TEXTE_SANCTION
|
||||
),
|
||||
"Prof": prof_name or "",
|
||||
"CS": settings.get("chef_section") or _DEFAULT_CHEF_SECTION,
|
||||
"CS": (
|
||||
(chef_override or "").strip()
|
||||
or settings.get("chef_section")
|
||||
or _DEFAULT_CHEF_SECTION
|
||||
),
|
||||
}
|
||||
|
||||
# Lecture du template + clone vers writer (préserve la structure AcroForm)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue