Ajoute colonnes Δ sem. / Fin cible, supprime solde global, indicateurs minima
- Nouvelles colonnes : Δ sem. (solde cumulé semaine) et Fin cible (heure de sortie aprem optimale pour 0h suppl. en fin de semaine) - Δ sem. n'est affiché que quand la journée est complète et la chaîne de jours ininterrompue (pas de saut de jour non saisi) - Fin cible disparaît quand la journée est validée (passée/complète) - Delta = "—" pour les jours non travaillés (futur ou congé jour) - Indicateur warn (fond rouge) sur matin/aprem si horaires < minimum (matin 09:00→12:00, aprem 14:00→17:00) ; mise à jour live via JS - Suppression du solde global (3 cartes au lieu de 4) - Tableau sans largeurs fixes + container 1100 px pour éviter scroll horizontal sur écrans normaux ; touch-scroll conservé sur mobile - Base reste 39h/semaine (7h48/jour) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@ -71,6 +71,51 @@ def compute_week(pointages: list[dict], conges: list[dict], heures_jour_min: int
|
|||||||
"conge": {c["type"] for c in conges if c["date"] == p["date"]},
|
"conge": {c["type"] for c in conges if c["date"] == p["date"]},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# Mark each day as complete (all non-congé slots filled, or day is a congé/weekend)
|
||||||
|
for jour in jours:
|
||||||
|
cg = jour["conge"]
|
||||||
|
matin_done = bool(jour.get("matin_entree") and jour.get("matin_sortie")) or "matin" in cg or "jour" in cg
|
||||||
|
aprem_done = bool(jour.get("aprem_entree") and jour.get("aprem_sortie")) or "aprem" in cg or "jour" in cg
|
||||||
|
jour["is_complete"] = (jour["du_min"] == 0) or (matin_done and aprem_done)
|
||||||
|
|
||||||
|
# Cumulative weekly delta — accumulated for all days, but visible only when chain is unbroken
|
||||||
|
cumul = 0
|
||||||
|
cumul_chain_ok = True
|
||||||
|
for jour in jours:
|
||||||
|
cumul += jour["delta_min"]
|
||||||
|
jour["delta_cumul_min"] = cumul
|
||||||
|
jour["delta_cumul"] = minutes_to_hhmm(abs(cumul))
|
||||||
|
# Chain breaks on the first incomplete workday — subsequent days don't show cumul
|
||||||
|
if not jour["is_complete"] and jour["du_min"] > 0:
|
||||||
|
cumul_chain_ok = False
|
||||||
|
jour["cumul_visible"] = jour["is_complete"] and cumul_chain_ok
|
||||||
|
|
||||||
|
# Sortie cible: optimal aprem exit so the week totals exactly the target hours
|
||||||
|
worked_before = 0
|
||||||
|
for i, jour in enumerate(jours):
|
||||||
|
cg = jour["conge"]
|
||||||
|
if jour["du_min"] == 0 or "aprem" in cg or "jour" in cg:
|
||||||
|
jour["sortie_cible"] = None
|
||||||
|
worked_before += jour["travaille_min"]
|
||||||
|
continue
|
||||||
|
|
||||||
|
du_remaining = sum(j["du_min"] for j in jours[i + 1:])
|
||||||
|
needed_today = max(0, total_du - worked_before - du_remaining)
|
||||||
|
|
||||||
|
worked_morning = 0
|
||||||
|
if jour.get("matin_entree") and jour.get("matin_sortie"):
|
||||||
|
worked_morning = max(0, hhmm_to_minutes(jour["matin_sortie"]) - hhmm_to_minutes(jour["matin_entree"]))
|
||||||
|
|
||||||
|
needed_aprem = max(0, needed_today - worked_morning)
|
||||||
|
aprem_start = hhmm_to_minutes(jour["aprem_entree"]) if jour.get("aprem_entree") else 14 * 60
|
||||||
|
sortie_min = max(aprem_start + needed_aprem, 17 * 60)
|
||||||
|
|
||||||
|
if sortie_min > 23 * 60 + 59:
|
||||||
|
jour["sortie_cible"] = None # deficit too large to recover in one afternoon
|
||||||
|
else:
|
||||||
|
jour["sortie_cible"] = f"{sortie_min // 60:02d}:{sortie_min % 60:02d}"
|
||||||
|
worked_before += jour["travaille_min"]
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"jours": jours,
|
"jours": jours,
|
||||||
"total_travaille": minutes_to_hhmm(total_travaille),
|
"total_travaille": minutes_to_hhmm(total_travaille),
|
||||||
|
|||||||
102
app/main.py
102
app/main.py
@ -43,7 +43,7 @@ def _weeks_by_year(all_weeks: list) -> dict:
|
|||||||
|
|
||||||
|
|
||||||
def _build_soldes_ctx(year: int, week: int, conges: list[dict], h_jour: int) -> dict:
|
def _build_soldes_ctx(year: int, week: int, conges: list[dict], h_jour: int) -> dict:
|
||||||
"""Compute week result + global solde for the soldes partial."""
|
"""Compute week result for the soldes partial."""
|
||||||
dates = week_dates(year, week)
|
dates = week_dates(year, week)
|
||||||
raw = load_week_pointages(year, week)
|
raw = load_week_pointages(year, week)
|
||||||
p_by_date = {p["date"]: p for p in raw}
|
p_by_date = {p["date"]: p for p in raw}
|
||||||
@ -53,21 +53,7 @@ def _build_soldes_ctx(year: int, week: int, conges: list[dict], h_jour: int) ->
|
|||||||
for d in dates
|
for d in dates
|
||||||
]
|
]
|
||||||
result = compute_week(pointages, conges, h_jour)
|
result = compute_week(pointages, conges, h_jour)
|
||||||
|
return {"result": result}
|
||||||
all_weeks = list_all_weeks()
|
|
||||||
total_t = total_d = 0
|
|
||||||
for yw in all_weeks:
|
|
||||||
for p in load_week_pointages(*yw):
|
|
||||||
for f, g in [("matin_sortie", "matin_entree"), ("aprem_sortie", "aprem_entree")]:
|
|
||||||
if p.get(f) and p.get(g):
|
|
||||||
total_t += max(0, hhmm_to_minutes(p[f]) - hhmm_to_minutes(p[g]))
|
|
||||||
total_d += heures_dues(p["date"], conges, h_jour)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"result": result,
|
|
||||||
"solde_global": minutes_to_hhmm(total_t - total_d),
|
|
||||||
"solde_global_min": total_t - total_d,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _build_jour_ctx(date: str, conges: list[dict], h_jour: int) -> dict:
|
def _build_jour_ctx(date: str, conges: list[dict], h_jour: int) -> dict:
|
||||||
@ -95,8 +81,8 @@ def _build_jour_ctx(date: str, conges: list[dict], h_jour: int) -> dict:
|
|||||||
def _oob_calc(date: str, jour: dict) -> str:
|
def _oob_calc(date: str, jour: dict) -> str:
|
||||||
"""Build OOB <span> fragments for the two calc cells (safe non-table elements)."""
|
"""Build OOB <span> fragments for the two calc cells (safe non-table elements)."""
|
||||||
trav_inner = f'<span class="calc-num">{"—" if not jour["travaille_min"] else jour["travaille"]}</span>'
|
trav_inner = f'<span class="calc-num">{"—" if not jour["travaille_min"] else jour["travaille"]}</span>'
|
||||||
|
if jour["travaille_min"]:
|
||||||
delta_cls = "delta-pos" if jour["delta_min"] > 0 else ("delta-neg" if jour["delta_min"] < 0 else "delta-zero")
|
delta_cls = "delta-pos" if jour["delta_min"] > 0 else ("delta-neg" if jour["delta_min"] < 0 else "delta-zero")
|
||||||
if jour["travaille_min"] or jour["du_min"]:
|
|
||||||
sign = "+" if jour["delta_min"] > 0 else ""
|
sign = "+" if jour["delta_min"] > 0 else ""
|
||||||
delta_inner = f'<span class="{delta_cls}">{sign}{jour["delta"]}</span>'
|
delta_inner = f'<span class="{delta_cls}">{sign}{jour["delta"]}</span>'
|
||||||
else:
|
else:
|
||||||
@ -107,20 +93,47 @@ def _oob_calc(date: str, jour: dict) -> str:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _htmx_conge_and_soldes(request: Request, date: str, d, conges: list[dict], h_jour: int) -> HTMLResponse:
|
def _oob_week_extras(result: dict) -> str:
|
||||||
"""Return OOB <div>/<span> fragments for congé state + calc + soldes. No table elements."""
|
"""OOB <span> for all 5 days' cumulative delta and sortie cible."""
|
||||||
iso = d.isocalendar()
|
html = ""
|
||||||
jour = _build_jour_ctx(date, conges, h_jour)
|
for jour in result["jours"]:
|
||||||
|
date = jour["date"]
|
||||||
|
# Cumulative delta — only when chain of complete days is unbroken
|
||||||
|
cm = jour["delta_cumul_min"]
|
||||||
|
if jour.get("cumul_visible"):
|
||||||
|
if cm == 0:
|
||||||
|
cumul_inner = '<span class="delta-zero">0h00</span>'
|
||||||
|
else:
|
||||||
|
cls = "delta-pos" if cm > 0 else "delta-neg"
|
||||||
|
sign = "+" if cm > 0 else "-"
|
||||||
|
cumul_inner = f'<span class="{cls}">{sign}{jour["delta_cumul"]}</span>'
|
||||||
|
else:
|
||||||
|
cumul_inner = '<span class="delta-zero">—</span>'
|
||||||
|
html += f'<span id="calc-{date}-cumul" hx-swap-oob="true">{cumul_inner}</span>'
|
||||||
|
# Sortie cible — only for incomplete days
|
||||||
|
cible = jour.get("sortie_cible") if not jour.get("is_complete") else None
|
||||||
|
cible_inner = f'<span class="cible-num">{cible}</span>' if cible else '<span class="delta-zero">—</span>'
|
||||||
|
html += f'<span id="calc-{date}-cible" hx-swap-oob="true">{cible_inner}</span>'
|
||||||
|
return html
|
||||||
|
|
||||||
|
|
||||||
|
def _oob_time_cells(date: str, jour: dict) -> str:
|
||||||
|
"""OOB <div> for matin/aprem cells with congé state and warn class."""
|
||||||
cg = jour["conge"]
|
cg = jour["conge"]
|
||||||
|
matin_cg = "matin" in cg or "jour" in cg
|
||||||
cg_matin = "matin" in cg or "jour" in cg
|
aprem_cg = "aprem" in cg or "jour" in cg
|
||||||
cg_aprem = "aprem" in cg or "jour" in cg
|
matin_warn = not matin_cg and jour["du_min"] > 0 and (
|
||||||
btn_ma_on = " on" if "matin" in cg else ""
|
(jour.get("matin_entree") and jour["matin_entree"] > "09:00") or
|
||||||
btn_am_on = " on" if "aprem" in cg else ""
|
(jour.get("matin_sortie") and jour["matin_sortie"] < "12:00")
|
||||||
btn_j_on = " jour-on" if "jour" in cg else ""
|
)
|
||||||
|
aprem_warn = not aprem_cg and jour["du_min"] > 0 and (
|
||||||
|
(jour.get("aprem_entree") and jour["aprem_entree"] > "14:00") or
|
||||||
|
(jour.get("aprem_sortie") and jour["aprem_sortie"] < "17:00")
|
||||||
|
)
|
||||||
|
matin_cls = "td-fill cg" if matin_cg else ("td-fill warn" if matin_warn else "td-fill")
|
||||||
|
aprem_cls = "td-fill cg" if aprem_cg else ("td-fill warn" if aprem_warn else "td-fill")
|
||||||
matin_html = (
|
matin_html = (
|
||||||
f'<div id="cg-matin-{date}" hx-swap-oob="true" class="td-fill{" cg" if cg_matin else ""}">'
|
f'<div id="cg-matin-{date}" hx-swap-oob="true" class="{matin_cls}">'
|
||||||
f'<div class="time-pair">'
|
f'<div class="time-pair">'
|
||||||
f'<input class="time-input" type="time" name="matin_entree" data-date="{date}" value="{jour["matin_entree"] or ""}">'
|
f'<input class="time-input" type="time" name="matin_entree" data-date="{date}" value="{jour["matin_entree"] or ""}">'
|
||||||
f'<span class="time-sep">→</span>'
|
f'<span class="time-sep">→</span>'
|
||||||
@ -128,13 +141,25 @@ def _htmx_conge_and_soldes(request: Request, date: str, d, conges: list[dict], h
|
|||||||
f'</div></div>'
|
f'</div></div>'
|
||||||
)
|
)
|
||||||
aprem_html = (
|
aprem_html = (
|
||||||
f'<div id="cg-aprem-{date}" hx-swap-oob="true" class="td-fill{" cg" if cg_aprem else ""}">'
|
f'<div id="cg-aprem-{date}" hx-swap-oob="true" class="{aprem_cls}">'
|
||||||
f'<div class="time-pair">'
|
f'<div class="time-pair">'
|
||||||
f'<input class="time-input" type="time" name="aprem_entree" data-date="{date}" value="{jour["aprem_entree"] or ""}">'
|
f'<input class="time-input" type="time" name="aprem_entree" data-date="{date}" value="{jour["aprem_entree"] or ""}">'
|
||||||
f'<span class="time-sep">→</span>'
|
f'<span class="time-sep">→</span>'
|
||||||
f'<input class="time-input" type="time" name="aprem_sortie" data-date="{date}" value="{jour["aprem_sortie"] or ""}">'
|
f'<input class="time-input" type="time" name="aprem_sortie" data-date="{date}" value="{jour["aprem_sortie"] or ""}">'
|
||||||
f'</div></div>'
|
f'</div></div>'
|
||||||
)
|
)
|
||||||
|
return matin_html + aprem_html
|
||||||
|
|
||||||
|
|
||||||
|
def _htmx_conge_and_soldes(request: Request, date: str, d, conges: list[dict], h_jour: int) -> HTMLResponse:
|
||||||
|
"""Return OOB <div>/<span> fragments for congé state + calc + soldes. No table elements."""
|
||||||
|
iso = d.isocalendar()
|
||||||
|
jour = _build_jour_ctx(date, conges, h_jour)
|
||||||
|
cg = jour["conge"]
|
||||||
|
|
||||||
|
btn_ma_on = " on" if "matin" in cg else ""
|
||||||
|
btn_am_on = " on" if "aprem" in cg else ""
|
||||||
|
btn_j_on = " jour-on" if "jour" in cg else ""
|
||||||
btns_html = (
|
btns_html = (
|
||||||
f'<div id="cg-btn-{date}" hx-swap-oob="true">'
|
f'<div id="cg-btn-{date}" hx-swap-oob="true">'
|
||||||
f'<div class="conge-btns">'
|
f'<div class="conge-btns">'
|
||||||
@ -146,16 +171,27 @@ def _htmx_conge_and_soldes(request: Request, date: str, d, conges: list[dict], h
|
|||||||
|
|
||||||
soldes_ctx = _build_soldes_ctx(iso.year, iso.week, conges, h_jour)
|
soldes_ctx = _build_soldes_ctx(iso.year, iso.week, conges, h_jour)
|
||||||
soldes_html = templates.get_template("_soldes.html").render({"request": request, **soldes_ctx})
|
soldes_html = templates.get_template("_soldes.html").render({"request": request, **soldes_ctx})
|
||||||
return HTMLResponse(matin_html + aprem_html + btns_html + _oob_calc(date, jour) + soldes_html)
|
return HTMLResponse(
|
||||||
|
_oob_time_cells(date, jour)
|
||||||
|
+ btns_html
|
||||||
|
+ _oob_calc(date, jour)
|
||||||
|
+ _oob_week_extras(soldes_ctx["result"])
|
||||||
|
+ soldes_html
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _htmx_calc_and_soldes(request: Request, date: str, d, conges: list[dict], h_jour: int) -> HTMLResponse:
|
def _htmx_calc_and_soldes(request: Request, date: str, d, conges: list[dict], h_jour: int) -> HTMLResponse:
|
||||||
"""Return OOB <span> for calc cells + OOB <div> for soldes. No table elements."""
|
"""Return OOB <span>/<div> for calc cells, time cells, week extras, and soldes."""
|
||||||
iso = d.isocalendar()
|
iso = d.isocalendar()
|
||||||
jour = _build_jour_ctx(date, conges, h_jour)
|
jour = _build_jour_ctx(date, conges, h_jour)
|
||||||
soldes_ctx = _build_soldes_ctx(iso.year, iso.week, conges, h_jour)
|
soldes_ctx = _build_soldes_ctx(iso.year, iso.week, conges, h_jour)
|
||||||
soldes_html = templates.get_template("_soldes.html").render({"request": request, **soldes_ctx})
|
soldes_html = templates.get_template("_soldes.html").render({"request": request, **soldes_ctx})
|
||||||
return HTMLResponse(_oob_calc(date, jour) + soldes_html)
|
return HTMLResponse(
|
||||||
|
_oob_calc(date, jour)
|
||||||
|
+ _oob_time_cells(date, jour)
|
||||||
|
+ _oob_week_extras(soldes_ctx["result"])
|
||||||
|
+ soldes_html
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.get("/", response_class=HTMLResponse)
|
@app.get("/", response_class=HTMLResponse)
|
||||||
|
|||||||
@ -8,7 +8,7 @@ CONGES_FILE = DATA_DIR / "conges.json"
|
|||||||
CONFIG_FILE = DATA_DIR / "config.json"
|
CONFIG_FILE = DATA_DIR / "config.json"
|
||||||
|
|
||||||
DEFAULT_CONFIG = {
|
DEFAULT_CONFIG = {
|
||||||
"heures_jour": 7.8, # 7h48 = 7.8 h
|
"heures_jour": 7.8, # 7h48 = 39h/semaine
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,13 @@
|
|||||||
|
{% set matin_cg = 'matin' in j.conge or 'jour' in j.conge %}
|
||||||
|
{% set aprem_cg = 'aprem' in j.conge or 'jour' in j.conge %}
|
||||||
|
{% set matin_warn = not matin_cg and j.du_min > 0 and (
|
||||||
|
(j.matin_entree and j.matin_entree > '09:00') or
|
||||||
|
(j.matin_sortie and j.matin_sortie < '12:00')
|
||||||
|
) %}
|
||||||
|
{% set aprem_warn = not aprem_cg and j.du_min > 0 and (
|
||||||
|
(j.aprem_entree and j.aprem_entree > '14:00') or
|
||||||
|
(j.aprem_sortie and j.aprem_sortie < '17:00')
|
||||||
|
) %}
|
||||||
<tr id="row-{{ j.date }}">
|
<tr id="row-{{ j.date }}">
|
||||||
|
|
||||||
<td class="col-jour">
|
<td class="col-jour">
|
||||||
@ -6,7 +16,7 @@
|
|||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td class="c-matin">
|
<td class="c-matin">
|
||||||
<div id="cg-matin-{{ j.date }}" class="td-fill{% if 'matin' in j.conge or 'jour' in j.conge %} cg{% endif %}">
|
<div id="cg-matin-{{ j.date }}" class="td-fill{% if matin_cg %} cg{% elif matin_warn %} warn{% endif %}">
|
||||||
<div class="time-pair">
|
<div class="time-pair">
|
||||||
<input class="time-input" type="time" name="matin_entree"
|
<input class="time-input" type="time" name="matin_entree"
|
||||||
data-date="{{ j.date }}" value="{{ j.matin_entree or '' }}">
|
data-date="{{ j.date }}" value="{{ j.matin_entree or '' }}">
|
||||||
@ -18,7 +28,7 @@
|
|||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td class="c-aprem">
|
<td class="c-aprem">
|
||||||
<div id="cg-aprem-{{ j.date }}" class="td-fill{% if 'aprem' in j.conge or 'jour' in j.conge %} cg{% endif %}">
|
<div id="cg-aprem-{{ j.date }}" class="td-fill{% if aprem_cg %} cg{% elif aprem_warn %} warn{% endif %}">
|
||||||
<div class="time-pair">
|
<div class="time-pair">
|
||||||
<input class="time-input" type="time" name="aprem_entree"
|
<input class="time-input" type="time" name="aprem_entree"
|
||||||
data-date="{{ j.date }}" value="{{ j.aprem_entree or '' }}">
|
data-date="{{ j.date }}" value="{{ j.aprem_entree or '' }}">
|
||||||
@ -37,14 +47,36 @@
|
|||||||
|
|
||||||
<td class="col-calc">
|
<td class="col-calc">
|
||||||
<span id="calc-{{ j.date }}-delta">
|
<span id="calc-{{ j.date }}-delta">
|
||||||
<span class="{% if j.delta_min > 0 %}delta-pos{% elif j.delta_min < 0 %}delta-neg{% else %}delta-zero{% endif %}">
|
<span class="{% if j.travaille_min and j.delta_min > 0 %}delta-pos{% elif j.travaille_min and j.delta_min < 0 %}delta-neg{% else %}delta-zero{% endif %}">
|
||||||
{%- if j.travaille_min or j.du_min -%}
|
{%- if j.travaille_min -%}
|
||||||
{% if j.delta_min > 0 %}+{% endif %}{{ j.delta }}
|
{% if j.delta_min > 0 %}+{% endif %}{{ j.delta }}
|
||||||
{%- else %}—{%- endif %}
|
{%- else %}—{%- endif %}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
|
<td class="col-cumul">
|
||||||
|
<span id="calc-{{ j.date }}-cumul">
|
||||||
|
{%- if j.cumul_visible -%}
|
||||||
|
<span class="{% if j.delta_cumul_min > 0 %}delta-pos{% elif j.delta_cumul_min < 0 %}delta-neg{% else %}delta-zero{% endif %}">
|
||||||
|
{% if j.delta_cumul_min > 0 %}+{% elif j.delta_cumul_min < 0 %}-{% endif %}{{ j.delta_cumul if j.delta_cumul_min != 0 else '0h00' }}
|
||||||
|
</span>
|
||||||
|
{%- else -%}
|
||||||
|
<span class="delta-zero">—</span>
|
||||||
|
{%- endif %}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td class="col-cible">
|
||||||
|
<span id="calc-{{ j.date }}-cible">
|
||||||
|
{%- if j.sortie_cible and not j.is_complete -%}
|
||||||
|
<span class="cible-num">{{ j.sortie_cible }}</span>
|
||||||
|
{%- else -%}
|
||||||
|
<span class="delta-zero">—</span>
|
||||||
|
{%- endif %}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
|
||||||
<td class="col-save">
|
<td class="col-save">
|
||||||
<button class="btn-valider"
|
<button class="btn-valider"
|
||||||
hx-post="/pointage/{{ j.date }}"
|
hx-post="/pointage/{{ j.date }}"
|
||||||
|
|||||||
@ -14,11 +14,5 @@
|
|||||||
{% if result.solde_min > 0 %}+{% endif %}{{ result.solde }}
|
{% if result.solde_min > 0 %}+{% endif %}{{ result.solde }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="solde-item {% if solde_global_min > 0 %}item-pos{% elif solde_global_min < 0 %}item-neg{% endif %}">
|
|
||||||
<div class="solde-eyebrow">Solde global</div>
|
|
||||||
<div class="solde-num {% if solde_global_min > 0 %}pos{% elif solde_global_min < 0 %}neg{% endif %}">
|
|
||||||
{% if solde_global_min > 0 %}+{% endif %}{{ solde_global }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -82,7 +82,7 @@
|
|||||||
|
|
||||||
/* ── Layout ── */
|
/* ── Layout ── */
|
||||||
.container {
|
.container {
|
||||||
max-width: 940px;
|
max-width: 1100px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 2rem 1.5rem;
|
padding: 2rem 1.5rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -124,7 +124,7 @@
|
|||||||
/* ── Soldes ── */
|
/* ── Soldes ── */
|
||||||
.soldes {
|
.soldes {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(4, 1fr);
|
grid-template-columns: repeat(3, 1fr);
|
||||||
border: 1.5px solid var(--rule);
|
border: 1.5px solid var(--rule);
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
}
|
}
|
||||||
@ -156,12 +156,12 @@
|
|||||||
.solde-num.neutral { color: var(--text); }
|
.solde-num.neutral { color: var(--text); }
|
||||||
|
|
||||||
/* ── Table ── */
|
/* ── Table ── */
|
||||||
.table-section { overflow-x: auto; }
|
.table-section { overflow-x: auto; -webkit-overflow-scrolling: touch; }
|
||||||
table {
|
table {
|
||||||
width: 100%;
|
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
border: 1.5px solid var(--rule);
|
border: 1.5px solid var(--rule);
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.th-group th {
|
.th-group th {
|
||||||
@ -272,14 +272,27 @@
|
|||||||
.time-input:focus { outline: none; border-color: var(--accent); background: #fff; }
|
.time-input:focus { outline: none; border-color: var(--accent); background: #fff; }
|
||||||
tr.htmx-request .time-input { opacity: .4; }
|
tr.htmx-request .time-input { opacity: .4; }
|
||||||
|
|
||||||
.col-calc { text-align: right; white-space: nowrap; width: 72px; }
|
.col-calc { text-align: right; padding-left: .5rem; }
|
||||||
.calc-num { font-family: var(--mono); font-size: 13px; color: var(--muted); }
|
.calc-num { font-family: var(--mono); font-size: 13px; color: var(--muted); }
|
||||||
.delta-pos { font-family: var(--mono); font-size: 13px; font-weight: 500; color: var(--pos); }
|
.delta-pos { font-family: var(--mono); font-size: 13px; font-weight: 500; color: var(--pos); }
|
||||||
.delta-neg { font-family: var(--mono); font-size: 13px; font-weight: 500; color: var(--neg); }
|
.delta-neg { font-family: var(--mono); font-size: 13px; font-weight: 500; color: var(--neg); }
|
||||||
.delta-zero{ font-family: var(--mono); font-size: 13px; color: var(--rule); }
|
.delta-zero{ font-family: var(--mono); font-size: 13px; color: var(--rule); }
|
||||||
|
|
||||||
.col-save { width: 36px; text-align: center; padding: .4rem .3rem; }
|
/* Warn state: time input violates minimum directive */
|
||||||
.g-save { width: 36px; background: var(--ground); }
|
td.c-matin .td-fill.warn,
|
||||||
|
td.c-aprem .td-fill.warn { border-left-color: var(--neg); background: var(--neg-bg); }
|
||||||
|
.td-fill.warn .time-input { background: transparent; }
|
||||||
|
tbody tr:hover .td-fill.warn { background: var(--neg-bg); }
|
||||||
|
|
||||||
|
/* Cumulative delta and sortie cible columns */
|
||||||
|
.col-cumul { text-align: right; padding-left: .5rem; }
|
||||||
|
.col-cible { text-align: right; padding-left: .5rem; }
|
||||||
|
.g-cumul { text-align: right; }
|
||||||
|
.g-cible { text-align: right; }
|
||||||
|
.cible-num { font-family: var(--mono); font-size: 12px; color: var(--accent); font-weight: 500; }
|
||||||
|
|
||||||
|
.col-save { text-align: center; padding: .4rem .3rem; }
|
||||||
|
.g-save { background: var(--ground); }
|
||||||
.btn-valider {
|
.btn-valider {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -294,7 +307,7 @@
|
|||||||
}
|
}
|
||||||
.btn-valider:hover { background: var(--accent); color: #fff; }
|
.btn-valider:hover { background: var(--accent); color: #fff; }
|
||||||
.btn-valider.htmx-request { opacity: .4; pointer-events: none; }
|
.btn-valider.htmx-request { opacity: .4; pointer-events: none; }
|
||||||
.col-conge { width: 88px; text-align: center; }
|
.col-conge { text-align: center; }
|
||||||
.conge-btns { display: flex; gap: 3px; justify-content: center; }
|
.conge-btns { display: flex; gap: 3px; justify-content: center; }
|
||||||
.btn-cg {
|
.btn-cg {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
|
|||||||
@ -46,7 +46,9 @@
|
|||||||
<th class="g-matin">Matin</th>
|
<th class="g-matin">Matin</th>
|
||||||
<th class="g-aprem">Après-midi</th>
|
<th class="g-aprem">Après-midi</th>
|
||||||
<th class="g-calc" rowspan="2">Travaillé</th>
|
<th class="g-calc" rowspan="2">Travaillé</th>
|
||||||
<th class="g-calc" rowspan="2">Delta</th>
|
<th class="g-calc" rowspan="2">Δ jour</th>
|
||||||
|
<th class="g-cumul" rowspan="2">Δ sem.</th>
|
||||||
|
<th class="g-cible" rowspan="2">Fin cible</th>
|
||||||
<th class="g-save" rowspan="2"></th>
|
<th class="g-save" rowspan="2"></th>
|
||||||
<th class="g-conge" rowspan="2">Congé</th>
|
<th class="g-conge" rowspan="2">Congé</th>
|
||||||
</tr>
|
</tr>
|
||||||
@ -202,4 +204,25 @@
|
|||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Live warn on time inputs that violate minimum directives
|
||||||
|
document.addEventListener('input', function(e) {
|
||||||
|
if (!e.target.matches('.time-input')) return;
|
||||||
|
var date = e.target.dataset.date;
|
||||||
|
var me = document.querySelector('[name="matin_entree"][data-date="' + date + '"]');
|
||||||
|
var ms = document.querySelector('[name="matin_sortie"][data-date="' + date + '"]');
|
||||||
|
var ae = document.querySelector('[name="aprem_entree"][data-date="' + date + '"]');
|
||||||
|
var as = document.querySelector('[name="aprem_sortie"][data-date="' + date + '"]');
|
||||||
|
var maDiv = document.getElementById('cg-matin-' + date);
|
||||||
|
var apDiv = document.getElementById('cg-aprem-' + date);
|
||||||
|
if (maDiv && !maDiv.classList.contains('cg')) {
|
||||||
|
var warn = (me && me.value && me.value > '09:00') || (ms && ms.value && ms.value < '12:00');
|
||||||
|
maDiv.classList.toggle('warn', !!warn);
|
||||||
|
}
|
||||||
|
if (apDiv && !apDiv.classList.contains('cg')) {
|
||||||
|
var warnA = (ae && ae.value && ae.value > '14:00') || (as && as.value && as.value < '17:00');
|
||||||
|
apDiv.classList.toggle('warn', !!warnA);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
Reference in New Issue
Block a user