accents corrigés
This commit is contained in:
parent
f60cbf1b1c
commit
ee4e212f7d
8 changed files with 322 additions and 3803 deletions
|
|
@ -8,6 +8,6 @@
|
||||||
"MONTAUT 1:abs": "https://escadaweb.vs.ch/Lehrpersonen/ViewAbsenzenErweitert.aspx?id=b51ec970-5bf4-4982-a05e-80546bb7421f",
|
"MONTAUT 1:abs": "https://escadaweb.vs.ch/Lehrpersonen/ViewAbsenzenErweitert.aspx?id=b51ec970-5bf4-4982-a05e-80546bb7421f",
|
||||||
"MONTAUT 2:abs": "https://escadaweb.vs.ch/Lehrpersonen/ViewAbsenzenErweitert.aspx?id=aaacc343-c248-4f21-93f6-5d9e3079aa5d",
|
"MONTAUT 2:abs": "https://escadaweb.vs.ch/Lehrpersonen/ViewAbsenzenErweitert.aspx?id=aaacc343-c248-4f21-93f6-5d9e3079aa5d",
|
||||||
"MONTAUT 3:abs": "https://escadaweb.vs.ch/Lehrpersonen/ViewAbsenzenErweitert.aspx?id=18c1ddbe-471f-44f6-bde6-8619adc3b767",
|
"MONTAUT 3:abs": "https://escadaweb.vs.ch/Lehrpersonen/ViewAbsenzenErweitert.aspx?id=18c1ddbe-471f-44f6-bde6-8619adc3b767",
|
||||||
"EM-AU 2:abs": "https://escadaweb.vs.ch/Lehrpersonen/ViewAbsenzenErweitert.aspx?id=c2c982af-b6f9-44ab-8ed5-7bc567b967cf",
|
"EM-AU 1:abs": "https://escadaweb.vs.ch/Lehrpersonen/ViewAbsenzenErweitert.aspx?id=a4c4c187-920c-4c91-9620-7f153cf3738a",
|
||||||
"EM-AU 1:abs": "https://escadaweb.vs.ch/Lehrpersonen/ViewAbsenzenErweitert.aspx?id=a4c4c187-920c-4c91-9620-7f153cf3738a"
|
"EM-AU 2:abs": "https://escadaweb.vs.ch/Lehrpersonen/ViewAbsenzenErweitert.aspx?id=7bddb158-278f-4258-934d-eddb7de88af3"
|
||||||
}
|
}
|
||||||
|
|
@ -286,7 +286,7 @@ def _absence_pdf_apprenti(sess, apprenti) -> bytes:
|
||||||
total_periodes = total_e + total_n
|
total_periodes = total_e + total_n
|
||||||
footer_label = (
|
footer_label = (
|
||||||
f"{len(blocs)} absence(s) | "
|
f"{len(blocs)} absence(s) | "
|
||||||
f"{total_periodes} periode(s) | "
|
f"{total_periodes} période(s) | "
|
||||||
f"{total_e} excusee(s) | "
|
f"{total_e} excusee(s) | "
|
||||||
f"{total_n} non excusee(s)"
|
f"{total_n} non excusee(s)"
|
||||||
)
|
)
|
||||||
|
|
@ -353,6 +353,15 @@ class ClasseState(AuthState):
|
||||||
selected_class: str = ""
|
selected_class: str = ""
|
||||||
has_classes: bool = False
|
has_classes: bool = False
|
||||||
apprentis_data: list[dict] = []
|
apprentis_data: list[dict] = []
|
||||||
|
class_search: str = ""
|
||||||
|
class_select_open: bool = False
|
||||||
|
|
||||||
|
@rx.var
|
||||||
|
def filtered_classes(self) -> list[str]:
|
||||||
|
q = self.class_search.lower().strip()
|
||||||
|
if not q:
|
||||||
|
return self.classes
|
||||||
|
return [c for c in self.classes if q in c.lower()]
|
||||||
|
|
||||||
def load_data(self):
|
def load_data(self):
|
||||||
if not self.authenticated:
|
if not self.authenticated:
|
||||||
|
|
@ -377,8 +386,18 @@ class ClasseState(AuthState):
|
||||||
|
|
||||||
def set_class(self, classe: str):
|
def set_class(self, classe: str):
|
||||||
self.selected_class = classe
|
self.selected_class = classe
|
||||||
|
self.class_select_open = False
|
||||||
|
self.class_search = ""
|
||||||
self._reload()
|
self._reload()
|
||||||
|
|
||||||
|
def set_class_search(self, v: str):
|
||||||
|
self.class_search = v
|
||||||
|
|
||||||
|
def set_class_select_open(self, v: bool):
|
||||||
|
self.class_select_open = v
|
||||||
|
if not v:
|
||||||
|
self.class_search = ""
|
||||||
|
|
||||||
def download_abs_pdf(self, apprenti_id: int):
|
def download_abs_pdf(self, apprenti_id: int):
|
||||||
sess = get_session()
|
sess = get_session()
|
||||||
apprenti = sess.get(Apprenti, apprenti_id)
|
apprenti = sess.get(Apprenti, apprenti_id)
|
||||||
|
|
@ -544,6 +563,75 @@ class ClasseState(AuthState):
|
||||||
|
|
||||||
# ── UI components ─────────────────────────────────────────────────────────────
|
# ── UI components ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _classe_option(classe: rx.Var) -> rx.Component:
|
||||||
|
return rx.box(
|
||||||
|
rx.text(classe, size="2"),
|
||||||
|
padding="0.45rem 0.75rem",
|
||||||
|
cursor="pointer",
|
||||||
|
on_click=ClasseState.set_class(classe),
|
||||||
|
_hover={"background_color": "var(--gray-3)"},
|
||||||
|
width="100%",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _classe_searchable_select() -> rx.Component:
|
||||||
|
return rx.popover.root(
|
||||||
|
rx.popover.trigger(
|
||||||
|
rx.box(
|
||||||
|
rx.flex(
|
||||||
|
rx.cond(
|
||||||
|
ClasseState.selected_class != "",
|
||||||
|
rx.text(ClasseState.selected_class, size="2"),
|
||||||
|
rx.text("Sélectionner une classe…", size="2", color="var(--gray-9)"),
|
||||||
|
),
|
||||||
|
rx.spacer(),
|
||||||
|
rx.icon("chevron-down", size=18, color="var(--gray-9)"),
|
||||||
|
align="center",
|
||||||
|
width="100%",
|
||||||
|
),
|
||||||
|
padding="0.5rem 0.75rem",
|
||||||
|
border="1px solid var(--gray-7)",
|
||||||
|
border_radius="6px",
|
||||||
|
background_color="white",
|
||||||
|
cursor="pointer",
|
||||||
|
width="100%",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
rx.popover.content(
|
||||||
|
rx.vstack(
|
||||||
|
rx.input(
|
||||||
|
placeholder="Rechercher une classe…",
|
||||||
|
value=ClasseState.class_search,
|
||||||
|
on_change=ClasseState.set_class_search,
|
||||||
|
size="2",
|
||||||
|
width="100%",
|
||||||
|
auto_focus=True,
|
||||||
|
),
|
||||||
|
rx.cond(
|
||||||
|
ClasseState.filtered_classes.length() > 0,
|
||||||
|
rx.box(
|
||||||
|
rx.foreach(ClasseState.filtered_classes, _classe_option),
|
||||||
|
max_height="280px",
|
||||||
|
overflow_y="auto",
|
||||||
|
width="100%",
|
||||||
|
),
|
||||||
|
rx.box(
|
||||||
|
rx.text("Aucun résultat", size="2", color="var(--gray-9)"),
|
||||||
|
padding="0.5rem 0.75rem",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
spacing="2",
|
||||||
|
width="100%",
|
||||||
|
),
|
||||||
|
min_width="280px",
|
||||||
|
max_width="400px",
|
||||||
|
padding="0.5rem",
|
||||||
|
),
|
||||||
|
open=ClasseState.class_select_open,
|
||||||
|
on_open_change=ClasseState.set_class_select_open,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _kpi_mini(label: str, value, color: str = "#37474f") -> rx.Component:
|
def _kpi_mini(label: str, value, color: str = "#37474f") -> rx.Component:
|
||||||
return rx.box(
|
return rx.box(
|
||||||
rx.text(label, size="1", color="#888"),
|
rx.text(label, size="1", color="#888"),
|
||||||
|
|
@ -656,7 +744,7 @@ def _apprenti_card(item) -> rx.Component:
|
||||||
spacing="2", width="100%",
|
spacing="2", width="100%",
|
||||||
),
|
),
|
||||||
rx.text(
|
rx.text(
|
||||||
"Aucun bulletin de notes importe.",
|
"Aucun bulletin de notes importé.",
|
||||||
size="2", color="#666",
|
size="2", color="#666",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -702,12 +790,7 @@ def classe_page() -> rx.Component:
|
||||||
rx.cond(
|
rx.cond(
|
||||||
ClasseState.has_classes,
|
ClasseState.has_classes,
|
||||||
rx.vstack(
|
rx.vstack(
|
||||||
rx.select(
|
_classe_searchable_select(),
|
||||||
ClasseState.classes,
|
|
||||||
value=ClasseState.selected_class,
|
|
||||||
on_change=ClasseState.set_class,
|
|
||||||
width="100%",
|
|
||||||
),
|
|
||||||
|
|
||||||
rx.cond(
|
rx.cond(
|
||||||
ClasseState.apprentis_data.length() > 0,
|
ClasseState.apprentis_data.length() > 0,
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ if str(_ROOT) not in sys.path:
|
||||||
|
|
||||||
from src.db import CronJob, get_session, init_db, Apprenti # noqa: E402
|
from src.db import CronJob, get_session, init_db, Apprenti # noqa: E402
|
||||||
from src.notifier import test_telegram # noqa: E402
|
from src.notifier import test_telegram # noqa: E402
|
||||||
|
from src.logger import app_log # noqa: E402
|
||||||
|
|
||||||
from ..state import AuthState
|
from ..state import AuthState
|
||||||
from ..sidebar import layout
|
from ..sidebar import layout
|
||||||
|
|
@ -348,9 +349,11 @@ class CronState(AuthState):
|
||||||
classes_json = json.dumps(self.f_classes)
|
classes_json = json.dumps(self.f_classes)
|
||||||
|
|
||||||
sess = get_session()
|
sess = get_session()
|
||||||
|
user = self.username or "?"
|
||||||
try:
|
try:
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
if self.editing_id == 0:
|
is_new = self.editing_id == 0
|
||||||
|
if is_new:
|
||||||
job = CronJob(
|
job = CronJob(
|
||||||
name=self.f_name.strip(),
|
name=self.f_name.strip(),
|
||||||
enabled=self.f_enabled,
|
enabled=self.f_enabled,
|
||||||
|
|
@ -391,6 +394,12 @@ class CronState(AuthState):
|
||||||
job.notify_chat_id = self.f_notify_chat_id
|
job.notify_chat_id = self.f_notify_chat_id
|
||||||
job.updated_at = now
|
job.updated_at = now
|
||||||
sess.commit()
|
sess.commit()
|
||||||
|
verb = "création" if is_new else "modification"
|
||||||
|
app_log(
|
||||||
|
f"[cron] {user} : {verb} tâche '{job.name}' (id={job.id}) — "
|
||||||
|
f"{job.task_kind} / {schedule_value} / "
|
||||||
|
f"{'activée' if job.enabled else 'désactivée'}"
|
||||||
|
)
|
||||||
self.save_ok = True
|
self.save_ok = True
|
||||||
self._refresh()
|
self._refresh()
|
||||||
self.edit_open = False
|
self.edit_open = False
|
||||||
|
|
@ -402,6 +411,7 @@ class CronState(AuthState):
|
||||||
|
|
||||||
def toggle_enabled(self, job_id: int):
|
def toggle_enabled(self, job_id: int):
|
||||||
sess = get_session()
|
sess = get_session()
|
||||||
|
user = self.username or "?"
|
||||||
try:
|
try:
|
||||||
job = sess.get(CronJob, job_id)
|
job = sess.get(CronJob, job_id)
|
||||||
if job:
|
if job:
|
||||||
|
|
@ -413,17 +423,25 @@ class CronState(AuthState):
|
||||||
if was_disabled and job.enabled:
|
if was_disabled and job.enabled:
|
||||||
job.last_run_at = None
|
job.last_run_at = None
|
||||||
sess.commit()
|
sess.commit()
|
||||||
|
app_log(
|
||||||
|
f"[cron] {user} : "
|
||||||
|
f"{'activation' if job.enabled else 'désactivation'} "
|
||||||
|
f"tâche '{job.name}' (id={job.id})"
|
||||||
|
)
|
||||||
self._refresh()
|
self._refresh()
|
||||||
finally:
|
finally:
|
||||||
sess.close()
|
sess.close()
|
||||||
|
|
||||||
def delete_job(self, job_id: int):
|
def delete_job(self, job_id: int):
|
||||||
sess = get_session()
|
sess = get_session()
|
||||||
|
user = self.username or "?"
|
||||||
try:
|
try:
|
||||||
job = sess.get(CronJob, job_id)
|
job = sess.get(CronJob, job_id)
|
||||||
if job:
|
if job:
|
||||||
|
job_name = job.name
|
||||||
sess.delete(job)
|
sess.delete(job)
|
||||||
sess.commit()
|
sess.commit()
|
||||||
|
app_log(f"[cron] {user} : suppression tâche '{job_name}' (id={job_id})")
|
||||||
self._refresh()
|
self._refresh()
|
||||||
finally:
|
finally:
|
||||||
sess.close()
|
sess.close()
|
||||||
|
|
|
||||||
|
|
@ -159,7 +159,7 @@ class EscadaState(AuthState):
|
||||||
self.is_refreshing = False
|
self.is_refreshing = False
|
||||||
self.is_pushing = False
|
self.is_pushing = False
|
||||||
self.import_in_progress = False
|
self.import_in_progress = False
|
||||||
app_log("Sync annulee")
|
app_log("Sync annulée")
|
||||||
|
|
||||||
# ── load_data ──────────────────────────────────────────────────────────────
|
# ── load_data ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
@ -228,10 +228,11 @@ class EscadaState(AuthState):
|
||||||
|
|
||||||
@_background
|
@_background
|
||||||
async def refresh_classes(self):
|
async def refresh_classes(self):
|
||||||
app_log("Rafraichissement liste classes Escada")
|
|
||||||
async with self:
|
async with self:
|
||||||
|
user = self.username or "?"
|
||||||
self.is_refreshing = True
|
self.is_refreshing = True
|
||||||
self.op_log = "Connexion a Escadaweb..."
|
self.op_log = "Connexion à Escadaweb…"
|
||||||
|
app_log(f"Rafraîchissement liste classes Escada par {user}")
|
||||||
|
|
||||||
cmd = [sys.executable, str(_SYNC_SCRIPT), "--list-classes"]
|
cmd = [sys.executable, str(_SYNC_SCRIPT), "--list-classes"]
|
||||||
lines: list[str] = []
|
lines: list[str] = []
|
||||||
|
|
@ -329,9 +330,9 @@ class EscadaState(AuthState):
|
||||||
]
|
]
|
||||||
|
|
||||||
if ui_classes:
|
if ui_classes:
|
||||||
app_log(f"Classes recuperees : {', '.join(ui_classes)}")
|
app_log(f"Classes récupérées : {', '.join(ui_classes)}")
|
||||||
else:
|
else:
|
||||||
app_log(f"Aucune classe recuperee (code={_rc_holder[0]}, lignes={len(lines)})")
|
app_log(f"Aucune classe récupérée (code={_rc_holder[0]}, lignes={len(lines)})")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
_t = asyncio.current_task()
|
_t = asyncio.current_task()
|
||||||
|
|
@ -353,7 +354,7 @@ class EscadaState(AuthState):
|
||||||
self.op_log = "\n".join(lines[-60:])
|
self.op_log = "\n".join(lines[-60:])
|
||||||
self.is_refreshing = False
|
self.is_refreshing = False
|
||||||
except Exception as _e:
|
except Exception as _e:
|
||||||
app_log(f"Erreur mise a jour etat refresh : {_e}")
|
app_log(f"Erreur mise à jour état refresh : {_e}")
|
||||||
try:
|
try:
|
||||||
async with self:
|
async with self:
|
||||||
self.is_refreshing = False
|
self.is_refreshing = False
|
||||||
|
|
@ -384,7 +385,16 @@ class EscadaState(AuthState):
|
||||||
self.sync_res_notes = []
|
self.sync_res_notes = []
|
||||||
self.sync_res_matu = []
|
self.sync_res_matu = []
|
||||||
|
|
||||||
app_log(f"Sync Escada — {len(selected)} classe(s) : {', '.join(selected)}")
|
_types = []
|
||||||
|
if sync_abs: _types.append("abs" + ("/forcé" if force_abs else ""))
|
||||||
|
if sync_bn: _types.append("BN")
|
||||||
|
if sync_notes: _types.append("notes")
|
||||||
|
if sync_fiches: _types.append("fiches")
|
||||||
|
_types_label = ", ".join(_types) or "—"
|
||||||
|
app_log(
|
||||||
|
f"Sync Escada démarrée par {username} — "
|
||||||
|
f"{len(selected)} classe(s) [{_types_label}] : {', '.join(selected)}"
|
||||||
|
)
|
||||||
|
|
||||||
args = ["--sync-all"] + selected
|
args = ["--sync-all"] + selected
|
||||||
if not sync_abs: args.append("--skip-abs")
|
if not sync_abs: args.append("--skip-abs")
|
||||||
|
|
@ -485,7 +495,7 @@ class EscadaState(AuthState):
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
_rc_holder[0] = _proc.wait() or 0
|
_rc_holder[0] = _proc.wait() or 0
|
||||||
app_log(f" subprocess termine, code={_rc_holder[0]}", debug=True)
|
app_log(f" subprocess terminé, code={_rc_holder[0]}", debug=True)
|
||||||
break
|
break
|
||||||
|
|
||||||
elif _all_done_payload:
|
elif _all_done_payload:
|
||||||
|
|
@ -507,7 +517,7 @@ class EscadaState(AuthState):
|
||||||
except Exception: pass
|
except Exception: pass
|
||||||
|
|
||||||
_rc = _rc_holder[0]
|
_rc = _rc_holder[0]
|
||||||
app_log(f"Sync script termine — code={_rc}, lignes={len(lines)}")
|
app_log(f"Sync script terminé — code={_rc}, lignes={len(lines)}")
|
||||||
if not _all_done_payload:
|
if not _all_done_payload:
|
||||||
app_log(f"ALL_DONE non trouve (code={_rc})")
|
app_log(f"ALL_DONE non trouve (code={_rc})")
|
||||||
|
|
||||||
|
|
@ -590,7 +600,7 @@ class EscadaState(AuthState):
|
||||||
break
|
break
|
||||||
|
|
||||||
# ── État final — async with self #3 ──────────────────────────────────────
|
# ── État final — async with self #3 ──────────────────────────────────────
|
||||||
app_log(f"Poll termine — result_ready={_result_ready}")
|
app_log(f"Poll terminé — result_ready={_result_ready}")
|
||||||
_uncancel()
|
_uncancel()
|
||||||
async with self:
|
async with self:
|
||||||
self.import_in_progress = False
|
self.import_in_progress = False
|
||||||
|
|
@ -601,22 +611,23 @@ class EscadaState(AuthState):
|
||||||
self.sync_res_matu = _result_data.get("res_matu", [])
|
self.sync_res_matu = _result_data.get("res_matu", [])
|
||||||
self.sync_errors = _result_data.get("errors", [])
|
self.sync_errors = _result_data.get("errors", [])
|
||||||
self.sync_done = True
|
self.sync_done = True
|
||||||
app_log("Resultats charges — sync terminee OK")
|
app_log("Résultats chargés — sync terminée OK")
|
||||||
else:
|
else:
|
||||||
self.sync_errors = ["Import timeout — verifiez les logs (> 15min)."]
|
self.sync_errors = ["Import timeout — vérifiez les logs (> 15min)."]
|
||||||
|
|
||||||
# ── Background: push vers Escada ───────────────────────────────────────────
|
# ── Background: push vers Escada ───────────────────────────────────────────
|
||||||
|
|
||||||
@_background
|
@_background
|
||||||
async def push_escada(self):
|
async def push_escada(self):
|
||||||
async with self:
|
async with self:
|
||||||
|
user = self.username or "?"
|
||||||
self.is_pushing = True
|
self.is_pushing = True
|
||||||
self.op_log = "Envoi vers Escadaweb..."
|
self.op_log = "Envoi vers Escadaweb…"
|
||||||
self.push_done = False
|
self.push_done = False
|
||||||
self.push_ok = 0
|
self.push_ok = 0
|
||||||
self.push_errors = []
|
self.push_errors = []
|
||||||
|
|
||||||
app_log("Push Escada demarre")
|
app_log(f"Push Escada démarré par {user}")
|
||||||
extra: list[str] = []
|
extra: list[str] = []
|
||||||
cmd = [sys.executable, str(_PUSH_SCRIPT), *extra]
|
cmd = [sys.executable, str(_PUSH_SCRIPT), *extra]
|
||||||
lines: list[str] = []
|
lines: list[str] = []
|
||||||
|
|
@ -706,7 +717,7 @@ class EscadaState(AuthState):
|
||||||
app_log(f" Erreur parse PUSH_DONE : {_e}", debug=True)
|
app_log(f" Erreur parse PUSH_DONE : {_e}", debug=True)
|
||||||
|
|
||||||
if push_done:
|
if push_done:
|
||||||
app_log(f"Push termine — ok:{push_ok} erreurs:{len(push_errors)}")
|
app_log(f"Push terminé — ok:{push_ok} erreurs:{len(push_errors)}")
|
||||||
else:
|
else:
|
||||||
app_log(f"Push : PUSH_DONE non trouve (code={_rc}, lignes={len(lines)})")
|
app_log(f"Push : PUSH_DONE non trouve (code={_rc}, lignes={len(lines)})")
|
||||||
|
|
||||||
|
|
@ -723,7 +734,7 @@ class EscadaState(AuthState):
|
||||||
self.is_pushing = False
|
self.is_pushing = False
|
||||||
self._reload_pending()
|
self._reload_pending()
|
||||||
except Exception as _e:
|
except Exception as _e:
|
||||||
app_log(f"Erreur mise a jour etat push : {_e}")
|
app_log(f"Erreur mise à jour état push : {_e}")
|
||||||
try:
|
try:
|
||||||
async with self:
|
async with self:
|
||||||
self.is_pushing = False
|
self.is_pushing = False
|
||||||
|
|
@ -910,7 +921,7 @@ def _sync_progress() -> rx.Component:
|
||||||
size="3", font_weight="600", color="#1565c0",
|
size="3", font_weight="600", color="#1565c0",
|
||||||
),
|
),
|
||||||
rx.text(
|
rx.text(
|
||||||
"Telechargement depuis escadaweb.vs.ch (1-3 min)",
|
"Téléchargement depuis escadaweb.vs.ch (1-3 min)",
|
||||||
size="2", color="#555",
|
size="2", color="#555",
|
||||||
),
|
),
|
||||||
spacing="0",
|
spacing="0",
|
||||||
|
|
@ -935,7 +946,7 @@ def _sync_progress() -> rx.Component:
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
# Phase 2 : import en base
|
# Phase 2 : import
|
||||||
rx.cond(
|
rx.cond(
|
||||||
EscadaState.import_in_progress,
|
EscadaState.import_in_progress,
|
||||||
rx.box(
|
rx.box(
|
||||||
|
|
@ -943,11 +954,11 @@ def _sync_progress() -> rx.Component:
|
||||||
rx.spinner(size="3"),
|
rx.spinner(size="3"),
|
||||||
rx.vstack(
|
rx.vstack(
|
||||||
rx.text(
|
rx.text(
|
||||||
"Import des donnees en cours...",
|
"Import des données en cours…",
|
||||||
size="3", font_weight="600", color="#e65100",
|
size="3", font_weight="600", color="#e65100",
|
||||||
),
|
),
|
||||||
rx.text(
|
rx.text(
|
||||||
"Insertion en base de donnees (~30s)",
|
"Insertion dans la DB (~30s)",
|
||||||
size="2", color="#555",
|
size="2", color="#555",
|
||||||
),
|
),
|
||||||
spacing="0",
|
spacing="0",
|
||||||
|
|
@ -969,7 +980,7 @@ def _sync_progress() -> rx.Component:
|
||||||
rx.vstack(
|
rx.vstack(
|
||||||
rx.callout.root(
|
rx.callout.root(
|
||||||
rx.callout.icon(rx.icon("check", size=16)),
|
rx.callout.icon(rx.icon("check", size=16)),
|
||||||
rx.callout.text("Synchronisation et import termines."),
|
rx.callout.text("Synchronisation et import terminés."),
|
||||||
color_scheme="green",
|
color_scheme="green",
|
||||||
variant="soft",
|
variant="soft",
|
||||||
size="1",
|
size="1",
|
||||||
|
|
@ -1061,8 +1072,7 @@ def escada_page() -> rx.Component:
|
||||||
rx.vstack(
|
rx.vstack(
|
||||||
rx.heading("Synchronisation Escada", size="7"),
|
rx.heading("Synchronisation Escada", size="7"),
|
||||||
rx.text(
|
rx.text(
|
||||||
"Telecharge absences, BN, notes et fiches depuis escadaweb.vs.ch "
|
"Télécharge absences, BN, notes et données apprentis depuis Escadaweb.",
|
||||||
"et les importe directement en base.",
|
|
||||||
size="2", color="#666",
|
size="2", color="#666",
|
||||||
),
|
),
|
||||||
|
|
||||||
|
|
@ -1106,7 +1116,7 @@ def escada_page() -> rx.Component:
|
||||||
~EscadaState.has_classes,
|
~EscadaState.has_classes,
|
||||||
rx.box(
|
rx.box(
|
||||||
rx.text(
|
rx.text(
|
||||||
"Cliquez sur Actualiser pour recuperer la liste des classes depuis Escadaweb.",
|
"Cliquez sur Actualiser pour récupérer la liste des classes depuis Escadaweb.",
|
||||||
size="2", color="#555",
|
size="2", color="#555",
|
||||||
),
|
),
|
||||||
padding="0.75rem",
|
padding="0.75rem",
|
||||||
|
|
@ -1133,7 +1143,7 @@ def escada_page() -> rx.Component:
|
||||||
rx.flex(
|
rx.flex(
|
||||||
rx.checkbox(checked=EscadaState.sync_bn,
|
rx.checkbox(checked=EscadaState.sync_bn,
|
||||||
on_change=EscadaState.set_sync_bn, size="2"),
|
on_change=EscadaState.set_sync_bn, size="2"),
|
||||||
rx.text("BN + Matu", size="2"),
|
rx.text("BN + moyennes Matu", size="2"),
|
||||||
gap="0.4rem", align="center",
|
gap="0.4rem", align="center",
|
||||||
),
|
),
|
||||||
rx.flex(
|
rx.flex(
|
||||||
|
|
@ -1145,7 +1155,7 @@ def escada_page() -> rx.Component:
|
||||||
rx.flex(
|
rx.flex(
|
||||||
rx.checkbox(checked=EscadaState.sync_fiches,
|
rx.checkbox(checked=EscadaState.sync_fiches,
|
||||||
on_change=EscadaState.set_sync_fiches, size="2"),
|
on_change=EscadaState.set_sync_fiches, size="2"),
|
||||||
rx.text("Fiches", size="2"),
|
rx.text("Données apprentis", size="2"),
|
||||||
gap="0.4rem", align="center",
|
gap="0.4rem", align="center",
|
||||||
),
|
),
|
||||||
gap="1rem",
|
gap="1rem",
|
||||||
|
|
@ -1155,16 +1165,30 @@ def escada_page() -> rx.Component:
|
||||||
rx.cond(
|
rx.cond(
|
||||||
EscadaState.sync_abs,
|
EscadaState.sync_abs,
|
||||||
rx.flex(
|
rx.flex(
|
||||||
|
rx.icon(
|
||||||
|
"triangle-alert",
|
||||||
|
size=14,
|
||||||
|
color="#b45309",
|
||||||
|
),
|
||||||
rx.checkbox(
|
rx.checkbox(
|
||||||
checked=EscadaState.force_abs,
|
checked=EscadaState.force_abs,
|
||||||
on_change=EscadaState.set_force_abs,
|
on_change=EscadaState.set_force_abs,
|
||||||
size="2",
|
size="2",
|
||||||
|
color_scheme="amber",
|
||||||
),
|
),
|
||||||
rx.text(
|
rx.text(
|
||||||
"Forcer la reimportation des absences existantes",
|
"Les modifications non uploadées sur Escada lors de l'import sont conservées. Forcer la ré-importation complète des absences pour reprendre l'état complet des absences sur Escada.",
|
||||||
size="2", color="#555",
|
size="2",
|
||||||
|
color="#92400e",
|
||||||
|
font_weight="600",
|
||||||
),
|
),
|
||||||
gap="0.4rem", align="center",
|
gap="0.5rem",
|
||||||
|
align="center",
|
||||||
|
padding="0.5rem 0.75rem",
|
||||||
|
background_color="#fef3c7",
|
||||||
|
border="1px solid #fcd34d",
|
||||||
|
border_radius="6px",
|
||||||
|
flex_wrap="wrap",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
|
@ -1284,7 +1308,7 @@ def escada_page() -> rx.Component:
|
||||||
EscadaState.push_ok > 0,
|
EscadaState.push_ok > 0,
|
||||||
rx.text(
|
rx.text(
|
||||||
EscadaState.push_ok,
|
EscadaState.push_ok,
|
||||||
" changement(s) envoye(s) avec succes.",
|
" changement(s) envoyé(s) avec succès.",
|
||||||
size="2", color="#2e7d32", font_weight="600",
|
size="2", color="#2e7d32", font_weight="600",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ from src.db import (
|
||||||
from src.stats import nb_blocs_absences
|
from src.stats import nb_blocs_absences
|
||||||
from src.parser_bn import sem_short_label
|
from src.parser_bn import sem_short_label
|
||||||
from src.email_sender import build_template_vars, render_template
|
from src.email_sender import build_template_vars, render_template
|
||||||
|
from src.logger import app_log
|
||||||
|
|
||||||
MOIS_FR = [
|
MOIS_FR = [
|
||||||
"janvier", "fevrier", "mars", "avril", "mai", "juin",
|
"janvier", "fevrier", "mars", "avril", "mai", "juin",
|
||||||
|
|
@ -313,7 +314,7 @@ def _absence_pdf_apprenti(sess, apprenti) -> bytes:
|
||||||
total_periodes = total_e + total_n
|
total_periodes = total_e + total_n
|
||||||
footer_label = (
|
footer_label = (
|
||||||
f"{len(blocs)} absence(s) | "
|
f"{len(blocs)} absence(s) | "
|
||||||
f"{total_periodes} periode(s) | "
|
f"{total_periodes} période(s) | "
|
||||||
f"{total_e} excusee(s) | "
|
f"{total_e} excusee(s) | "
|
||||||
f"{total_n} non excusee(s)"
|
f"{total_n} non excusee(s)"
|
||||||
)
|
)
|
||||||
|
|
@ -381,6 +382,15 @@ class FicheState(AuthState):
|
||||||
selected_label: str = ""
|
selected_label: str = ""
|
||||||
selected_id: int = 0
|
selected_id: int = 0
|
||||||
has_apprentis: bool = False
|
has_apprentis: bool = False
|
||||||
|
apprenti_search: str = ""
|
||||||
|
apprenti_select_open: bool = False
|
||||||
|
|
||||||
|
@rx.var
|
||||||
|
def filtered_apprenti_labels(self) -> list[str]:
|
||||||
|
q = self.apprenti_search.lower().strip()
|
||||||
|
if not q:
|
||||||
|
return self.apprenti_labels
|
||||||
|
return [l for l in self.apprenti_labels if q in l.lower()]
|
||||||
|
|
||||||
# ── KPIs ─────────────────────────────────────────────────────────────────
|
# ── KPIs ─────────────────────────────────────────────────────────────────
|
||||||
kpi_total: int = 0
|
kpi_total: int = 0
|
||||||
|
|
@ -455,16 +465,23 @@ class FicheState(AuthState):
|
||||||
email_error: str = ""
|
email_error: str = ""
|
||||||
|
|
||||||
# ── Setters (edit periods) ────────────────────────────────────────────────
|
# ── Setters (edit periods) ────────────────────────────────────────────────
|
||||||
def set_edit_p1(self, v: str): self.edit_p1 = v
|
# Note: rx.segmented_control passe str | list[str] — on coerce.
|
||||||
def set_edit_p2(self, v: str): self.edit_p2 = v
|
@staticmethod
|
||||||
def set_edit_p3(self, v: str): self.edit_p3 = v
|
def _coerce_period(v) -> str:
|
||||||
def set_edit_p4(self, v: str): self.edit_p4 = v
|
if isinstance(v, list):
|
||||||
def set_edit_p5(self, v: str): self.edit_p5 = v
|
return v[0] if v else "present"
|
||||||
def set_edit_p6(self, v: str): self.edit_p6 = v
|
return v or "present"
|
||||||
def set_edit_p7(self, v: str): self.edit_p7 = v
|
|
||||||
def set_edit_p8(self, v: str): self.edit_p8 = v
|
def set_edit_p1(self, v): self.edit_p1 = self._coerce_period(v)
|
||||||
def set_edit_p9(self, v: str): self.edit_p9 = v
|
def set_edit_p2(self, v): self.edit_p2 = self._coerce_period(v)
|
||||||
def set_edit_p10(self, v: str): self.edit_p10 = v
|
def set_edit_p3(self, v): self.edit_p3 = self._coerce_period(v)
|
||||||
|
def set_edit_p4(self, v): self.edit_p4 = self._coerce_period(v)
|
||||||
|
def set_edit_p5(self, v): self.edit_p5 = self._coerce_period(v)
|
||||||
|
def set_edit_p6(self, v): self.edit_p6 = self._coerce_period(v)
|
||||||
|
def set_edit_p7(self, v): self.edit_p7 = self._coerce_period(v)
|
||||||
|
def set_edit_p8(self, v): self.edit_p8 = self._coerce_period(v)
|
||||||
|
def set_edit_p9(self, v): self.edit_p9 = self._coerce_period(v)
|
||||||
|
def set_edit_p10(self, v): self.edit_p10 = self._coerce_period(v)
|
||||||
|
|
||||||
# ── Setters (email) ───────────────────────────────────────────────────────
|
# ── Setters (email) ───────────────────────────────────────────────────────
|
||||||
def set_email_dest(self, v: str): self.email_dest = v
|
def set_email_dest(self, v: str): self.email_dest = v
|
||||||
|
|
@ -506,8 +523,18 @@ class FicheState(AuthState):
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
self.edit_date = ""
|
self.edit_date = ""
|
||||||
|
self.apprenti_select_open = False
|
||||||
|
self.apprenti_search = ""
|
||||||
self._reload(reset_email=True)
|
self._reload(reset_email=True)
|
||||||
|
|
||||||
|
def set_apprenti_search(self, v: str):
|
||||||
|
self.apprenti_search = v
|
||||||
|
|
||||||
|
def set_apprenti_select_open(self, v: bool):
|
||||||
|
self.apprenti_select_open = v
|
||||||
|
if not v:
|
||||||
|
self.apprenti_search = ""
|
||||||
|
|
||||||
def navigate_to(self, apprenti_id: int):
|
def navigate_to(self, apprenti_id: int):
|
||||||
if apprenti_id in self.apprenti_ids:
|
if apprenti_id in self.apprenti_ids:
|
||||||
idx = self.apprenti_ids.index(apprenti_id)
|
idx = self.apprenti_ids.index(apprenti_id)
|
||||||
|
|
@ -577,6 +604,12 @@ class FicheState(AuthState):
|
||||||
return
|
return
|
||||||
sess = get_session()
|
sess = get_session()
|
||||||
d = date.fromisoformat(self.edit_date)
|
d = date.fromisoformat(self.edit_date)
|
||||||
|
apprenti = sess.get(Apprenti, self.selected_id)
|
||||||
|
appr_label = (
|
||||||
|
f"{apprenti.nom} {apprenti.prenom} ({apprenti.classe})"
|
||||||
|
if apprenti else f"id={self.selected_id}"
|
||||||
|
)
|
||||||
|
user = self.username or "?"
|
||||||
existing = sess.execute(
|
existing = sess.execute(
|
||||||
select(Absence).where(
|
select(Absence).where(
|
||||||
Absence.apprenti_id == self.selected_id,
|
Absence.apprenti_id == self.selected_id,
|
||||||
|
|
@ -590,21 +623,31 @@ class FicheState(AuthState):
|
||||||
7: self.edit_p7, 8: self.edit_p8, 9: self.edit_p9,
|
7: self.edit_p7, 8: self.edit_p8, 9: self.edit_p9,
|
||||||
10: self.edit_p10,
|
10: self.edit_p10,
|
||||||
}
|
}
|
||||||
|
d_str = d.strftime("%d.%m.%Y")
|
||||||
for p, choice in choices.items():
|
for p, choice in choices.items():
|
||||||
ab = pm.get(p)
|
ab = pm.get(p)
|
||||||
if choice == "present":
|
if choice == "present":
|
||||||
if ab:
|
if ab:
|
||||||
upsert_escada_pending(sess, self.selected_id, d, p, "clear")
|
upsert_escada_pending(sess, self.selected_id, d, p, "clear")
|
||||||
sess.delete(ab)
|
sess.delete(ab)
|
||||||
|
app_log(
|
||||||
|
f"[abs] {user} : {appr_label} — {d_str} P{p} : "
|
||||||
|
f"{ab.type_origine} → présent (suppression)"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
type_o = "E" if choice == "excusee" else "N"
|
type_o = "E" if choice == "excusee" else "N"
|
||||||
statut = "excusee" if choice == "excusee" else "a_traiter"
|
statut = "excusee" if choice == "excusee" else "a_traiter"
|
||||||
if ab:
|
if ab:
|
||||||
if ab.statut != statut:
|
if ab.statut != statut:
|
||||||
|
old_type = ab.type_origine
|
||||||
ab.type_origine = type_o
|
ab.type_origine = type_o
|
||||||
ab.statut = statut
|
ab.statut = statut
|
||||||
ab.updated_by = self.username
|
ab.updated_by = self.username
|
||||||
upsert_escada_pending(sess, self.selected_id, d, p, type_o)
|
upsert_escada_pending(sess, self.selected_id, d, p, type_o)
|
||||||
|
app_log(
|
||||||
|
f"[abs] {user} : {appr_label} — {d_str} P{p} : "
|
||||||
|
f"{old_type} → {type_o}"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
sess.add(Absence(
|
sess.add(Absence(
|
||||||
apprenti_id=self.selected_id,
|
apprenti_id=self.selected_id,
|
||||||
|
|
@ -613,6 +656,10 @@ class FicheState(AuthState):
|
||||||
updated_by=self.username, import_id=None,
|
updated_by=self.username, import_id=None,
|
||||||
))
|
))
|
||||||
upsert_escada_pending(sess, self.selected_id, d, p, type_o)
|
upsert_escada_pending(sess, self.selected_id, d, p, type_o)
|
||||||
|
app_log(
|
||||||
|
f"[abs] {user} : {appr_label} — {d_str} P{p} : "
|
||||||
|
f"présent → {type_o} (création)"
|
||||||
|
)
|
||||||
sess.commit()
|
sess.commit()
|
||||||
self.edit_date = ""
|
self.edit_date = ""
|
||||||
self._reload(reset_email=False)
|
self._reload(reset_email=False)
|
||||||
|
|
@ -621,6 +668,13 @@ class FicheState(AuthState):
|
||||||
def excuse_day(self, date_str: str):
|
def excuse_day(self, date_str: str):
|
||||||
sess = get_session()
|
sess = get_session()
|
||||||
d = date.fromisoformat(date_str)
|
d = date.fromisoformat(date_str)
|
||||||
|
apprenti = sess.get(Apprenti, self.selected_id)
|
||||||
|
appr_label = (
|
||||||
|
f"{apprenti.nom} {apprenti.prenom} ({apprenti.classe})"
|
||||||
|
if apprenti else f"id={self.selected_id}"
|
||||||
|
)
|
||||||
|
user = self.username or "?"
|
||||||
|
d_str = d.strftime("%d.%m.%Y")
|
||||||
absences = sess.execute(
|
absences = sess.execute(
|
||||||
select(Absence).where(
|
select(Absence).where(
|
||||||
Absence.apprenti_id == self.selected_id,
|
Absence.apprenti_id == self.selected_id,
|
||||||
|
|
@ -629,10 +683,15 @@ class FicheState(AuthState):
|
||||||
)
|
)
|
||||||
).scalars().all()
|
).scalars().all()
|
||||||
for ab in absences:
|
for ab in absences:
|
||||||
|
old_type = ab.type_origine
|
||||||
ab.statut = "excusee"
|
ab.statut = "excusee"
|
||||||
ab.type_origine = "E"
|
ab.type_origine = "E"
|
||||||
ab.updated_by = self.username
|
ab.updated_by = self.username
|
||||||
upsert_escada_pending(sess, self.selected_id, d, ab.periode, "E")
|
upsert_escada_pending(sess, self.selected_id, d, ab.periode, "E")
|
||||||
|
app_log(
|
||||||
|
f"[abs] {user} : {appr_label} — {d_str} P{ab.periode} : "
|
||||||
|
f"{old_type} → E (excuse rapide)"
|
||||||
|
)
|
||||||
sess.commit()
|
sess.commit()
|
||||||
if self.edit_date == date_str:
|
if self.edit_date == date_str:
|
||||||
self.edit_date = ""
|
self.edit_date = ""
|
||||||
|
|
@ -991,6 +1050,75 @@ class FicheState(AuthState):
|
||||||
|
|
||||||
# ── UI components ─────────────────────────────────────────────────────────────
|
# ── UI components ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _apprenti_option(label: rx.Var) -> rx.Component:
|
||||||
|
return rx.box(
|
||||||
|
rx.text(label, size="2"),
|
||||||
|
padding="0.45rem 0.75rem",
|
||||||
|
cursor="pointer",
|
||||||
|
on_click=FicheState.handle_select(label),
|
||||||
|
_hover={"background_color": "var(--gray-3)"},
|
||||||
|
width="100%",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _apprenti_searchable_select() -> rx.Component:
|
||||||
|
return rx.popover.root(
|
||||||
|
rx.popover.trigger(
|
||||||
|
rx.box(
|
||||||
|
rx.flex(
|
||||||
|
rx.cond(
|
||||||
|
FicheState.selected_label != "",
|
||||||
|
rx.text(FicheState.selected_label, size="2"),
|
||||||
|
rx.text("Sélectionner un apprenti…", size="2", color="var(--gray-9)"),
|
||||||
|
),
|
||||||
|
rx.spacer(),
|
||||||
|
rx.icon("chevron-down", size=18, color="var(--gray-9)"),
|
||||||
|
align="center",
|
||||||
|
width="100%",
|
||||||
|
),
|
||||||
|
padding="0.5rem 0.75rem",
|
||||||
|
border="1px solid var(--gray-7)",
|
||||||
|
border_radius="6px",
|
||||||
|
background_color="white",
|
||||||
|
cursor="pointer",
|
||||||
|
width="100%",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
rx.popover.content(
|
||||||
|
rx.vstack(
|
||||||
|
rx.input(
|
||||||
|
placeholder="Rechercher un apprenti…",
|
||||||
|
value=FicheState.apprenti_search,
|
||||||
|
on_change=FicheState.set_apprenti_search,
|
||||||
|
size="2",
|
||||||
|
width="100%",
|
||||||
|
auto_focus=True,
|
||||||
|
),
|
||||||
|
rx.cond(
|
||||||
|
FicheState.filtered_apprenti_labels.length() > 0,
|
||||||
|
rx.box(
|
||||||
|
rx.foreach(FicheState.filtered_apprenti_labels, _apprenti_option),
|
||||||
|
max_height="280px",
|
||||||
|
overflow_y="auto",
|
||||||
|
width="100%",
|
||||||
|
),
|
||||||
|
rx.box(
|
||||||
|
rx.text("Aucun résultat", size="2", color="var(--gray-9)"),
|
||||||
|
padding="0.5rem 0.75rem",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
spacing="2",
|
||||||
|
width="100%",
|
||||||
|
),
|
||||||
|
min_width="320px",
|
||||||
|
max_width="500px",
|
||||||
|
padding="0.5rem",
|
||||||
|
),
|
||||||
|
open=FicheState.apprenti_select_open,
|
||||||
|
on_open_change=FicheState.set_apprenti_select_open,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _kpi_card(label: str, value, color: str = "#37474f") -> rx.Component:
|
def _kpi_card(label: str, value, color: str = "#37474f") -> rx.Component:
|
||||||
return rx.box(
|
return rx.box(
|
||||||
rx.text(label, size="1", color="#666"),
|
rx.text(label, size="1", color="#666"),
|
||||||
|
|
@ -1064,16 +1192,20 @@ def _period_select(p_num: int, val, setter) -> rx.Component:
|
||||||
return rx.hstack(
|
return rx.hstack(
|
||||||
rx.text(f"P{p_num}", size="2", weight="medium", color="#555",
|
rx.text(f"P{p_num}", size="2", weight="medium", color="#555",
|
||||||
min_width="28px", text_align="right"),
|
min_width="28px", text_align="right"),
|
||||||
rx.select.root(
|
rx.segmented_control.root(
|
||||||
rx.select.trigger(width="170px"),
|
rx.segmented_control.item("Présent", value="present"),
|
||||||
rx.select.content(
|
rx.segmented_control.item("E", value="excusee"),
|
||||||
rx.select.item("Présent", value="present"),
|
rx.segmented_control.item("N", value="non_excusee"),
|
||||||
rx.select.item("E — Excusée", value="excusee"),
|
|
||||||
rx.select.item("N — Non excusée", value="non_excusee"),
|
|
||||||
),
|
|
||||||
value=val,
|
value=val,
|
||||||
on_change=setter,
|
on_change=setter,
|
||||||
size="1",
|
size="1",
|
||||||
|
color_scheme=rx.match(
|
||||||
|
val,
|
||||||
|
("excusee", "orange"),
|
||||||
|
("non_excusee", "red"),
|
||||||
|
"gray",
|
||||||
|
),
|
||||||
|
radius="medium",
|
||||||
),
|
),
|
||||||
spacing="2",
|
spacing="2",
|
||||||
align="center",
|
align="center",
|
||||||
|
|
@ -1316,13 +1448,8 @@ def fiche_page() -> rx.Component:
|
||||||
FicheState.has_apprentis,
|
FicheState.has_apprentis,
|
||||||
rx.vstack(
|
rx.vstack(
|
||||||
|
|
||||||
# ── Sélecteur apprenti ────────────────────────────────────
|
# ── Sélecteur apprenti (recherche intégrée) ───────────────
|
||||||
rx.select(
|
_apprenti_searchable_select(),
|
||||||
FicheState.apprenti_labels,
|
|
||||||
value=FicheState.selected_label,
|
|
||||||
on_change=FicheState.handle_select,
|
|
||||||
width="100%",
|
|
||||||
),
|
|
||||||
|
|
||||||
# ── KPI cards ─────────────────────────────────────────────
|
# ── KPI cards ─────────────────────────────────────────────
|
||||||
rx.flex(
|
rx.flex(
|
||||||
|
|
@ -1542,7 +1669,7 @@ def fiche_page() -> rx.Component:
|
||||||
rx.hstack(
|
rx.hstack(
|
||||||
rx.icon("clock", size=15, color="#b45309"),
|
rx.icon("clock", size=15, color="#b45309"),
|
||||||
rx.text(
|
rx.text(
|
||||||
"Absences à traiter",
|
"Valider toutes les absences d'une journée",
|
||||||
size="2", weight="bold", color="#92400e",
|
size="2", weight="bold", color="#92400e",
|
||||||
),
|
),
|
||||||
spacing="2", align="center",
|
spacing="2", align="center",
|
||||||
|
|
@ -1592,7 +1719,7 @@ def fiche_page() -> rx.Component:
|
||||||
),
|
),
|
||||||
rx.box(
|
rx.box(
|
||||||
rx.text(
|
rx.text(
|
||||||
"Aucun apprenti en base. Faites d'abord un import.",
|
"Aucun apprenti. Faites d'abord un import.",
|
||||||
size="2", color="#666",
|
size="2", color="#666",
|
||||||
),
|
),
|
||||||
padding="1rem",
|
padding="1rem",
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ _ADMIN_PAGES = [
|
||||||
("Cron", "/cron", "alarm-clock"),
|
("Cron", "/cron", "alarm-clock"),
|
||||||
("Logs", "/logs", "file-text"),
|
("Logs", "/logs", "file-text"),
|
||||||
("Utilisateurs", "/users", "user-cog"),
|
("Utilisateurs", "/users", "user-cog"),
|
||||||
("Parametres", "/params", "settings"),
|
("Paramètres", "/params", "settings"),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
3733
src/app.py
3733
src/app.py
File diff suppressed because it is too large
Load diff
|
|
@ -37,7 +37,7 @@ def import_pdf(
|
||||||
imported_by: str = "system",
|
imported_by: str = "system",
|
||||||
force: bool = False,
|
force: bool = False,
|
||||||
) -> ImportResult:
|
) -> ImportResult:
|
||||||
"""Parse un PDF et insère les absences en base.
|
"""Parse un PDF et insère les absences dans la DB.
|
||||||
|
|
||||||
Déduplication : (apprenti_id, date, periode) déjà présent → ignoré.
|
Déduplication : (apprenti_id, date, periode) déjà présent → ignoré.
|
||||||
Si force=True, réinitialise le statut de toutes les absences existantes
|
Si force=True, réinitialise le statut de toutes les absences existantes
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue