feat(5.13): carduri compacte mobil/tableta + fix editare preview (OOB tr) + toast
Dogfood pe import + Trimiteri (mobil/tableta <1024px), pur CSS + markup, backend
trimitere neatins:
- Card compact real pentru .tabel-trimiteri (preview + Trimiteri): vehicul=titlu,
stare=pill dreapta-sus, operatie+cod, meta data/km muted, nota mica. Inlocuieste
stiva generica eticheta+valoare (carduri de ~450px -> ~135px). Anuleaza regula
desktop tr.trimitere-row > td{padding:11px} in blocul compact.
- FIX editare preview: OOB swap pe <tr> esua tacit in htmx 1.9 (un <tr> brut se
pierde la parsarea unui fragment fara context de tabel) -> randul ramanea cu
starea veche dupa salvare. Inlocuit cu reload complet al preview-ului prin
HX-Trigger:reincarcaPreview + detalii randSalvat. /editeaza si /confirma-review
folosesc helper-ul _raspuns_rand_salvat.
- Feedback post-salvare: toast global "Randul N actualizat · <stare>" + scroll +
flash pe randul actualizat (base.html window.arataToast + listener randSalvat).
- Modal editare: Salveaza + Anuleaza pe acelasi rand (sistem .act): desktop text,
mobil doua iconite Lucide 44px alaturate (save/x). Macro icon('x') + .act-primary.
- Randuri deja-trimise/duplicate colapsate implicit in preview + toggle "Arata N".
- Select "Operatii de mapat" full-width pe mobil (nu mai iese din viewport).
- Bara de filtre Trimiteri adaptata mobil: pills pe banda cu scroll orizontal,
cautare vehicul proeminenta (nu 8 butoane full-width stivuite).
- Nota preview = culoarea camp-fix (accent) ca sa atraga atentia; hint-urile
camp-fix per-camp scoase (campul Note e self-explanatory).
- Confirmare trimitere: scos campul email (Declarant); text mai clar
("Confirma numarul din N gata de trimis"). Backend confirmed_by ramane optional.
Teste: contractul OOB (rupt in browser) inlocuit cu noul contract
(reincarcaPreview + randSalvat) in test_web_preview_edit / test_preview_edit_ui /
test_import_review. Suita: 992 passed (exclus live).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2380,6 +2380,33 @@ def _preview_one_row(conn, import_id: int, account_id: int, row_index: int):
|
||||
return result, row
|
||||
|
||||
|
||||
def _raspuns_rand_salvat(import_id: int, row: dict) -> HTMLResponse:
|
||||
"""Raspuns dupa salvarea/confirmarea unui rand de preview.
|
||||
|
||||
NU mai face OOB swap pe `<tr>` (htmx 1.9 pierde un `<tr>` brut la parsarea unui
|
||||
fragment care nu incepe cu context de tabel -> swap-ul esua tacit, randul ramanea
|
||||
cu starea veche; cauza confirmata a bug-ului 'editez si ramane la fel'). In schimb:
|
||||
- `reincarcaPreview` (HX-Trigger) -> #import-section isi reincarca preview-ul complet
|
||||
(rand + contoare + colaps deja-trimise, toate corecte, fara fragilitatea OOB).
|
||||
- `randSalvat` (HX-Trigger, cu detalii) -> base.html arata un toast cu numarul randului
|
||||
si noua stare, apoi evidentiaza randul dupa reload (feedback vizual clar: ce s-a salvat).
|
||||
- `inchideModal` (HX-Trigger-After-Settle) -> inchide modalul.
|
||||
"""
|
||||
payload = {
|
||||
"reincarcaPreview": True,
|
||||
"randSalvat": {
|
||||
"nr": row["row_index"] + 1,
|
||||
"rowIndex": row["row_index"],
|
||||
"stare": row.get("stare_eticheta") or "",
|
||||
"stareCss": row.get("stare_css") or "",
|
||||
},
|
||||
}
|
||||
resp = HTMLResponse(content='<div style="display:none;"></div>')
|
||||
resp.headers["HX-Trigger"] = json.dumps(payload)
|
||||
resp.headers["HX-Trigger-After-Settle"] = "inchideModal"
|
||||
return resp
|
||||
|
||||
|
||||
def _render_preview_rand(
|
||||
request: Request, *, import_id: int, row: dict, editing: bool,
|
||||
include_oob: bool, summary: dict, message: str | None = None,
|
||||
@@ -2537,25 +2564,8 @@ async def web_editeaza_rand(request: Request, import_id: int, row_index: int) ->
|
||||
"message": "Mai sunt valori invalide — corecteaza campurile marcate.",
|
||||
})
|
||||
|
||||
# Succes: OOB swap rand + contoare + inchideModal.
|
||||
# Continut primar (swap in #detaliu-modal-body): stub invizibil + script recalc.
|
||||
# OOB: <tr> actualizat + rezumat + contor.
|
||||
# HX-Trigger-After-Settle: inchideModal → base.html JS inchide modalul.
|
||||
oob_content = templates.get_template("_preview_rand.html").render({
|
||||
"request": request,
|
||||
"import_id": import_id,
|
||||
"row": row,
|
||||
"editing": False,
|
||||
"oob_tr": True,
|
||||
"include_oob": True,
|
||||
"summary": result["summary"],
|
||||
"message": None,
|
||||
"csrf_token": get_csrf_token(request),
|
||||
})
|
||||
html_body = '<div style="display:none;"></div>' + oob_content
|
||||
resp = HTMLResponse(content=html_body)
|
||||
resp.headers["HX-Trigger-After-Settle"] = "inchideModal"
|
||||
return resp
|
||||
# Succes: reincarca preview-ul complet + toast + inchide modal (vezi helper).
|
||||
return _raspuns_rand_salvat(import_id, row)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
@@ -2604,22 +2614,8 @@ async def web_confirma_review(
|
||||
if row is None or isinstance(result, str):
|
||||
raise HTTPException(status_code=404, detail="rand de import inexistent")
|
||||
|
||||
# OOB: rand actualizat + rezumat + contor ok + inchideModal (identic cu succes editeaza)
|
||||
oob_content = templates.get_template("_preview_rand.html").render({
|
||||
"request": request,
|
||||
"import_id": import_id,
|
||||
"row": row,
|
||||
"editing": False,
|
||||
"oob_tr": True,
|
||||
"include_oob": True,
|
||||
"summary": result["summary"],
|
||||
"message": None,
|
||||
"csrf_token": get_csrf_token(request),
|
||||
})
|
||||
html_body = '<div style="display:none;"></div>' + oob_content
|
||||
resp = HTMLResponse(content=html_body)
|
||||
resp.headers["HX-Trigger-After-Settle"] = "inchideModal"
|
||||
return resp
|
||||
# Reincarca preview-ul complet + toast + inchide modal (identic cu succes editeaza).
|
||||
return _raspuns_rand_salvat(import_id, row)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
@@ -45,14 +45,10 @@
|
||||
Salvarea nu a reusit (retea / sesiune). Valorile introduse sunt pastrate — reincearca.
|
||||
</div>
|
||||
|
||||
{# with_cancel=True: _form_editare.html randeaza Salveaza + Anuleaza pe acelasi
|
||||
rand (sistemul .act: desktop text, mobil iconite Lucide 44px alaturate). #}
|
||||
{% set with_cancel = true %}
|
||||
{% include "_form_editare.html" %}
|
||||
|
||||
<div style="margin-top:10px; display:flex; gap:8px; align-items:center; flex-wrap:wrap;">
|
||||
<button type="button"
|
||||
style="min-height:44px; padding:8px 18px;
|
||||
background:var(--card); color:var(--muted); border-color:var(--line);"
|
||||
data-modal-close>Anuleaza</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{% if is_needs_review %}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
vin_context — string VIN pentru aria-label (poate fi '')
|
||||
btn_label — eticheta butonului primar (ex. 'Salveaza si retrimite')
|
||||
#}
|
||||
{% from "_macros.html" import camp %}
|
||||
{% from "_macros.html" import camp, icon %}
|
||||
|
||||
{# Nr. inmatriculare pe rand propriu, VIN dedesubt — ambele latime plina. #}
|
||||
{{ camp('nr_inmatriculare', 'Numar inmatriculare', form_nr,
|
||||
@@ -35,7 +35,19 @@
|
||||
err_map=err_map, fix_map=fix_map, vin_context=vin_context) }}
|
||||
</div>
|
||||
|
||||
{# Buton primar parametrizat. #}
|
||||
{# Buton primar parametrizat.
|
||||
with_cancel=True (modal editare preview): Salveaza + Anuleaza pe ACELASI rand,
|
||||
sistemul .act (desktop = text alaturat; mobil = doua iconite Lucide 44px alaturate).
|
||||
Implicit (ex. _trimitere_detaliu): un singur buton text, neschimbat. #}
|
||||
{% if with_cancel %}
|
||||
<div class="act-group" style="margin-top:14px;">
|
||||
<button type="submit" class="act act-primary" aria-label="{{ btn_label or 'Salveaza' }}">
|
||||
<span class="act-tx">{{ btn_label or 'Salveaza' }}</span>{{ icon('save') }}</button>
|
||||
<button type="button" class="act" aria-label="{{ cancel_label or 'Anuleaza' }}" data-modal-close>
|
||||
<span class="act-tx">{{ cancel_label or 'Anuleaza' }}</span>{{ icon('x') }}</button>
|
||||
</div>
|
||||
{% else %}
|
||||
<div style="margin-top:14px;">
|
||||
<button type="submit">{{ btn_label or 'Salveaza' }}</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -50,3 +50,25 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{# PRD 5.13 — sistem butoane de actiune responsive.
|
||||
CSS-ul aferent (.act, .act-tx, .act-ic, .act-save, .act-del, .act-group)
|
||||
este definit in base.html.
|
||||
Desktop: se afiseaza textul (.act-tx); mobil: se afiseaza iconita (.act-ic). #}
|
||||
|
||||
{% macro icon(name) -%}
|
||||
<svg class="act-ic" viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor"
|
||||
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
{%- if name == 'save' -%}<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/>
|
||||
{%- elif name == 'trash' -%}<polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><line x1="10" y1="11" x2="10" y2="17"/><line x1="14" y1="11" x2="14" y2="17"/>
|
||||
{%- elif name == 'edit' -%}<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
|
||||
{%- elif name == 'plus' -%}<line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>
|
||||
{%- elif name == 'x' -%}<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
|
||||
{%- endif -%}
|
||||
</svg>
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro act_btn(label, ic, kind='', attrs='') -%}
|
||||
<button class="act{% if kind %} act-{{ kind }}{% endif %}" aria-label="{{ label }}" {{ attrs | safe }}>
|
||||
<span class="act-tx">{{ label }}</span>{{ icon(ic) }}</button>
|
||||
{%- endmacro %}
|
||||
|
||||
@@ -135,24 +135,11 @@
|
||||
</select>
|
||||
</td>
|
||||
<td style="text-align:right; white-space:nowrap;" data-eticheta="Actiuni">
|
||||
{# Butoane icon mereu vizibile (fara kebab). SVG aria-hidden; aria-label pe buton.
|
||||
{# Butoane act_btn (desktop: text; mobil: iconita 44px).
|
||||
data-dirty-form e citit de JS din base.html: la schimbarea select-ului din acelasi rand,
|
||||
JS adauga clasa "dirty" pe butonul de salvare (fundal --accent = modificari nesalvate). #}
|
||||
<button type="submit" form="map-salv-{{ loop.index }}"
|
||||
class="icon-btn"
|
||||
data-dirty-form="map-salv-{{ loop.index }}"
|
||||
aria-label="Salveaza maparea pentru {{ m.cod_op_service }}">
|
||||
<svg aria-hidden="true" viewBox="0 0 16 16" width="16" height="16" fill="currentColor">
|
||||
<path d="M2 2a1 1 0 011-1h7.5L13 3.5V14a1 1 0 01-1 1H3a1 1 0 01-1-1V2zm5 10a2 2 0 100-4 2 2 0 000 4zM3 3v3h6V3H3z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button type="submit" form="map-del-{{ loop.index }}"
|
||||
class="icon-btn danger"
|
||||
aria-label="Sterge maparea pentru {{ m.cod_op_service }}">
|
||||
<svg aria-hidden="true" viewBox="0 0 16 16" width="16" height="16" fill="currentColor">
|
||||
<path d="M5.5 5.5A.5.5 0 016 6v6a.5.5 0 01-1 0V6a.5.5 0 01.5-.5zm2.5 0a.5.5 0 01.5.5v6a.5.5 0 01-1 0V6a.5.5 0 01.5-.5zm3 .5a.5.5 0 00-1 0v6a.5.5 0 001 0V6zM14 3a1 1 0 01-1 1H3a1 1 0 110-2h3.5l1-1h2l1 1H13a1 1 0 011 1zm-1 1H3v9a1 1 0 001 1h8a1 1 0 001-1V4z"/>
|
||||
</svg>
|
||||
</button>
|
||||
{{ ui.act_btn('Salveaza', 'save', 'save', 'type="submit" form="map-salv-' ~ loop.index ~ '" data-dirty-form="map-salv-' ~ loop.index ~ '"') }}
|
||||
{{ ui.act_btn('Sterge', 'trash', 'del', 'type="submit" form="map-del-' ~ loop.index ~ '"') }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
@@ -206,10 +193,7 @@
|
||||
{{ r.cod_prestatie }}
|
||||
</td>
|
||||
<td style="text-align:right; white-space:nowrap;">
|
||||
<button type="submit" form="rt-del-{{ loop.index }}"
|
||||
style="background:var(--card); color:var(--err); border-color:var(--err);">
|
||||
Sterge
|
||||
</button>
|
||||
{{ ui.act_btn('Sterge', 'trash', 'del', 'type="submit" form="rt-del-' ~ loop.index ~ '"') }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
@@ -308,9 +292,7 @@
|
||||
hx-confirm="Stergi acest format de coloane?">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token or '' }}">
|
||||
<input type="hidden" name="format_id" value="{{ f.id }}">
|
||||
<button type="submit" style="background:var(--card); color:var(--err); border-color:var(--err);">
|
||||
Sterge
|
||||
</button>
|
||||
{{ ui.act_btn('Sterge', 'trash', 'del', 'type="submit"') }}
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
{% import '_macros.html' as ui %}
|
||||
<div id="import-section">
|
||||
{# reincarcaPreview (emis de /editeaza si /confirma-review prin HX-Trigger): preview-ul
|
||||
se reincarca COMPLET (rand + contoare + colaps deja-trimise corecte) in loc de OOB swap
|
||||
pe <tr> (fragil in htmx 1.9). Evidentierea + toast-ul randului salvat: base.html. #}
|
||||
<div id="import-section"
|
||||
hx-get="/_import/{{ import_id }}/preview"
|
||||
hx-trigger="reincarcaPreview from:body"
|
||||
hx-target="#import-section"
|
||||
hx-swap="outerHTML">
|
||||
{% set pas = 3 %}{% include '_stepper.html' %}
|
||||
<div class="card">
|
||||
<div style="display:flex; align-items:center; gap:8px; flex-wrap:wrap; margin-bottom:12px;">
|
||||
@@ -123,10 +130,22 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Toggle randuri deja-trimise / duplicate: colapsate implicit (nu ocupa loc).
|
||||
Click -> comuta clasa .preview-arata-trimise pe tabel (CSS in base.html). -->
|
||||
{% set _n_trimise = summary.get('already_sent', 0) + summary.get('duplicate_in_file', 0) %}
|
||||
{% if _n_trimise %}
|
||||
<div style="margin-bottom:8px;">
|
||||
<button type="button" class="btn-secondary btn-sm" aria-expanded="false"
|
||||
onclick="var t=document.getElementById('preview-tabel'); var on=t.classList.toggle('preview-arata-trimise'); this.setAttribute('aria-expanded', on); this.querySelector('.tgl-tx').textContent = on ? 'Ascunde {{ _n_trimise }} deja trimise / duplicate' : 'Arata {{ _n_trimise }} deja trimise / duplicate';">
|
||||
<span class="tgl-tx">Arata {{ _n_trimise }} deja trimise / duplicate</span>
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Tabel preview in format identic cu tabelul Trimiteri (.tabel-trimiteri).
|
||||
US-007: 8 coloane (coloana de verificare eliminata).
|
||||
Randurile au FORM PROPRIU pentru editare (NU sunt in #confirm-form). -->
|
||||
<div class="tablewrap tabel-trimiteri">
|
||||
<div id="preview-tabel" class="tablewrap tabel-trimiteri">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -169,31 +188,19 @@
|
||||
prezentari la RAR (ireversibil).
|
||||
</div>
|
||||
|
||||
<div style="display:flex; gap:12px; align-items:center; flex-wrap:wrap;">
|
||||
<div>
|
||||
<label for="n-confirmat"
|
||||
style="font-size:13px; color:var(--muted); display:block; margin-bottom:2px;">
|
||||
Numar prezentari de confirmat
|
||||
</label>
|
||||
<input type="number" id="n-confirmat" name="n_confirmat"
|
||||
value="{{ summary.get('ok', 0) }}"
|
||||
min="0" required
|
||||
style="max-width:80px;"
|
||||
aria-describedby="n-hint">
|
||||
<span id="n-hint" class="muted" style="font-size:12px; margin-left:6px;">
|
||||
(<span id="n-hint-ok">{{ summary.get('ok', 0) }}</span> ok)
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="confirmed-by"
|
||||
style="font-size:13px; color:var(--muted); display:block; margin-bottom:2px;">
|
||||
Declarant (optional)
|
||||
</label>
|
||||
<input type="text" id="confirmed-by" name="confirmed_by"
|
||||
placeholder="email sau nume"
|
||||
style="max-width:200px;">
|
||||
</div>
|
||||
<div style="display:flex; gap:8px; align-items:center; flex-wrap:wrap;">
|
||||
<label for="n-confirmat"
|
||||
style="font-size:13px; color:var(--muted);">
|
||||
Confirma numarul
|
||||
</label>
|
||||
<input type="number" id="n-confirmat" name="n_confirmat"
|
||||
value="{{ summary.get('ok', 0) }}"
|
||||
min="0" required
|
||||
style="max-width:80px;"
|
||||
aria-describedby="n-hint">
|
||||
<span id="n-hint" class="muted" style="font-size:12px;">
|
||||
din <span id="n-hint-ok">{{ summary.get('ok', 0) }}</span> gata de trimis
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -281,5 +288,18 @@
|
||||
/* Filtru implicit "Toate" activ la incarcare */
|
||||
filterRows('all');
|
||||
updateN();
|
||||
|
||||
/* Evidentiere rand dupa reincarcarea preview-ului (window.__randSalvat setat de
|
||||
listener-ul 'randSalvat' din base.html): scroll + flash, ca userul sa vada CARE
|
||||
rand s-a schimbat si sa nu ramana cu impresia ca "nu s-a intamplat nimic". */
|
||||
if (window.__randSalvat) {
|
||||
var d = window.__randSalvat; window.__randSalvat = null;
|
||||
var r = document.getElementById('preview-row-' + d.rowIndex);
|
||||
if (r) {
|
||||
r.scrollIntoView({block:'center', behavior:'smooth'});
|
||||
void r.offsetWidth;
|
||||
r.classList.add('rand-actualizat');
|
||||
}
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
||||
@@ -18,11 +18,11 @@
|
||||
#}
|
||||
{%- set res = row.resolved -%}
|
||||
{%- set status = row.resolved_status -%}
|
||||
{%- set disp_fix_map = {} -%}
|
||||
{%- for e in row.errors -%}{%- if e is mapping and e.get('field') and e.get('fix') -%}{%- set _ = disp_fix_map.update({e.field: e.get('fix')}) -%}{%- endif -%}{%- endfor -%}
|
||||
{%- set _sent_dup = status in ('already_sent', 'duplicate_in_file') -%}
|
||||
<tr id="preview-row-{{ row.row_index }}" data-status="{{ status }}"
|
||||
{% if _sent_dup %}class="preview-sent-row"{% endif %}
|
||||
{% if oob_tr %}hx-swap-oob="outerHTML:#preview-row-{{ row.row_index }}"{% endif %}
|
||||
style="{% if status == 'needs_review' %}background:rgba(230,179,74,.04);{% elif status in ('already_sent', 'duplicate_in_file') %}opacity:.65;{% endif %}">
|
||||
style="{% if status == 'needs_review' %}background:rgba(230,179,74,.04);{% elif _sent_dup %}opacity:.6;{% endif %}">
|
||||
<td class="col-id muted" data-eticheta="#">{{ row.row_index + 1 }}</td>
|
||||
<td class="col-stare" data-eticheta="Stare">
|
||||
<span class="pill {{ row.stare_css }}">{{ row.stare_eticheta }}</span>
|
||||
@@ -32,9 +32,6 @@
|
||||
{% if row.prez.vin_scurt and row.prez.vin_scurt != '—' %}
|
||||
<div class="muted" style="font-size:12px; white-space:nowrap;">{{ row.prez.vin_scurt }}</div>
|
||||
{% endif %}
|
||||
{# Fix-uri de validare pe vehicul #}
|
||||
{% if disp_fix_map.get('vin') %}<span class="camp-fix">{{ disp_fix_map.get('vin') }}</span>{% endif %}
|
||||
{% if disp_fix_map.get('nr_inmatriculare') %}<span class="camp-fix">{{ disp_fix_map.get('nr_inmatriculare') }}</span>{% endif %}
|
||||
</td>
|
||||
<td class="col-operatie" data-eticheta="Operatie">
|
||||
<div>{{ row.prez.operatie }}</div>
|
||||
@@ -44,14 +41,8 @@
|
||||
<div class="muted cod-rar-sub">nemapat</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="col-data" data-eticheta="Data prestatie">
|
||||
{{ row.prez.data_prestatie }}
|
||||
{% if disp_fix_map.get('data_prestatie') %}<span class="camp-fix">{{ disp_fix_map.get('data_prestatie') }}</span>{% endif %}
|
||||
</td>
|
||||
<td class="col-km" data-eticheta="KM final">
|
||||
{{ row.prez.odometru }}
|
||||
{% if disp_fix_map.get('odometru_final') %}<span class="camp-fix">{{ disp_fix_map.get('odometru_final') }}</span>{% endif %}
|
||||
</td>
|
||||
<td class="col-data" data-eticheta="Data prestatie">{{ row.prez.data_prestatie }}</td>
|
||||
<td class="col-km" data-eticheta="KM final">{{ row.prez.odometru }}</td>
|
||||
<td class="col-note" data-eticheta="Note"
|
||||
style="font-size:12px; white-space:normal;">
|
||||
{% if status == 'already_sent' and row.get('already_sent_info') %}
|
||||
@@ -68,7 +59,7 @@
|
||||
<td class="col-actiuni" data-eticheta="Actiuni" style="text-align:center;">
|
||||
{% if status not in ('already_sent', 'duplicate_in_file') %}
|
||||
<button type="button" class="btn-editeaza"
|
||||
style="min-height:44px; padding:6px 14px; font-size:13px;
|
||||
style="min-height:36px; padding:6px 14px; font-size:13px;
|
||||
background:transparent; border-color:var(--line); color:var(--ink);"
|
||||
hx-get="/_import/{{ import_id }}/rand/{{ row.row_index }}/editare-modal"
|
||||
hx-target="#detaliu-modal-body" hx-swap="innerHTML"
|
||||
|
||||
@@ -1,22 +1,9 @@
|
||||
{#
|
||||
_stepper.html — Antet wizard import (PUR vizual, fara logica de rutare).
|
||||
|
||||
Parametru: `pas` (integer 1-4) — pasul curent.
|
||||
Utilizare in template-uri care mostenesc contextul Jinja2:
|
||||
{% set pas = 1 %}{% include '_stepper.html' %}
|
||||
sau cu `with`:
|
||||
{% with pas=2 %}{% include '_stepper.html' %}{% endwith %}
|
||||
|
||||
Cei 4 pasi ficsi:
|
||||
1. Incarca fisier
|
||||
2. Potriveste coloanele
|
||||
3. Verifica
|
||||
4. Confirma trimiterea
|
||||
|
||||
Stari vizuale:
|
||||
- index < pas → "facut" (bulina plina, text bifat)
|
||||
- index == pas → "activ" (evidentiat, aria-current="step")
|
||||
- index > pas → "viitor" (estompat)
|
||||
_stepper.html — Antet wizard import COMPACT (PUR vizual). PRD 5.13.
|
||||
Parametru: `pas` (integer 1-4). Clasele .stepper-* sunt definite in base.html.
|
||||
>=1024px: bara slim orizontala (.stepper-track). <1024px: forma colapsata
|
||||
"Pasul N din 4 - Titlu" + bara de progres (.stepper-collapsed).
|
||||
Utilizare: {% set pas = 1 %}{% include '_stepper.html' %}
|
||||
#}
|
||||
{%- set _pasi_import = [
|
||||
(1, "Incarca fisier", "Trage un fisier xlsx/csv aici sau foloseste butonul de alegere."),
|
||||
@@ -24,73 +11,26 @@
|
||||
(3, "Verifica", "Verifica randurile inainte sa le trimiti la RAR."),
|
||||
(4, "Confirma trimiterea", "Confirma numarul de prezentari — actiunea e ireversibila."),
|
||||
] -%}
|
||||
<nav class="stepper-import" aria-label="Pasii importului" style="
|
||||
display:flex;
|
||||
gap:0;
|
||||
align-items:stretch;
|
||||
margin-bottom:20px;
|
||||
border:1px solid var(--line);
|
||||
border-radius:8px;
|
||||
overflow:hidden;
|
||||
background:var(--card);
|
||||
">
|
||||
{% for nr, titlu, ajutor in _pasi_import %}
|
||||
{%- if nr < pas %}
|
||||
{%- set cls = "facut" -%}
|
||||
{%- set aria = "" -%}
|
||||
{%- elif nr == pas %}
|
||||
{%- set cls = "activ" -%}
|
||||
{%- set aria = ' aria-current="step"' -%}
|
||||
{%- else %}
|
||||
{%- set cls = "viitor" -%}
|
||||
{%- set aria = "" -%}
|
||||
{%- endif %}
|
||||
<div class="stepper-pas stepper-pas--{{ cls }}"{{ aria | safe }}
|
||||
style="
|
||||
flex:1;
|
||||
padding:10px 14px;
|
||||
border-right:{% if not loop.last %}1px solid var(--line){% else %}none{% endif %};
|
||||
{% if cls == 'activ' %}
|
||||
background:rgba(91,141,239,.10);
|
||||
{% elif cls == 'facut' %}
|
||||
opacity:1;
|
||||
{% else %}
|
||||
opacity:.4;
|
||||
{% endif %}
|
||||
">
|
||||
<div style="display:flex; align-items:center; gap:6px; margin-bottom:2px;">
|
||||
<span class="stepper-nr" style="
|
||||
display:inline-flex;
|
||||
align-items:center;
|
||||
justify-content:center;
|
||||
width:20px;
|
||||
height:20px;
|
||||
border-radius:50%;
|
||||
font-size:11px;
|
||||
font-weight:700;
|
||||
flex-shrink:0;
|
||||
{% if cls == 'activ' %}
|
||||
background:var(--accent);
|
||||
color:#fff;
|
||||
{% elif cls == 'facut' %}
|
||||
background:var(--ok);
|
||||
color:#fff;
|
||||
{% else %}
|
||||
background:var(--line);
|
||||
color:var(--muted);
|
||||
{% endif %}
|
||||
">
|
||||
{% if cls == 'facut' %}✓{% else %}{{ nr }}{% endif %}
|
||||
</span>
|
||||
<span style="
|
||||
font-size:13px;
|
||||
font-weight:{% if cls == 'activ' %}600{% else %}400{% endif %};
|
||||
color:{% if cls == 'activ' %}var(--ink){% elif cls == 'facut' %}var(--ink){% else %}var(--muted){% endif %};
|
||||
">{{ titlu }}</span>
|
||||
{%- set _activ = _pasi_import[pas - 1] -%}
|
||||
<div class="stepper">
|
||||
{# Desktop (>=1024px): bara slim orizontala. #}
|
||||
<nav class="stepper-track" aria-label="Pasii importului">
|
||||
{% for nr, titlu, ajutor in _pasi_import %}
|
||||
{%- if nr < pas %}{% set cls = "is-done" %}{% set aria = "" %}
|
||||
{%- elif nr == pas %}{% set cls = "is-active" %}{% set aria = ' aria-current="step"' %}
|
||||
{%- else %}{% set cls = "" %}{% set aria = "" %}{% endif %}
|
||||
<div class="stepper-step {{ cls }}"{{ aria | safe }}>
|
||||
<span class="stepper-nr">{% if nr < pas %}✓{% else %}{{ nr }}{% endif %}</span>
|
||||
<span class="stepper-tx">{{ titlu }}</span>
|
||||
</div>
|
||||
{% if cls == 'activ' %}
|
||||
<p class="muted" style="margin:0; font-size:12px; padding-left:26px;">{{ ajutor }}</p>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</nav>
|
||||
{# Tableta/mobil (<1024px): colapsat "Pasul N din 4 - Titlu" + progres. #}
|
||||
<div class="stepper-collapsed">
|
||||
<div class="stepper-current">Pasul {{ pas }} din 4 <span class="muted">· {{ _activ[1] }}</span></div>
|
||||
<div class="stepper-progress" role="progressbar" aria-valuenow="{{ pas }}" aria-valuemin="1" aria-valuemax="4"
|
||||
aria-label="Pasul {{ pas }} din 4"><span style="width:{{ (pas / 4 * 100) | round | int }}%;"></span></div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</nav>
|
||||
{# Ajutorul pasului activ — o singura linie, sub bara (valabil pe ambele forme). #}
|
||||
<p class="stepper-help">{{ _activ[2] }}</p>
|
||||
</div>
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
{# Eticheta umana scurta sub pill — text mic, `s-error` pe error/needs_*
|
||||
(singurele stari pe care `eticheta_problema` e ne-goala).
|
||||
Stare transmisa prin TEXT, nu doar culoare. Codul brut ramane in modal. #}
|
||||
{% if r.eticheta_problema %}
|
||||
{% if r.eticheta_problema and r.eticheta_problema != r.stare_scurt and r.eticheta_problema != r.stare_text %}
|
||||
<div class="eticheta-problema s-error">{{ r.eticheta_problema }}</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
@@ -177,7 +177,7 @@
|
||||
.banner.warn { border-left-color:var(--warn); background:color-mix(in srgb, var(--warn) 12%, var(--card)); }
|
||||
/* Bara confirmare sticky */
|
||||
.sticky-bar { position:sticky; bottom:0; background:var(--card); border-top:1px solid var(--line);
|
||||
padding:12px 16px; display:flex; align-items:flex-start; gap:16px;
|
||||
padding:10px 14px; display:flex; align-items:flex-start; gap:12px;
|
||||
flex-wrap:wrap; z-index:10; }
|
||||
/* Indicator HTMX — ascuns pana la request */
|
||||
.htmx-indicator { display:none; }
|
||||
@@ -199,6 +199,83 @@
|
||||
select { max-width:340px; }
|
||||
button { background:var(--accent); border-color:var(--accent); color:#fff; cursor:pointer; }
|
||||
button:hover { filter:brightness(1.08); }
|
||||
/* Sistem butoane unificat (design.md §5.1). Primarul = `button`/`.btn` (deja stilat). */
|
||||
.btn-secondary { background:transparent; color:var(--ink); border:1px solid var(--line); border-radius:6px;
|
||||
padding:8px 14px; font:inherit; font-weight:500; cursor:pointer; min-height:36px;
|
||||
display:inline-flex; align-items:center; justify-content:center; gap:6px; }
|
||||
.btn-secondary:hover { background:var(--line); filter:none; }
|
||||
.btn-ghost { background:transparent; color:var(--accent); border:1px solid transparent; border-radius:6px;
|
||||
padding:8px 14px; font:inherit; font-weight:500; cursor:pointer; min-height:36px;
|
||||
display:inline-flex; align-items:center; justify-content:center; gap:6px; }
|
||||
.btn-ghost:hover { background:var(--line); filter:none; }
|
||||
.btn-danger { background:transparent; color:var(--err); border:1px solid var(--err); border-radius:6px;
|
||||
padding:8px 14px; font:inherit; font-weight:500; cursor:pointer; min-height:36px;
|
||||
display:inline-flex; align-items:center; justify-content:center; gap:6px; }
|
||||
.btn-danger:hover, .btn-danger:focus-visible { background:var(--err); color:#fff; filter:none; }
|
||||
.btn-sm { padding:5px 10px; min-height:32px; font-size:13px; }
|
||||
button:focus-visible, .btn-secondary:focus-visible, .btn-ghost:focus-visible, .btn-danger:focus-visible {
|
||||
outline:2px solid var(--accent); outline-offset:2px; }
|
||||
/* Actiuni de rand (design.md §5.1): desktop = text, mobil = iconita patrata 44px.
|
||||
act_btn randeaza si .act-tx (text) si .act-ic (svg); CSS ascunde unul per breakpoint. */
|
||||
.act { display:inline-flex; align-items:center; justify-content:center; gap:6px; font:inherit; font-weight:500;
|
||||
border-radius:7px; padding:6px 12px; min-height:36px; cursor:pointer; background:transparent;
|
||||
border:1px solid var(--line); color:var(--ink); }
|
||||
.act:hover { background:var(--line); filter:none; }
|
||||
.act:focus-visible { outline:2px solid var(--accent); outline-offset:2px; }
|
||||
.act .act-ic { width:18px; height:18px; display:none; }
|
||||
.act .act-tx { display:inline; }
|
||||
.act-save.dirty { background:var(--accent); color:#fff; border-color:var(--accent); }
|
||||
.act-save.dirty:hover { filter:brightness(.92); }
|
||||
/* Variant primar mereu-accent (ex. Salveaza in modalul de editare). */
|
||||
.act-primary { background:var(--accent); color:#fff; border-color:var(--accent); }
|
||||
.act-primary:hover { filter:brightness(.92); background:var(--accent); }
|
||||
.act-del { color:var(--err); border-color:var(--err); }
|
||||
.act-del:hover, .act-del:focus-visible { background:var(--err); color:#fff; }
|
||||
.act-group { display:inline-flex; gap:8px; align-items:center; }
|
||||
.btn-editeaza { white-space:nowrap; }
|
||||
/* Toast global (feedback tranzitoriu post-actiune). */
|
||||
#toast { position:fixed; left:50%; bottom:24px; transform:translateX(-50%) translateY(8px);
|
||||
z-index:1300; max-width:90vw; padding:11px 18px; border-radius:10px;
|
||||
background:var(--ink); color:var(--card); font-size:14px; font-weight:500;
|
||||
box-shadow:0 8px 28px rgba(0,0,0,.28); display:flex; align-items:center; gap:9px;
|
||||
opacity:0; pointer-events:none; transition:opacity .2s, transform .2s; }
|
||||
#toast[hidden] { display:none; }
|
||||
#toast.show { opacity:1; transform:translateX(-50%) translateY(0); }
|
||||
#toast::before { content:""; width:9px; height:9px; border-radius:50%; background:var(--ok); flex:0 0 auto; }
|
||||
#toast.t-err::before, #toast.t-s-error::before, #toast.t-s-needs_data::before,
|
||||
#toast.t-s-needs_mapping::before { background:var(--err); }
|
||||
#toast.t-warn::before, #toast.t-s-needs_review::before { background:var(--warn); }
|
||||
/* Rand de preview tocmai actualizat: flash scurt ca userul sa-l localizeze. */
|
||||
@keyframes rand-flash { 0% { background:color-mix(in srgb, var(--accent) 26%, var(--card)); }
|
||||
100% { background:transparent; } }
|
||||
.tabel-trimiteri tr.rand-actualizat { animation:rand-flash 1.6s ease-out; }
|
||||
/* Randuri deja-trimise / duplicate in preview: colapsate implicit (nu ocupa loc).
|
||||
Reafisate cand userul apasa toggle-ul -> .preview-arata-trimise pe container.
|
||||
!important fiindca regulile compacte mobil/tableta seteaza `tr{display:flex}`. */
|
||||
.tabel-trimiteri tr.preview-sent-row { display:none !important; }
|
||||
.tabel-trimiteri.preview-arata-trimise tr.preview-sent-row { display:table-row !important; }
|
||||
/* Stepper import compact (design.md §5.4). >=1024px: bara slim. <1024px: "Pasul N din 4" + progres. */
|
||||
.stepper { margin-bottom:16px; }
|
||||
.stepper-track { display:flex; align-items:stretch; border:1px solid var(--line); border-radius:8px;
|
||||
overflow:hidden; background:var(--card); }
|
||||
.stepper-step { flex:1; display:flex; align-items:center; gap:8px; padding:10px 14px; min-height:44px;
|
||||
border-right:1px solid var(--line); }
|
||||
.stepper-step:last-child { border-right:none; }
|
||||
.stepper-nr { display:inline-flex; align-items:center; justify-content:center; width:20px; height:20px;
|
||||
border-radius:50%; font-size:11px; font-weight:700; flex-shrink:0;
|
||||
background:var(--line); color:var(--muted); }
|
||||
.stepper-tx { font-size:13px; color:var(--muted); white-space:nowrap; }
|
||||
.stepper-step.is-active { background:color-mix(in srgb, var(--accent) 10%, transparent); }
|
||||
.stepper-step.is-active .stepper-nr { background:var(--accent); color:#fff; }
|
||||
.stepper-step.is-active .stepper-tx { color:var(--ink); font-weight:600; }
|
||||
.stepper-step.is-done .stepper-nr { background:var(--ok); color:#fff; }
|
||||
.stepper-step.is-done .stepper-tx { color:var(--ink); }
|
||||
.stepper-help { margin:6px 2px 0; font-size:12px; color:var(--muted); }
|
||||
.stepper-collapsed { display:none; }
|
||||
.stepper-current { font-size:14px; font-weight:600; margin-bottom:6px; }
|
||||
.stepper-current .muted { font-weight:400; }
|
||||
.stepper-progress { height:5px; border-radius:99px; background:var(--line); overflow:hidden; }
|
||||
.stepper-progress > span { display:block; height:100%; background:var(--accent); border-radius:99px; }
|
||||
.chk { font-size:13px; color:var(--muted); display:flex; align-items:center; gap:6px; }
|
||||
/* Tab-bar */
|
||||
.tab-bar { display:flex; gap:2px; overflow-x:auto; -webkit-overflow-scrolling:touch;
|
||||
@@ -341,9 +418,12 @@
|
||||
.tabel-trimiteri tr.trimitere-row:hover { background:color-mix(in srgb, var(--accent) 6%, transparent); }
|
||||
.tabel-trimiteri tr.trimitere-row:focus,
|
||||
.tabel-trimiteri tr.trimitere-row:focus-visible { outline:2px solid var(--accent); outline-offset:-2px; }
|
||||
/* 768-1024px: ascunde Actualizat (e in detaliu) -> 7 coloane, fara scroll */
|
||||
/* col-actualizat ca linie meta mica in carduri (decizie 5.13 #8). */
|
||||
.tabel-trimiteri td.col-actualizat { font-size:12px; }
|
||||
/* Stepper: sub 1024px ascunde track-ul slim, arata forma colapsata (decizie 5.13 #11). */
|
||||
@media (max-width:1024px) {
|
||||
.tabel-trimiteri .col-actualizat { display:none; }
|
||||
.stepper-track { display:none; }
|
||||
.stepper-collapsed { display:block; }
|
||||
}
|
||||
/* Tableta (768–1024px): header compact fara suprapuneri.
|
||||
Grila 3-coloane se pastreaza; logo si titlu mai mici; versiunea ascunsa
|
||||
@@ -358,6 +438,43 @@
|
||||
ascunsa pentru a elibera spatiu in celula dreapta. */
|
||||
.header-right > .muted { display:none; }
|
||||
}
|
||||
/* Tableta 768-1024px: listele actionabile raman O COLOANA, cardificate (nu tabel storcit,
|
||||
nu 2/rand). Decizie 5.13 (premisa user). Tabelele dense read-only raman .tablewrap. */
|
||||
@media (min-width:768px) and (max-width:1024px) {
|
||||
.tabel-trimiteri table, .tabel-card table { table-layout:auto; }
|
||||
.tabel-trimiteri thead, .tabel-card thead { display:none; }
|
||||
.tabel-trimiteri table, .tabel-trimiteri tbody, .tabel-trimiteri tr, .tabel-trimiteri td,
|
||||
.tabel-card table, .tabel-card tbody, .tabel-card tr, .tabel-card td { display:block; width:auto; }
|
||||
.tabel-trimiteri tr, .tabel-card tr { border:1px solid var(--line); border-radius:8px;
|
||||
padding:10px 12px; margin-bottom:10px; }
|
||||
.tabel-trimiteri td, .tabel-card td { border-bottom:none; padding:3px 0; }
|
||||
.tabel-trimiteri td::before, .tabel-card td::before { content:attr(data-eticheta); display:block;
|
||||
color:var(--muted); font-size:12px; margin-bottom:2px; }
|
||||
.tabel-trimiteri td.col-chk, .tabel-trimiteri td.col-id { display:none; }
|
||||
.tabel-trimiteri td[data-eticheta=""]::before, .tabel-card td[data-eticheta=""]::before,
|
||||
.tabel-card td:not([data-eticheta])::before { display:none; }
|
||||
.tabel-card td select, .tabel-card td input[type=text], .tabel-card td input[type=search] {
|
||||
width:100%; max-width:none; }
|
||||
/* Card compact si pe tableta (acelasi layout ca pe mobil) pentru `.tabel-trimiteri`. */
|
||||
.tabel-trimiteri tr { display:flex; flex-wrap:wrap; align-items:baseline; gap:1px 8px; }
|
||||
.tabel-trimiteri td { padding:0; }
|
||||
/* Regula desktop `tr.trimitere-row > td { padding:11px }` e mai specifica -> o anulam
|
||||
in cardul compact, altfel randurile de Trimiteri raman inalte/aerisite. */
|
||||
.tabel-trimiteri tr.trimitere-row > td { padding-top:0; padding-bottom:0; }
|
||||
.tabel-trimiteri td::before { display:none; }
|
||||
.tabel-trimiteri td.col-vehicul { order:1; flex:1 1 55%; min-width:0; font-weight:600; font-size:15px; line-height:1.25; }
|
||||
.tabel-trimiteri td.col-vehicul .muted { font-weight:400; }
|
||||
.tabel-trimiteri td.col-stare { order:2; flex:0 0 auto; margin-left:auto; align-self:flex-start; }
|
||||
.tabel-trimiteri td.col-operatie { order:3; flex:1 1 100%; font-size:13px; line-height:1.3; margin-top:1px; }
|
||||
.tabel-trimiteri td.col-data, .tabel-trimiteri td.col-km, .tabel-trimiteri td.col-rar { font-size:12px; color:var(--muted); }
|
||||
.tabel-trimiteri td.col-data { order:4; } .tabel-trimiteri td.col-km { order:5; } .tabel-trimiteri td.col-rar { order:6; }
|
||||
.tabel-trimiteri td.col-km::before { content:"· "; display:inline; color:var(--muted); }
|
||||
.tabel-trimiteri td.col-actualizat { order:7; flex:1 1 100%; font-size:12px; color:var(--muted); }
|
||||
.tabel-trimiteri td.col-note { order:8; flex:1 1 100%; font-size:12px; color:var(--accent); line-height:1.3; margin-top:1px; }
|
||||
.tabel-trimiteri td.col-actiuni { order:9; flex:0 0 auto; margin-left:auto; margin-top:4px; text-align:right; }
|
||||
.tabel-trimiteri td.col-actiuni button, .tabel-trimiteri td.col-actiuni .act { width:auto; min-height:32px; padding:5px 14px; }
|
||||
.tabel-trimiteri.preview-arata-trimise tr.preview-sent-row { display:flex !important; }
|
||||
}
|
||||
/* === Preview import: coloane extra fata de tabelul Trimiteri.
|
||||
SCOPAT prin .tabel-trimiteri (clasa partajata). Regiune separata —
|
||||
nu atinge coloanele existente (col-chk/id/stare/data/rar/actualizat).
|
||||
@@ -367,7 +484,12 @@
|
||||
Restul (~680px la 1280px) revine lui col-vehicul + col-operatie (fluid). === */
|
||||
.tabel-trimiteri .col-km { width:76px; }
|
||||
.tabel-trimiteri .col-note { width:176px; }
|
||||
.tabel-trimiteri .col-actiuni { width:92px; }
|
||||
/* Nota preview = culoarea camp-fix (accent), ca sa atraga atentia (dogfood 5.13):
|
||||
campul Note e self-explanatory, asa ca hint-urile per-camp au fost scoase. */
|
||||
.tabel-trimiteri td.col-note { color:var(--accent); }
|
||||
/* Pill-ul de stare nu se rupe pe doua randuri in cardul compact. */
|
||||
.tabel-trimiteri td.col-stare .pill { white-space:nowrap; }
|
||||
.tabel-trimiteri .col-actiuni { width:104px; }
|
||||
/* Randul de editare inline iese din grila table-layout:fixed (display:block),
|
||||
astfel formularul nu e constrans de latimile coloanelor individuale.
|
||||
Salveaza/Anuleaza sunt mereu vizibile (overflow:visible, nu clip). */
|
||||
@@ -401,16 +523,51 @@
|
||||
Breakpoint unic 767px (vezi conventia de sus). Cuprinde: card per rand pe tabelul
|
||||
de trimiteri, modal full-screen, header/nav colapsat cu tinte touch
|
||||
>=44px. Desktop (>=1024px) ramane neschimbat — regulile de baza nu se modifica. */
|
||||
/* SENTINEL-TESTE-MOBIL: blocul mobil principal incepe mai jos; testele ancoreaza pe acest marker si feliaza pana la sfarsitul stilului. NU muta/sterge. */
|
||||
@media (max-width:767px) {
|
||||
/* Tabel trimiteri: card per rand (eticheta:valoare stivuit) -> fara scroll orizontal */
|
||||
.tabel-trimiteri table { table-layout:auto; }
|
||||
.tabel-trimiteri thead { display:none; }
|
||||
.tabel-trimiteri table, .tabel-trimiteri tbody, .tabel-trimiteri tr, .tabel-trimiteri td { display:block; width:auto; }
|
||||
.tabel-trimiteri tr { border:1px solid var(--line); border-radius:8px; padding:8px 12px; margin-bottom:10px; }
|
||||
.tabel-trimiteri td { border-bottom:none; padding:4px 0; display:flex; gap:10px; align-items:baseline; }
|
||||
.tabel-trimiteri td::before { content:attr(data-eticheta); color:var(--muted); font-size:12px;
|
||||
flex:0 0 auto; min-width:120px; }
|
||||
.tabel-trimiteri td.col-chk { display:none; }
|
||||
.tabel-trimiteri td { border-bottom:none; padding:3px 0; display:block; }
|
||||
.tabel-trimiteri td::before { content:attr(data-eticheta); display:block; color:var(--muted);
|
||||
font-size:12px; margin-bottom:2px; }
|
||||
.tabel-trimiteri td.col-chk, .tabel-trimiteri td.col-id { display:none; }
|
||||
|
||||
/* === Card COMPACT (PRD 5.13, corectie dogfood) ===
|
||||
Inlocuieste stiva generica eticheta+valoare (prea inalta) cu un card
|
||||
scanabil la prima vedere: vehicul = titlu, stare = pill dreapta-sus,
|
||||
operatie+cod pe rand, meta (data/km/rar) muted mic, nota mica. Fara
|
||||
etichete-zgomot. Override DUPA regulile de baza (cascada: ultimul castiga). */
|
||||
.tabel-trimiteri tr { display:flex; flex-wrap:wrap; align-items:baseline;
|
||||
gap:1px 8px; padding:9px 12px; }
|
||||
.tabel-trimiteri td { display:block; padding:0; }
|
||||
.tabel-trimiteri tr.trimitere-row > td { padding-top:0; padding-bottom:0; }
|
||||
.tabel-trimiteri td::before { display:none; } /* compact: fara etichete */
|
||||
.tabel-trimiteri td.col-vehicul { order:1; flex:1 1 55%; min-width:0;
|
||||
font-weight:600; font-size:15px; line-height:1.25; }
|
||||
.tabel-trimiteri td.col-vehicul .muted { font-weight:400; }
|
||||
.tabel-trimiteri td.col-stare { order:2; flex:0 0 auto; margin-left:auto;
|
||||
align-self:flex-start; }
|
||||
.tabel-trimiteri td.col-operatie { order:3; flex:1 1 100%; font-size:13px;
|
||||
line-height:1.3; margin-top:1px; }
|
||||
.tabel-trimiteri td.col-data,
|
||||
.tabel-trimiteri td.col-km,
|
||||
.tabel-trimiteri td.col-rar { font-size:12px; color:var(--muted); }
|
||||
.tabel-trimiteri td.col-data { order:4; }
|
||||
.tabel-trimiteri td.col-km { order:5; }
|
||||
.tabel-trimiteri td.col-km::before { content:"· "; display:inline; color:var(--muted); }
|
||||
.tabel-trimiteri td.col-rar { order:6; }
|
||||
.tabel-trimiteri td.col-actualizat { order:7; flex:1 1 100%; font-size:12px;
|
||||
color:var(--muted); }
|
||||
.tabel-trimiteri td.col-note { order:8; flex:1 1 100%; font-size:12px;
|
||||
color:var(--accent); line-height:1.3; margin-top:1px; }
|
||||
.tabel-trimiteri td.col-actiuni { order:9; flex:0 0 auto; margin-left:auto;
|
||||
margin-top:4px; text-align:right; }
|
||||
.tabel-trimiteri td.col-actiuni button,
|
||||
.tabel-trimiteri td.col-actiuni .act { width:auto; min-height:32px; padding:5px 14px; }
|
||||
.tabel-trimiteri.preview-arata-trimise tr.preview-sent-row { display:flex !important; }
|
||||
|
||||
/* Modal full-screen: ocupa tot ecranul, fara backdrop lateral (overlay fara
|
||||
padding, dialog la latime/inaltime pline, fara colturi/umbra). Scroll intern
|
||||
@@ -483,16 +640,40 @@
|
||||
#import-section #upload-btn { width:100%; min-height:44px; }
|
||||
/* Bara de status: contoarele/randurile raman aliniate la stanga, fara scroll orizontal. */
|
||||
#status-bar > div { gap:10px; }
|
||||
/* Bara de filtre trimiteri: o coloana, fiecare control full-width, buton >=44px.
|
||||
!important suprascrie latimile inline (ex. max-width:180px pe vehicul) DOAR pe mobil. */
|
||||
#filtre-trimiteri { flex-direction:column; align-items:stretch; }
|
||||
/* Bara de filtre trimiteri ADAPTATA pentru mobil (nu doar stivuita):
|
||||
- cautarea vehicul = rand propriu prioritar (input + buton pe acelasi rand);
|
||||
- grupurile de pill-uri (data + stare) = scroll orizontal, compacte (nu 8 butoane
|
||||
full-width unul sub altul). !important suprascrie latimile inline doar pe mobil. */
|
||||
#filtre-trimiteri { flex-direction:column; align-items:stretch; gap:8px; }
|
||||
#filtre-trimiteri > div { width:100%; }
|
||||
#filtre-trimiteri select, #filtre-trimiteri input[type=text],
|
||||
#filtre-trimiteri input[type=date] { width:100% !important; max-width:none !important; }
|
||||
#filtre-trimiteri button { width:100%; min-height:44px; }
|
||||
/* Cautarea vehicul: input creste, butonul Filtreaza compact langa el. */
|
||||
#filtre-trimiteri input[type=text] { flex:1 1 auto; width:auto !important; max-width:none !important; min-height:44px; }
|
||||
#filtre-trimiteri input[type=date] { width:100% !important; max-width:none !important; min-height:44px; }
|
||||
#filtre-trimiteri button[type=submit] { flex:0 0 auto; width:auto; min-height:44px; }
|
||||
/* Grupurile de pill-uri: o singura banda scrolabila orizontal, compacta. */
|
||||
#filtre-trimiteri .pills-categorii { margin-left:0 !important; flex-wrap:nowrap;
|
||||
overflow-x:auto; -webkit-overflow-scrolling:touch; padding-bottom:2px; }
|
||||
#filtre-trimiteri .pill-cat { flex:0 0 auto; }
|
||||
|
||||
/* Operatii de mapat (preview import): randul de mapare stiva pe o coloana,
|
||||
select-ul full-width (altfel max-width:340px global il scoate din viewport). */
|
||||
.maprow { gap:6px 12px; padding:10px 0; }
|
||||
.maprow .mapcol { flex:1 1 100%; min-width:0; }
|
||||
.maprow select { width:100% !important; max-width:none !important; }
|
||||
.maprow button { width:100%; min-height:44px; }
|
||||
|
||||
/* Card de autentificare (login/signup): centrat si nu depaseste viewport-ul pe mobil. */
|
||||
.auth-card { max-width:100%; margin:24px auto; }
|
||||
/* Versiunea ascunsa pe mobil (la fel ca pe tableta). */
|
||||
.header-right > .muted { display:none; }
|
||||
/* Actiuni .act pe mobil: iconita patrata 44px, textul ascuns. */
|
||||
.act { min-width:44px; min-height:44px; width:44px; padding:0; }
|
||||
.act .act-tx { display:none; }
|
||||
.act .act-ic { display:inline-block; }
|
||||
.act-group { gap:10px; }
|
||||
/* Bara confirmare compacta pe mobil. */
|
||||
.sticky-bar { padding:10px 12px; gap:10px; }
|
||||
.sticky-bar button { width:100%; min-height:44px; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
@@ -551,6 +732,9 @@
|
||||
{# aria-live pentru anuntarea schimbarilor de tema (accesibilitate) #}
|
||||
<span id="tema-live" role="status" aria-live="polite"
|
||||
style="position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;"></span>
|
||||
{# Toast global: feedback tranzitoriu (ex. dupa salvarea unui rand de import).
|
||||
aria-live=polite -> citit de screen-reader. window.arataToast(text, stareCss). #}
|
||||
<div id="toast" role="status" aria-live="polite" hidden></div>
|
||||
<main>{% block content %}{% endblock %}</main>
|
||||
{# Modal detaliu trimitere: container global, SIBLING al <main> (nu descendent),
|
||||
ca `inert`+`aria-hidden` pe <main> sa nu-l prinda si pe el. Corpul
|
||||
@@ -696,6 +880,39 @@
|
||||
if (saveBtn) saveBtn.classList.add('dirty');
|
||||
});
|
||||
</script>
|
||||
<script>
|
||||
// Toast global: feedback tranzitoriu vizibil + accesibil (aria-live).
|
||||
// window.arataToast(text, stareCss) — stareCss (ex. "s-error"/"s-needs_review")
|
||||
// coloreaza punctul indicator (rosu/galben/verde). Auto-dispare dupa ~3.2s.
|
||||
(function() {
|
||||
var t = document.getElementById('toast');
|
||||
var timer = null;
|
||||
window.arataToast = function(text, stareCss) {
|
||||
if (!t) return;
|
||||
t.className = '';
|
||||
if (stareCss) t.classList.add('t-' + stareCss);
|
||||
t.textContent = text;
|
||||
t.hidden = false;
|
||||
void t.offsetWidth; // reflow -> tranzitia porneste
|
||||
t.classList.add('show');
|
||||
if (timer) clearTimeout(timer);
|
||||
timer = setTimeout(function() {
|
||||
t.classList.remove('show');
|
||||
setTimeout(function() { t.hidden = true; }, 220);
|
||||
}, 3200);
|
||||
};
|
||||
})();
|
||||
|
||||
// Feedback dupa salvarea/confirmarea unui rand de import (HX-Trigger 'randSalvat').
|
||||
// Toast imediat (ce rand + ce stare are acum); evidentierea randului se aplica dupa
|
||||
// ce preview-ul se reincarca (reincarcaPreview), de scriptul din _preview_import.
|
||||
document.body.addEventListener('randSalvat', function(e) {
|
||||
var d = e.detail || {};
|
||||
if (window.arataToast)
|
||||
window.arataToast('Randul ' + d.nr + ' actualizat · ' + (d.stare || ''), d.stareCss || '');
|
||||
window.__randSalvat = d;
|
||||
});
|
||||
</script>
|
||||
<script>
|
||||
// Cautare + paginare client-side pentru tabele mari (data-dt="<page_size>"). Filtreaza si
|
||||
// pagineaza DOM-ul deja randat (fara cereri server) — potrivit pentru maparile care pot creste
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -102,7 +102,7 @@ Un singur prag conceptual mobil la **768px**; un prag de densitate la **1024px**
|
||||
| Interval | Numit | Regula |
|
||||
|----------|-------|--------|
|
||||
| `>= 1024px` | desktop | layout complet; aplica si compactarile globale (wizard) |
|
||||
| `768–1024px` | tableta | **card-uri** pentru tabelele actionabile (preview, mapari), 2 pe rand; tabelele dense read-only raman cu scroll contained |
|
||||
| `768–1024px` | tableta | **card-uri** pentru tabelele actionabile (Trimiteri, Preview, Mapari), UN card pe rand (nu 2/rand); tabelele dense read-only raman cu scroll contained |
|
||||
| `< 768px` | mobil | un card pe rand, o coloana, tinte touch 44px |
|
||||
|
||||
CSS custom properties NU functioneaza in `@media`; pragul se scrie literal
|
||||
@@ -167,8 +167,9 @@ flex" — sparge valorile pe verticala. In schimb:
|
||||
- **Card semantic**: linie titlu (identificator + stare), linii secundare mici. Preferat
|
||||
pentru liste lungi (Trimiteri, Preview).
|
||||
|
||||
Sub 768px: un card pe rand. 768–1024px: grid 2 carduri pe rand
|
||||
(`grid-template-columns:repeat(2,1fr); gap:12px`).
|
||||
Listele actionabile (Trimiteri, Preview, Mapari) raman **O COLOANA (un card pe rand)** pe
|
||||
TOT intervalul sub 1024px — nu se foloseste grila 2/rand. Decizie confirmata cu userul
|
||||
(gate 2026-06-27): simplitate si consecventa primeaza fata de densitate pe tableta.
|
||||
|
||||
Tabelele **dense read-only** (Jurnal, Nomenclator, Admin) raman tabel cu scroll orizontal
|
||||
**contained in card** (`.tablewrap { overflow-x:auto }`), nu se cardifica.
|
||||
|
||||
@@ -316,9 +316,10 @@ def test_confirmare_in_modal_seteaza_reviewed_si_devine_ok(client):
|
||||
Verifica:
|
||||
- Raspuns 200
|
||||
- reviewed=1 in DB
|
||||
- Raspuns contine OOB cu pill 'Gata de trimis' (starea ok)
|
||||
- Header HX-Trigger-After-Settle: inchideModal
|
||||
- HX-Trigger: randSalvat cu noua stare 'Gata de trimis' (pentru toast)
|
||||
- HX-Trigger: reincarcaPreview + HX-Trigger-After-Settle: inchideModal
|
||||
"""
|
||||
import json as _json
|
||||
_seed_op1()
|
||||
iid = _upload_and_preview_needs_review(client)
|
||||
|
||||
@@ -334,14 +335,11 @@ def test_confirmare_in_modal_seteaza_reviewed_si_devine_ok(client):
|
||||
assert _get_reviewed(iid, 0) == 1, \
|
||||
"reviewed trebuie sa fie 1 in DB dupa confirmare"
|
||||
|
||||
# Raspuns contine OOB cu randul actualizat
|
||||
html = r.text
|
||||
assert 'id="preview-row-0"' in html or "preview-row-0" in html, \
|
||||
"Raspunsul trebuie sa contina randul actualizat (OOB)"
|
||||
|
||||
# Starea a devenit ok
|
||||
assert "Gata de trimis" in html or "s-ok" in html, \
|
||||
"Dupa confirmare, randul trebuie sa fie ok (pill 'Gata de trimis')"
|
||||
# Contractul nou: reload preview + randSalvat cu noua stare (nu OOB pe <tr>).
|
||||
trig = _json.loads(r.headers.get("HX-Trigger", "{}"))
|
||||
assert trig.get("reincarcaPreview") is True, "confirma-review trebuie sa ceara reincarcaPreview"
|
||||
assert trig.get("randSalvat", {}).get("stare") == "Gata de trimis", \
|
||||
"Dupa confirmare, randSalvat.stare trebuie sa fie 'Gata de trimis' (pentru toast)"
|
||||
|
||||
# Modal se inchide
|
||||
trigger = r.headers.get("HX-Trigger-After-Settle", "")
|
||||
@@ -585,16 +583,17 @@ def test_confirma_review_form_nu_foloseste_hx_swap_none():
|
||||
)
|
||||
|
||||
|
||||
def test_confirma_review_raspuns_contine_script_updateN(client):
|
||||
"""Bug B1 (functional): raspunsul POST confirma-review contine scriptul
|
||||
updateN in payload-ul principal (nu doar OOB), astfel ca htmx il va executa
|
||||
cand face swap in #detaliu-modal-body.
|
||||
def test_confirma_review_cere_reincarcarea_preview(client):
|
||||
"""Contractul nou (dogfood 5.13): confirma-review NU mai depinde de scriptul updateN
|
||||
din payload (care, cu OOB pe <tr> rupt, lasa randul stale). Acum cere reincarcaPreview,
|
||||
iar preview-ul reincarcat re-randeaza contorul si butonul de confirmare cu n_confirmat
|
||||
corect server-side — deci problema B1 (n_confirmat stale -> 422) dispare structural.
|
||||
|
||||
Verifica:
|
||||
- Raspuns 200
|
||||
- Raspunsul contine 'window.updateN' (scriptul de recalcul contor)
|
||||
- Raspunsul contine 'updateN' inainte de ultimul OOB-element (@script tag nu e OOB)
|
||||
- HX-Trigger contine reincarcaPreview (reincarca contorul/confirmarea, fresh)
|
||||
"""
|
||||
import json as _json
|
||||
_seed_op1()
|
||||
iid = _upload_and_preview_needs_review(client)
|
||||
|
||||
@@ -602,17 +601,8 @@ def test_confirma_review_raspuns_contine_script_updateN(client):
|
||||
r = client.post(f"/_import/{iid}/rand/0/confirma-review", data={"csrf_token": csrf})
|
||||
assert r.status_code == 200, r.text
|
||||
|
||||
html = r.text
|
||||
# Scriptul trebuie sa fie in raspuns
|
||||
assert "window.updateN" in html or "updateN" in html, (
|
||||
"Raspunsul confirma-review trebuie sa contina scriptul updateN "
|
||||
"pentru ca htmx sa-l execute la swap in #detaliu-modal-body."
|
||||
)
|
||||
# Scriptul NU trebuie sa aiba hx-swap-oob (altfel nu ar fi executat nici asa)
|
||||
script_idx = html.rfind("<script>")
|
||||
assert script_idx >= 0, "Tag-ul <script> nu a fost gasit in raspuns"
|
||||
script_content = html[script_idx:]
|
||||
assert "hx-swap-oob" not in script_content, (
|
||||
"Scriptul updateN NU trebuie sa aiba hx-swap-oob — trebuie sa fie in "
|
||||
"continutul principal pentru executie."
|
||||
trig = _json.loads(r.headers.get("HX-Trigger", "{}"))
|
||||
assert trig.get("reincarcaPreview") is True, (
|
||||
"confirma-review trebuie sa ceara reincarcaPreview — preview-ul reincarcat aduce "
|
||||
"n_confirmat corect server-side (fara dependenta de scriptul updateN din payload)."
|
||||
)
|
||||
|
||||
@@ -95,17 +95,23 @@ def test_editeaza_intra_in_mod_editare_form_propriu(client):
|
||||
assert 'name="data_prestatie"' in html and 'name="vin"' in html
|
||||
|
||||
|
||||
def test_salveaza_reda_doar_randul(client):
|
||||
"""POST editeaza: raspuns = fragmentul randului + OOB contoare, NU tot #import-section (D-3.1)."""
|
||||
def test_salveaza_cere_reincarcare_si_toast(client):
|
||||
"""POST editeaza: raspuns minimal + HX-Trigger(reincarcaPreview + randSalvat).
|
||||
|
||||
Contractul nou (dogfood 5.13): nu mai facem OOB swap pe <tr> (fragil in htmx 1.9 ->
|
||||
randul ramanea cu starea veche). Raspunsul cere reincarcarea preview-ului si emite
|
||||
detaliile randului salvat pentru toast/evidentiere."""
|
||||
import json as _json
|
||||
_seed_op1()
|
||||
iid = _upload_and_preview(client)
|
||||
r = client.post(f"/_import/{iid}/rand/0/editeaza", data={"data_prestatie": "2026-06-10"})
|
||||
assert r.status_code == 200
|
||||
html = r.text
|
||||
assert 'id="preview-row-0"' in html
|
||||
# OOB pe rezumat (contoare), NU re-randarea sectiunii intregi.
|
||||
assert 'id="preview-rezumat"' in html and 'hx-swap-oob="true"' in html
|
||||
assert 'id="import-section"' not in html
|
||||
trig = _json.loads(r.headers.get("HX-Trigger", "{}"))
|
||||
assert trig.get("reincarcaPreview") is True
|
||||
assert trig.get("randSalvat", {}).get("nr") == 1
|
||||
# Raspunsul e doar un stub; randul real vine din reload-ul preview-ului.
|
||||
assert 'id="preview-row-0"' not in r.text
|
||||
assert 'id="import-section"' not in r.text
|
||||
|
||||
|
||||
def test_enter_in_camp_editare_nu_declanseaza_confirm(client):
|
||||
|
||||
@@ -232,21 +232,21 @@ def test_stepper_pas3_la_preview_direct_mapare_retinuta(client):
|
||||
|
||||
|
||||
def test_stepper_marcheaza_pasii_facuti(client):
|
||||
"""In preview (pas 3), pasii 1 si 2 sunt marcati ca facuti (clasa 'facut').
|
||||
"""In preview (pas 3), pasii 1 si 2 sunt marcati ca facuti (clasa is-done).
|
||||
|
||||
Verifica prin prezenta clasei CSS sau a marcajului vizual de 'facut'.
|
||||
Verifica prin prezenta clasei CSS is-done (doua aparitii: pasii 1 si 2).
|
||||
"""
|
||||
_seed_op_mapping(client)
|
||||
import_id = _upload_and_get_import_id(client)
|
||||
text = _get_preview_via_mapare(client, import_id)
|
||||
|
||||
# Clasa "facut" trebuie sa apara pentru pasii 1 si 2 (index < pas curent)
|
||||
assert "facut" in text, \
|
||||
"Clasa/marcajul 'facut' nu a fost gasit in preview (pasii 1 si 2 ar trebui marcati ca facuti)"
|
||||
# Numarul de aparitii: cel putin 2 pasi marcati ca facuti
|
||||
count_facut = text.count("facut")
|
||||
assert count_facut >= 2, \
|
||||
f"Asteptat cel putin 2 pasi marcati ca 'facut' in preview, gasit {count_facut}"
|
||||
# Clasa "is-done" trebuie sa apara pentru pasii 1 si 2 (index < pas curent)
|
||||
assert "is-done" in text, \
|
||||
"Clasa 'is-done' nu a fost gasita in preview (pasii 1 si 2 ar trebui marcati ca is-done)"
|
||||
# Numarul de aparitii: cel putin 2 pasi marcati ca is-done
|
||||
count_done = text.count("is-done")
|
||||
assert count_done >= 2, \
|
||||
f"Asteptat cel putin 2 pasi marcati ca 'is-done' in preview, gasit {count_done}"
|
||||
|
||||
|
||||
def test_import_hx_target_in_tab(client):
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
"""Teste US-011 (PRD 5.10): butoane icon salvare/stergere + dirty state pe Mapari.
|
||||
"""Teste US-011 (PRD 5.10): butoane salvare/stergere + dirty state pe Mapari.
|
||||
|
||||
Actualizat PRD 5.13: superseda sistemul .icon-btn din 5.10 -> sistem .act
|
||||
(desktop text / mobil iconita 44px).
|
||||
|
||||
Cerinte:
|
||||
- Butoane .icon-btn mereu vizibile pe rand (nu ascunse in kebab)
|
||||
- Butoane .act mereu vizibile pe rand (nu ascunse in kebab)
|
||||
- Meniu kebab (<details class="kebab">) eliminat
|
||||
- aria-label descriptiv pe fiecare buton icon
|
||||
- aria-label pe fiecare buton .act (label scurt: "Salveaza" / "Sterge")
|
||||
- Iconita Lucide (.act-ic svg) prezenta in output
|
||||
- data-dirty-form pe butonul de salvare (permite JS dirty-state)
|
||||
"""
|
||||
|
||||
@@ -75,7 +79,10 @@ def client(monkeypatch):
|
||||
|
||||
|
||||
def test_butoane_icon_vizibile_pe_rand_salvate(client):
|
||||
"""Butoanele de salvare/stergere in 'Mapari salvate' au clasa icon-btn (mereu vizibile)."""
|
||||
"""Butoanele de salvare/stergere in 'Mapari salvate' au clasele .act (mereu vizibile pe rand).
|
||||
|
||||
PRD 5.13: .icon-btn inlocuit cu sistemul .act (act-save / act-del).
|
||||
"""
|
||||
acct = _create_account_user("actiuni_icon@test.com")
|
||||
_seed_saved_mapping(acct)
|
||||
_login(client, "actiuni_icon@test.com")
|
||||
@@ -84,9 +91,13 @@ def test_butoane_icon_vizibile_pe_rand_salvate(client):
|
||||
assert resp.status_code == 200
|
||||
html = resp.text
|
||||
|
||||
assert 'class="icon-btn' in html, (
|
||||
"Butoanele de actiune din 'Mapari salvate' trebuie sa aiba clasa 'icon-btn' "
|
||||
"(mereu vizibile pe rand, nu ascunse in kebab)."
|
||||
assert 'class="act act-save"' in html, (
|
||||
"Butonul de salvare din 'Mapari salvate' trebuie sa aiba clasa 'act act-save' "
|
||||
"(mereu vizibil pe rand, nu ascuns in kebab)."
|
||||
)
|
||||
assert 'class="act act-del"' in html, (
|
||||
"Butonul de stergere din 'Mapari salvate' trebuie sa aiba clasa 'act act-del' "
|
||||
"(mereu vizibil pe rand, nu ascuns in kebab)."
|
||||
)
|
||||
|
||||
|
||||
@@ -109,7 +120,11 @@ def test_fara_kebab_meniu(client):
|
||||
|
||||
|
||||
def test_butoane_cu_aria_label(client):
|
||||
"""Butoanele icon-btn au aria-label descriptiv."""
|
||||
"""Butoanele .act din 'Mapari salvate' au aria-label si iconita Lucide (.act-ic).
|
||||
|
||||
PRD 5.13: aria-label scurt ("Salveaza" / "Sterge") pe butonul .act;
|
||||
iconita SVG cu clasa .act-ic (Lucide stroke) prezenta pentru mobil.
|
||||
"""
|
||||
acct = _create_account_user("actiuni_aria@test.com")
|
||||
_seed_saved_mapping(acct)
|
||||
_login(client, "actiuni_aria@test.com")
|
||||
@@ -118,10 +133,13 @@ def test_butoane_cu_aria_label(client):
|
||||
assert resp.status_code == 200
|
||||
html = resp.text
|
||||
|
||||
icon_btns = re.findall(r'<button[^>]+class="icon-btn[^"]*"[^>]*>', html)
|
||||
assert icon_btns, "Trebuie sa existe butoane cu clasa icon-btn in 'Mapari salvate'."
|
||||
assert any('aria-label' in btn for btn in icon_btns), (
|
||||
"Cel putin un buton icon-btn trebuie sa aiba atributul aria-label descriptiv."
|
||||
act_btns = re.findall(r'<button[^>]+class="act[^"]*"[^>]*>', html)
|
||||
assert act_btns, "Trebuie sa existe butoane cu clasa 'act' in 'Mapari salvate'."
|
||||
assert any('aria-label' in btn for btn in act_btns), (
|
||||
"Cel putin un buton .act trebuie sa aiba atributul aria-label."
|
||||
)
|
||||
assert 'class="act-ic"' in html, (
|
||||
"Iconita Lucide (.act-ic svg) trebuie sa fie prezenta in output (afisata pe mobil)."
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -241,17 +241,23 @@ def test_editeaza_preview_serveste_fragment_modal(client):
|
||||
"Fragmentul modal nu trebuie sa contina confirm-form"
|
||||
|
||||
|
||||
def test_salvare_preview_inchide_modal_si_oob_rand(client):
|
||||
"""POST /_import/{id}/rand/0/editeaza cu date valide → HX-Trigger-After-Settle: inchideModal
|
||||
+ OOB pe rand (#preview-row-0) si contoare (#preview-rezumat).
|
||||
def test_salvare_preview_inchide_modal_si_reincarca(client):
|
||||
"""POST /_import/{id}/rand/0/editeaza cu date valide → inchide modalul + reincarca preview-ul.
|
||||
|
||||
Contractul a fost schimbat (dogfood 5.13): OOB swap pe <tr> esua tacit in htmx 1.9
|
||||
(un <tr> brut se pierde la parsarea fragmentului) -> randul ramanea cu starea veche.
|
||||
Acum raspunsul NU mai contine OOB pe rand; emite HX-Trigger:
|
||||
- reincarcaPreview -> #import-section isi reincarca preview-ul complet;
|
||||
- randSalvat (cu nr + stare) -> toast + evidentiere in base.html;
|
||||
iar HX-Trigger-After-Settle: inchideModal inchide modalul.
|
||||
|
||||
Verifica:
|
||||
- Status 200
|
||||
- Header HX-Trigger-After-Settle contine 'inchideModal'
|
||||
- Raspuns contine OOB pentru randul actualizat (hx-swap-oob prezent)
|
||||
- Raspuns contine OOB pentru rezumat (#preview-rezumat)
|
||||
- NU re-randeaza intreaga sectiune (#import-section absent)
|
||||
- HX-Trigger-After-Settle contine 'inchideModal'
|
||||
- HX-Trigger contine 'reincarcaPreview' si 'randSalvat' (cu numarul randului)
|
||||
- Raspunsul NU re-randeaza inline randul/sectiunea (reload via GET)
|
||||
"""
|
||||
import json as _json
|
||||
_seed_op1()
|
||||
iid = _upload_and_preview(client)
|
||||
|
||||
@@ -259,26 +265,21 @@ def test_salvare_preview_inchide_modal_si_oob_rand(client):
|
||||
"data_prestatie": "2026-06-15",
|
||||
})
|
||||
assert r.status_code == 200, r.text
|
||||
html = r.text
|
||||
|
||||
# Header de inchidere modal
|
||||
trigger = r.headers.get("HX-Trigger-After-Settle", "")
|
||||
assert "inchideModal" in trigger, \
|
||||
f"Header HX-Trigger-After-Settle trebuie sa contina 'inchideModal', gasit: '{trigger}'"
|
||||
trigger_settle = r.headers.get("HX-Trigger-After-Settle", "")
|
||||
assert "inchideModal" in trigger_settle, \
|
||||
f"HX-Trigger-After-Settle trebuie sa contina 'inchideModal', gasit: '{trigger_settle}'"
|
||||
|
||||
# OOB pe randul actualizat
|
||||
assert 'id="preview-row-0"' in html, \
|
||||
"Raspunsul trebuie sa contina randul actualizat (#preview-row-0)"
|
||||
assert "hx-swap-oob" in html, \
|
||||
"Raspunsul trebuie sa contina OOB swap"
|
||||
trigger = _json.loads(r.headers.get("HX-Trigger", "{}"))
|
||||
assert trigger.get("reincarcaPreview") is True, \
|
||||
"HX-Trigger trebuie sa ceara reincarcarea preview-ului (reincarcaPreview)"
|
||||
assert "randSalvat" in trigger, "HX-Trigger trebuie sa contina detaliile randului salvat"
|
||||
assert trigger["randSalvat"]["nr"] == 1, "randSalvat.nr = numarul (1-based) al randului editat"
|
||||
assert "stare" in trigger["randSalvat"], "randSalvat trebuie sa contina noua stare (pentru toast)"
|
||||
|
||||
# OOB pe rezumatul stari
|
||||
assert 'id="preview-rezumat"' in html, \
|
||||
"Raspunsul trebuie sa contina OOB pe #preview-rezumat"
|
||||
|
||||
# NU re-randeaza intreaga sectiune de import
|
||||
assert 'id="import-section"' not in html, \
|
||||
"Editarea randului NU trebuie sa re-randeze intreaga sectiune #import-section"
|
||||
# Raspunsul e doar un stub invizibil — randul real vine din reload, nu din OOB.
|
||||
assert 'id="preview-row-0"' not in r.text, \
|
||||
"Noul contract NU mai face OOB swap pe rand (reload complet via reincarcaPreview)"
|
||||
|
||||
|
||||
def test_anuleaza_nu_lasa_rand_orfan(client):
|
||||
|
||||
@@ -65,6 +65,14 @@ def _login(client, email: str, password: str = "parolasecreta10") -> None:
|
||||
assert resp.status_code == 303
|
||||
|
||||
|
||||
def _bloc_mobil_principal(html: str) -> str:
|
||||
"""Felie din CSS de la sentinel-ul blocului mobil principal pana la `</style>`."""
|
||||
i = html.find("SENTINEL-TESTE-MOBIL")
|
||||
assert i != -1, "Lipseste SENTINEL-TESTE-MOBIL in base.html (vezi PRD 5.13 Wave 0)"
|
||||
j = html.find("</style>", i)
|
||||
return html[i:(j if j != -1 else len(html))]
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def client(monkeypatch):
|
||||
tmp = tempfile.mkdtemp()
|
||||
@@ -332,10 +340,7 @@ def test_header_elemente_nu_au_min_height_fix_pe_mobil(client):
|
||||
"Blocul tableta nu reseteaza min-height pentru header"
|
||||
|
||||
# Blocul mobil (<768px) reseteaza si el min-height (regresie: nu a disparut).
|
||||
# Folosim `{` ca sa nu potrivim mentionarile din comentarii CSS.
|
||||
mobil_idx = html.find("@media (max-width:767px) {")
|
||||
assert mobil_idx != -1
|
||||
mobil = html[mobil_idx:mobil_idx + 5000]
|
||||
mobil = _bloc_mobil_principal(html)
|
||||
assert "min-height:0" in mobil, "Blocul mobil a pierdut resetarea min-height pe header"
|
||||
|
||||
|
||||
@@ -348,10 +353,7 @@ def test_modal_full_screen_pe_mobil(client):
|
||||
html = client.get("/?tab=acasa").text
|
||||
|
||||
# Regula CSS full-screen exista in blocul @media (max-width:767px) {.
|
||||
# Folosim varianta cu `{` ca sa NU potrivim mentionarile din comentarii CSS.
|
||||
mobil_idx = html.find("@media (max-width:767px) {")
|
||||
assert mobil_idx != -1, "Nu exista bloc @media (max-width:767px) { in CSS"
|
||||
mobil = html[mobil_idx:mobil_idx + 5000]
|
||||
mobil = _bloc_mobil_principal(html)
|
||||
assert "100vw" in mobil, "Dialogul nu are latime 100vw pe mobil"
|
||||
assert "100vh" in mobil, "Dialogul nu are inaltime 100vh pe mobil"
|
||||
# Butonul de inchidere >=44px (tinta touch) pe mobil.
|
||||
@@ -363,3 +365,138 @@ def test_modal_full_screen_pe_mobil(client):
|
||||
# Target swap pentru editare preview (US-006) exista in DOM.
|
||||
assert 'id="detaliu-modal-body"' in html, \
|
||||
"Target #detaliu-modal-body lipseste din base.html"
|
||||
|
||||
|
||||
# ============================================================
|
||||
# PRD 5.13: guard-uri responsive card mobil + sistem actiuni + stepper compact
|
||||
# ============================================================
|
||||
|
||||
|
||||
def test_card_mobil_fara_break_vertical_120px(client):
|
||||
"""P0: blocul mobil principal NU mai forteaza min-width:120px pe eticheta
|
||||
(cauza break-ului vertical caracter-cu-caracter). Eticheta stivuita deasupra
|
||||
valorii: td::before cu display:block. Checkbox + # ascunse pe card."""
|
||||
_create_account_user("card120@test.com")
|
||||
_login(client, "card120@test.com")
|
||||
html = client.get("/?tab=acasa").text
|
||||
bloc = _bloc_mobil_principal(html)
|
||||
|
||||
# min-width:120px nu mai exista in blocul mobil.
|
||||
assert "min-width:120px" not in bloc, \
|
||||
"min-width:120px inca prezent in blocul mobil — cauzeaza break vertical"
|
||||
|
||||
# td::before stivuieste eticheta deasupra valorii (display:block).
|
||||
assert ".tabel-trimiteri td::before" in bloc, \
|
||||
".tabel-trimiteri td::before lipseste din blocul mobil"
|
||||
assert "display:block" in bloc, \
|
||||
"display:block lipseste din blocul mobil (eticheta nu e stivuita)"
|
||||
|
||||
# col-chk si col-id ascunse pe card (nu ocupa spatiu).
|
||||
assert ".tabel-trimiteri td.col-chk, .tabel-trimiteri td.col-id { display:none; }" in bloc, \
|
||||
"col-chk/col-id nu sunt ascunse in blocul mobil"
|
||||
|
||||
|
||||
def test_sistem_act_desktop_text_mobil_icon(client):
|
||||
"""Sistemul .act: desktop = iconita ascunsa (text vizibil),
|
||||
mobil = text ascuns, iconita vizibila 44px (tinta touch)."""
|
||||
_create_account_user("act_sys@test.com")
|
||||
_login(client, "act_sys@test.com")
|
||||
html = client.get("/?tab=acasa").text
|
||||
bloc = _bloc_mobil_principal(html)
|
||||
|
||||
# Desktop: act-ic ascunsa implicit (display:none in regula de baza).
|
||||
assert ".act .act-ic { width:18px; height:18px; display:none; }" in html, \
|
||||
"Regula desktop care ascunde .act-ic lipseste din CSS"
|
||||
|
||||
# Mobil: text ascuns, iconita vizibila.
|
||||
assert ".act .act-tx { display:none; }" in bloc, \
|
||||
".act .act-tx nu e ascuns in blocul mobil"
|
||||
assert ".act .act-ic { display:inline-block; }" in bloc, \
|
||||
".act .act-ic nu devine vizibila in blocul mobil"
|
||||
|
||||
# .act are tinta touch >=44px pe mobil.
|
||||
assert ".act { min-width:44px" in bloc, \
|
||||
".act nu are min-width:44px in blocul mobil"
|
||||
|
||||
|
||||
def _seed_saved_mapping_responsive(acct_id: int) -> None:
|
||||
"""Insereaza o mapare salvata in operations_mapping (pentru test act aria-label)."""
|
||||
from app.db import get_connection
|
||||
conn = get_connection()
|
||||
try:
|
||||
conn.execute(
|
||||
"INSERT INTO operations_mapping (account_id, cod_op_service, cod_prestatie, auto_send) "
|
||||
"VALUES (?, ?, ?, ?)",
|
||||
(acct_id, "OP-RESP-99", "OE-1", 1),
|
||||
)
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def test_act_btn_aria_label_invariant(client):
|
||||
"""Invariant a11y: butoanele .act din Mapari au mereu aria-label
|
||||
(accesibil in modul icon-only pe mobil) si iconita .act-ic."""
|
||||
acct = _create_account_user("actaria@test.com")
|
||||
_seed_saved_mapping_responsive(acct)
|
||||
_login(client, "actaria@test.com")
|
||||
|
||||
html = client.get("/_fragments/mapari").text
|
||||
|
||||
# Butoane cu clasa act exista in fragmentul de mapari.
|
||||
act_btns = re.findall(r'<button[^>]+class="act[^"]*"[^>]*>', html)
|
||||
assert act_btns, "Trebuie sa existe butoane cu clasa 'act' in fragmentul Mapari"
|
||||
|
||||
# Fiecare buton .act are aria-label (accesibil cand textul e ascuns pe mobil).
|
||||
assert any("aria-label" in btn for btn in act_btns), \
|
||||
"Niciun buton .act nu are aria-label — inaccessibil in modul icon-only"
|
||||
|
||||
# Iconita .act-ic prezenta in markup (afisata pe mobil).
|
||||
assert 'class="act-ic"' in html, \
|
||||
"Iconita .act-ic lipseste din fragmentul Mapari"
|
||||
|
||||
|
||||
def test_stepper_compact_clase(client):
|
||||
"""Stepper compact (5.13): clasele stepper-track, stepper-collapsed,
|
||||
stepper-progress prezente; textul 'Pasul N din 4' randat; vechile clase
|
||||
stepper-pas-- absente; comutarea la <1024px declarata in CSS."""
|
||||
_create_account_user("stepper@test.com")
|
||||
_login(client, "stepper@test.com")
|
||||
html = client.get("/?tab=acasa").text
|
||||
|
||||
# Elementele de structura ale stepper-ului compact.
|
||||
assert "stepper-track" in html, "stepper-track lipseste din markup"
|
||||
assert "stepper-collapsed" in html, "stepper-collapsed lipseste din markup"
|
||||
assert "stepper-progress" in html, "stepper-progress lipseste din markup"
|
||||
|
||||
# Textul de progres in forma colapsata (<1024px).
|
||||
assert "Pasul 1 din 4" in html, "Textul 'Pasul 1 din 4' lipseste din markup"
|
||||
|
||||
# Vechile clase anti-pattern cu stepper-pas-- nu mai exista.
|
||||
assert "stepper-pas--" not in html, \
|
||||
"Clasa veche stepper-pas-- inca prezenta — curata markup-ul"
|
||||
|
||||
# CSS declara comutarea la <1024px (track ascuns, collapsed afisat).
|
||||
assert "@media (max-width:1024px)" in html, \
|
||||
"Lipseste regula @media (max-width:1024px) pentru comutarea stepper-ului"
|
||||
|
||||
|
||||
def test_liste_actionabile_o_coloana_pana_1024(client):
|
||||
"""Guard scope (decizie 5.13): listele actionabile raman O COLOANA
|
||||
pana la 1024px — fara grila 2/rand (repeat(2)). Blocul tableta 768-1024px
|
||||
cardifica (thead ascuns, card per rand)."""
|
||||
_create_account_user("ocoloana@test.com")
|
||||
_login(client, "ocoloana@test.com")
|
||||
html = client.get("/?tab=acasa").text
|
||||
|
||||
# Nicaieri in CSS nu apare grila 2/rand (repeat(2, ...)).
|
||||
assert "repeat(2" not in html, \
|
||||
"CSS contine repeat(2 — listele actionabile NU trebuie sa fie 2/rand pana la 1024px"
|
||||
|
||||
# Exista blocul tableta (768-1024px).
|
||||
assert "@media (min-width:768px) and (max-width:1024px)" in html, \
|
||||
"Lipseste blocul @media tableta (min-width:768px) and (max-width:1024px)"
|
||||
|
||||
# Blocul tableta cardifica listele (thead ascuns = card per rand, o coloana).
|
||||
assert ".tabel-trimiteri thead, .tabel-card thead { display:none; }" in html, \
|
||||
"Blocul tableta nu ascunde thead-ul pentru cardificare (o coloana)"
|
||||
|
||||
Reference in New Issue
Block a user