feat(5.9): US-007 - responsive pagini de continut (card Mapari, scroll contained Jurnal/Nomenclator/Admin, formulare stivate)

- base.html @media(max-width:767px): clasa .tabel-card (card per rand) scopata
  SEPARAT de .tabel-trimiteri 5.8; reguli formular full-width + butoane >=44px
  scopate pe #card-cont / #form-test-cheie / #filtre-jurnal (marker US-007)
- _mapari.html: tabel-card + data-eticheta pe toate 4 tabelele; override select
  in card local (evita batalia de specificitate cu stilul inline)
- _integrare.html: id=form-test-cheie (ancora de scope pe formularul de test)
- R12 per-tabel: Mapari=card; Jurnal/Nomenclator/Admin=.tablewrap scroll contained;
  Cont/Integrare=fara tabele (doar formulare)
- tests: +3 (tabele clasa responsive, formulare full-width, regresie carduri 5.8)
- suita: 838 passed, 1 deselected
- prd.json US-007 passes=true + notes; progress.txt

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-06-25 09:17:47 +00:00
parent 141949dc95
commit d3433015ad
6 changed files with 220 additions and 25 deletions

View File

@@ -229,7 +229,8 @@
{# Formular test conexiune #}
<div class="card" style="margin-bottom:16px;">
<h3 style="margin:0 0 12px; font-size:15px;">Testeaza conexiunea</h3>
<form hx-post="/integrare/test-cheie"
<form id="form-test-cheie"
hx-post="/integrare/test-cheie"
hx-target="#integrare-test-rezultat"
hx-swap="innerHTML"
style="display:flex; gap:8px; flex-wrap:wrap; align-items:flex-end;">

View File

@@ -4,6 +4,10 @@
/* Selectul de cod RAR e principalul vinovat de latimea tabelelor de mapari. Il limitam ca
tabelul sa incapa in card fara scroll orizontal -> coloana Actiuni (kebab) ramane vizibila. */
#mapari-section td select { width:100%; max-width:240px; min-width:150px; }
/* US-007 (R12): in card per rand (sub 767px) selectul/inputurile umplu cardul. */
@media (max-width:767px) {
#mapari-section td select, #mapari-section td input[type=text] { max-width:none; min-width:0; }
}
</style>
{% if message %}
@@ -42,7 +46,7 @@
<input type="search" data-dt-search class="dt-search"
placeholder="Cauta operatie sau cod..." aria-label="Cauta in operatiile de rezolvat">
</div>
<div class="tablewrap">
<div class="tablewrap tabel-card">
<table>
<thead><tr>
<th>Operatie</th>
@@ -58,7 +62,7 @@
{# data-dt-row = haystack de cautare (randul contine un <select> cu tot nomenclatorul). #}
<tr data-dt-row="{{ e.cod_op_service }} {{ e.denumire or '' }}
{%- for s in e.suggestions[:3] %} {{ s.cod_prestatie }}{% endfor %}">
<td>
<td data-eticheta="Operatie">
<form id="map-rez-{{ loop.index }}" hx-post="/mapari" hx-target="#mapari-section" hx-swap="outerHTML">
<input type="hidden" name="csrf_token" value="{{ csrf_token or '' }}">
<input type="hidden" name="cod_op_service" value="{{ e.cod_op_service }}">
@@ -67,14 +71,14 @@
<span class="pill" title="submission-uri blocate">{{ e.blocked }} blocate</span></div>
<div class="muted">{{ e.denumire or '(fara denumire)' }}</div>
</td>
<td class="muted" style="font-size:12px;">
<td class="muted" style="font-size:12px;" data-eticheta="Sugestii">
{% if e.suggestions %}
{% for s in e.suggestions[:3] %}
<span class="sugg">{{ s.cod_prestatie }} ({{ s.score|round|int }}%)</span>{% if not loop.last %}, {% endif %}
{% endfor %}
{% else %}—{% endif %}
</td>
<td>
<td data-eticheta="Cod RAR">
<select name="cod_prestatie" form="map-rez-{{ loop.index }}" required
aria-label="Cod RAR pentru {{ e.cod_op_service }}">
<option value="">— alege cod RAR —</option>
@@ -85,7 +89,7 @@
{% endfor %}
</select>
</td>
<td>
<td data-eticheta="In coada">
{{ ui.autosend_toggle(form_id="map-rez-" ~ loop.index, checked=True) }}
</td>
<td>
@@ -120,7 +124,7 @@
<input type="search" data-dt-search class="dt-search"
placeholder="Cauta operatie sau cod RAR..." aria-label="Cauta in maparile salvate">
</div>
<div class="tablewrap">
<div class="tablewrap tabel-card">
<table>
<thead><tr>
<th>Operatie</th>
@@ -132,7 +136,7 @@
{% for m in saved_mappings %}
{# data-dt-row = haystack de cautare (randul contine un <select> cu tot nomenclatorul). #}
<tr data-dt-row="{{ m.cod_op_service }} {{ m.cod_prestatie }} {{ m.nume_prestatie or '' }}">
<td>
<td data-eticheta="Operatie">
<form id="map-salv-{{ loop.index }}" hx-post="/mapari/salvate" hx-target="#mapari-section" hx-swap="outerHTML">
<input type="hidden" name="csrf_token" value="{{ csrf_token or '' }}">
<input type="hidden" name="cod_op_service" value="{{ m.cod_op_service }}">
@@ -147,7 +151,7 @@
acum: {{ m.cod_prestatie }}{% if m.nume_prestatie %} — {{ m.nume_prestatie }}{% endif %}
</div>
</td>
<td>
<td data-eticheta="Cod RAR">
<select name="cod_prestatie" form="map-salv-{{ loop.index }}" required
aria-label="Cod RAR pentru {{ m.cod_op_service }}">
{% for n in nomenclator %}
@@ -157,10 +161,10 @@
{% endfor %}
</select>
</td>
<td>
<td data-eticheta="In coada">
{{ ui.autosend_toggle(form_id="map-salv-" ~ loop.index, checked=m.auto_send) }}
</td>
<td style="text-align:right; white-space:nowrap;">
<td style="text-align:right; white-space:nowrap;" data-eticheta="Actiuni">
{# Salveaza/Sterge in meniu contextual (kebab) — randul ramane ingust. Butoanele se
leaga prin form= de cele doua form-uri hx-post definite in prima celula a randului. #}
<details class="kebab">
@@ -203,7 +207,7 @@
<input type="search" data-dt-search class="dt-search"
placeholder="Cauta coloana sau camp..." aria-label="Cauta in formatele de coloane">
</div>
<div class="tablewrap">
<div class="tablewrap tabel-card">
<table>
<thead><tr>
<th>Coloane</th>
@@ -214,15 +218,15 @@
<tbody>
{% for f in column_formats %}
<tr>
<td style="white-space:nowrap;">
<td style="white-space:nowrap;" data-eticheta="Coloane">
<strong>{{ f.columns | length }} coloane</strong>
</td>
<td class="muted" style="font-size:12px; white-space:normal; max-width:340px;">
<td class="muted" style="font-size:12px; white-space:normal; max-width:340px;" data-eticheta="Mapari (coloana &rarr; camp)">
{% for col, camp in f.mappings.items() %}
<span class="sugg">{{ col }}</span> &rarr; {{ camp }}{% if not loop.last %}; {% endif %}
{% endfor %}
</td>
<td>
<td data-eticheta="Format data">
<form id="fmt-edit-{{ loop.index }}" hx-post="/formate-coloane/editeaza"
hx-target="#mapari-section" hx-swap="outerHTML"
style="display:flex; gap:6px; align-items:center;">
@@ -274,7 +278,7 @@
</div>
{% endif %}
<div class="tablewrap">
<div class="tablewrap tabel-card">
<table>
<thead><tr>
<th>Daca operatia contine</th>
@@ -285,7 +289,7 @@
<tbody>
{% for r in text_rules %}
<tr>
<td>
<td data-eticheta="Daca operatia contine">
<form id="rt-del-{{ loop.index }}" hx-post="/mapari/reguli-text/sterge"
hx-target="#mapari-section" hx-swap="outerHTML"
hx-confirm="Stergi regula «{{ r.pattern }}»?">
@@ -294,10 +298,10 @@
</form>
<div>contine <strong>«{{ r.pattern }}»</strong></div>
</td>
<td class="muted" style="font-size:12px;">
<td class="muted" style="font-size:12px;" data-eticheta="Cod RAR">
{{ r.cod_prestatie }}
</td>
<td class="muted" style="font-size:12px;">
<td class="muted" style="font-size:12px;" data-eticheta="In coada">
{% if r.auto_send %}Auto (in coada){% else %}Manual (verificare){% endif %}
</td>
<td style="text-align:right; white-space:nowrap;">
@@ -310,7 +314,7 @@
{% endfor %}
{# Rand de adaugare (mereu prezent ca placeholder, inclusiv in empty state). #}
<tr>
<td>
<td data-eticheta="Daca operatia contine">
<form id="rt-add" hx-post="/mapari/reguli-text" hx-target="#mapari-section" hx-swap="outerHTML">
<input type="hidden" name="csrf_token" value="{{ csrf_token or '' }}">
<input type="text" name="pattern" required
@@ -324,7 +328,7 @@
hx-include="#rt-add">
</form>
</td>
<td>
<td data-eticheta="Cod RAR">
<select name="cod_prestatie" form="rt-add" required aria-label="Cod RAR pentru regula text">
<option value="">— alege cod RAR —</option>
{% for n in nomenclator %}
@@ -332,7 +336,7 @@
{% endfor %}
</select>
</td>
<td>
<td data-eticheta="In coada">
{{ ui.autosend_toggle(form_id="rt-add", checked=False) }}
</td>
<td style="text-align:right; white-space:nowrap;">

View File

@@ -256,6 +256,40 @@
.icon-btn { min-height:44px; min-width:44px; }
.tab-link { min-height:44px; padding:10px 14px; }
.cont-menu a, .cont-menu button { min-height:44px; }
/* === PRD 5.9 US-007 (R12): paginile de continut pe mobil ===
Tabele ACTIONABILE (Mapari) -> card per rand. Clasa proprie `.tabel-card`,
scopata SEPARAT de `.tabel-trimiteri` (5.8) ca sa NU strice cardurile de
trimiteri. Tabele DENSE read-only (Jurnal, Nomenclator) + Admin raman in
`.tablewrap` (scroll orizontal CONTAINED, definit global mai sus). */
.tabel-card table { table-layout:auto; }
.tabel-card thead { display:none; }
.tabel-card table, .tabel-card tbody, .tabel-card tr, .tabel-card td { display:block; width:auto; }
.tabel-card tr { border:1px solid var(--line); border-radius:8px; padding:10px 12px; margin-bottom:10px; }
.tabel-card td { border-bottom:none; padding:5px 0; }
.tabel-card td::before { content:attr(data-eticheta); display:block; color:var(--muted);
font-size:12px; margin-bottom:3px; }
/* Celulele fara eticheta (doar actiuni) nu primesc antet gol. */
.tabel-card td:not([data-eticheta])::before,
.tabel-card td[data-eticheta=""]::before { display:none; }
/* Controale full-width in card; butoanele primesc tinta touch >=44px. */
.tabel-card td select, .tabel-card td input[type=text],
.tabel-card td input[type=search] { width:100%; max-width:none; }
.tabel-card td button { width:100%; min-height:44px; }
/* Formulare de continut: o coloana, inputuri/selecturi full-width, butoane >=44px.
Scopat strict pe controalele de formular din sectiunile de continut (nu atinge
tabelul de trimiteri, modalul, taburile ARIA sau butoanele de copiere absolute). */
#card-cont input[type=email], #card-cont input[type=password],
#form-test-cheie input[type=password],
#jurnal-section select, #jurnal-section input[type=date],
#jurnal-section input[type=number] {
/* !important fiindca aceste inputuri au latimi inline (ex. width:280px / max-width:100px)
pe desktop; le suprascriem DOAR sub 767px, deci desktop ramane neschimbat. */
width:100% !important; max-width:none !important; box-sizing:border-box; }
#jurnal-section #filtre-jurnal > div { width:100%; }
#card-cont button, #form-test-cheie button,
#jurnal-section #filtre-jurnal button { min-height:44px; width:100%; }
}
</style>
</head>

View File

@@ -190,12 +190,12 @@
],
"requiresBrowserCheck": true,
"requiresDesignReview": false,
"passes": false,
"passes": true,
"failed": false,
"blocked": false,
"retries": 0,
"failureReason": "",
"notes": ""
"notes": "Implementat. R12 per-tabel: Mapari=CARD (clasa .tabel-card scopata separat de .tabel-trimiteri 5.8, data-eticheta pe td-uri in toate 4 tabelele); Jurnal/Nomenclator/Admin=.tablewrap scroll contained (deja existau; dense/admin-only, risc minim). Cont/Integrare=fara tabele, doar formulare stivate. CSS card+formulare in blocul @media(max-width:767px) din base.html (marker US-007). Formulare: inputuri full-width (!important doar pe mobil ca sa invinga latimile inline desktop), butoane >=44px, filtre jurnal pe o coloana. id=form-test-cheie adaugat la integrare pt scope. Override select mapari in card local in _mapari.html (evita batalia de ordine/specificitate cu stilul inline). Teste: test_tabele_continut_au_clasa_responsive, test_formulare_full_width_mobil, test_carduri_trimiteri_5_8_supravietuiesc (regresie). Suita: 838 passed, 1 deselected. E2E ramas pt browser (375px fara scroll orizontal de pagina pe fiecare pagina)."
},
{
"id": "US-008",

View File

@@ -142,3 +142,55 @@ Note: PRD APROBAT 2026-06-24 cu revizii obligatorii R1-R12 (raport AUTOPLAN). R1
fiecare camp o data (nr. rand propriu, VIN dedesubt), corectez data, „Salveaza si retrimite"
-> queued; error arata „Re-pune in coada"; „Sterge" clar separat, rosu, full-width pe mobil.
---
## US-007: Responsive — paginile de continut (Mapari, Cont, Nomenclator, Integrare, Jurnal, Admin)
### Decizia per-tabel (R12):
- Mapari = CARD per rand sub 767px (tabele actionabile: selecturi nomenclator + butoane +
kebab). Toate cele 4 tabele (De rezolvat, Mapari salvate, Formate coloane, Reguli text)
primesc `data-eticheta` pe `<td>`-uri + clasa `tabel-card` pe `.tablewrap`.
- Jurnal = .tablewrap (scroll orizontal contained). Dens read-only, multe coloane (Cand/Sursa/
Tip/Nivel/Cont/Cod/Mesaj) — cardul ar fi lung si zgomotos; scroll contained pastreaza tabularitatea.
- Nomenclator = .tablewrap (scroll contained). Dens read-only (Cod/Denumire/Actualizat).
- Admin = .tablewrap (scroll contained). Actionabil DAR admin-only, dens (8 coloane), actiunile sunt
deja in kebab compact + bulk-bar; cardul ar fi cost mare pe o pagina low-traffic pe mobil. Risc minim.
- Cont / Integrare = fara tabele (doar formulare + blocuri de cod). Formularele stivate; blocurile
`<pre>` de cod si tab-bar-urile aveau deja `overflow-x:auto` (contained).
### Ce s-a livrat:
- app/web/templates/base.html: in blocul `@media (max-width:767px)` (marker US-007):
- `.tabel-card` — card per rand SCOPAT separat de `.tabel-trimiteri` (5.8 intact). thead ascuns,
tr=card, td=block cu `::before` din `data-eticheta`; celulele fara eticheta (doar actiuni) nu
afiseaza antet gol; selecturi/inputuri full-width, butoane in card width:100% + min-height:44px.
- Formulare de continut pe o coloana: inputuri full-width (`width:100% !important` DOAR pe mobil,
ca sa invinga latimile inline de desktop — desktop neschimbat), butoane >=44px + full-width,
filtrele de jurnal stivuite (`#filtre-jurnal > div { width:100% }`). Scopat strict pe
`#card-cont`, `#form-test-cheie`, `#jurnal-section #filtre-jurnal` (nu atinge taburile ARIA,
butoanele de copiere absolute, tabelul de trimiteri sau modalul).
- app/web/templates/_mapari.html: `tabel-card` pe cele 4 `.tablewrap`; `data-eticheta` pe td-uri;
override local in `<style>` (`@media 767px` -> select/input `max-width:none; min-width:0`) ca sa
evite batalia de specificitate/ordine cu regula inline `#mapari-section td select`.
- app/web/templates/_integrare.html: `id="form-test-cheie"` pe formularul de test (ancora de scope).
- tests/test_web_responsive.py: +3 teste (test_tabele_continut_au_clasa_responsive,
test_formulare_full_width_mobil, test_carduri_trimiteri_5_8_supravietuiesc — regresie 5.8).
### Gates rulate:
- Typecheck/Lint: SKIP (comenzi goale in techStack).
- Tests: PASS — tests/test_web_responsive.py 6; suita completa 838 passed, 1 deselected.
- Browser (E2E requiresBrowserCheck): DEFERAT la VERIFY (loop fara browser).
### Learnings:
- `.tablewrap` (overflow-x:auto, scroll contained) exista DEJA global din 5.5 si era aplicat pe toate
tabelele de continut (Jurnal/Nomenclator/Mapari/Admin). US-007 a insemnat: (a) CONVERSIA Mapari la
card (nou), (b) confirmarea scroll-contained pe restul, (c) tratamentul de formular pe mobil.
- Stilurile inline (`style="width:..."`) din templates inv ing stylesheet-ul fara `!important`;
pe mobil am folosit `!important` scopat strict, evitand regresia pe desktop.
- Scoparea separata `.tabel-card` vs `.tabel-trimiteri` e CRITICA: ambele mecanisme de card coexista
in acelasi bloc media; test de regresie confirma ca blocul 5.8 (.tabel-trimiteri thead/td::before)
supravietuieste.
### Next:
- US-008 (responsive Acasa + login/signup), US-005 (poll nu inchide modalul).
- VERIFY: gstack browser la 375px pe /?tab=mapari, ?tab=jurnal, Cont/Nomenclator/Integrare, /admin —
fiecare fara scroll orizontal de pagina, tabele lizibile, formulare pe o coloana.
---

View File

@@ -110,3 +110,107 @@ def test_nav_colapsabil_sub_breakpoint(client):
# Sub breakpoint, tintele touch din header/nav cresc la >=44px.
mobil = html[html.find("@media (max-width:767px)"):]
assert "44px" in mobil
# ============================================================
# PRD 5.9 US-007: responsive paginile de continut
# (Mapari, Cont, Nomenclator, Integrare, Jurnal, Admin)
# ============================================================
def _seed_nomenclator(items):
from app.mapping import upsert_nomenclator
from app.db import get_connection
conn = get_connection()
try:
upsert_nomenclator(conn, items)
conn.commit()
finally:
conn.close()
def _seed_event(account_id, tip="test", mesaj="eveniment de test"):
from app.observ import log_event
from app.db import get_connection
conn = get_connection()
try:
log_event(tip, account_id=account_id, mesaj=mesaj, conn=conn)
conn.commit()
finally:
conn.close()
def test_tabele_continut_au_clasa_responsive(client):
"""R12 politica per-tabel:
- Mapari (actionabil) = CARD per rand: clasa proprie `.tabel-card` + `data-eticheta` pe `<td>`-uri.
- Jurnal / Nomenclator (dense read-only) = `.tablewrap` (scroll orizontal CONTAINED),
FARA `.tabel-card`.
Definitia regulii de card traieste in base.html, scopata SEPARAT de `.tabel-trimiteri`.
"""
acct = _create_account_user("t7@test.com")
_login(client, "t7@test.com")
# --- Mapari = card. Tabelul „Reguli automate (text)" e mereu randat (rand de adaugare),
# deci nu depinde de date seedate. ---
mapari = client.get("/?tab=mapari").text
assert "tabel-card" in mapari
assert 'data-eticheta="Cod RAR"' in mapari
assert 'data-eticheta="Daca operatia contine"' in mapari
# Regula de card e definita o data in base.html, scopata pe `.tabel-card`.
assert ".tabel-card thead" in mapari
assert "US-007" in mapari
# --- Jurnal = scroll contained. Cu un eveniment seedat, tabelul apare in `.tablewrap`,
# NU ca `.tabel-card`. ---
_seed_event(acct)
jurnal = client.get("/?tab=jurnal").text
assert "tablewrap" in jurnal
# Sectiunea de jurnal NU foloseste cardul actionabil.
body = jurnal[jurnal.find('id="jurnal-section"'):]
assert "tabel-card" not in body
# --- Nomenclator = scroll contained (dens read-only). ---
_seed_nomenclator([{"codPrestatie": "OE-2", "numePrestatie": "Revizie"}])
nomen = client.get("/?tab=nomenclator").text
assert "tablewrap" in nomen
def test_formulare_full_width_mobil(client):
"""Formularele de continut (Cont, Integrare test, filtre Jurnal) stiveaza pe o coloana
sub 767px: inputuri full-width + butoane >=44px. Verificam ancorele de scope (id-uri)
plus regulile CSS mobil din base.html (toate scopate sub `@media (max-width:767px)`)."""
_create_account_user("f7@test.com")
_login(client, "f7@test.com")
cont = client.get("/?tab=cont").text
assert 'id="card-cont"' in cont
# Regulile mobil de formular exista si sunt scopate pe sectiunile de continut.
mobil = cont[cont.find("@media (max-width:767px)"):]
assert "#card-cont button" in mobil
assert "min-height:44px" in mobil
assert "width:100% !important" in mobil # suprascrie latimile inline doar pe mobil
integrare = client.get("/?tab=integrare").text
assert 'id="form-test-cheie"' in integrare
# Filtrele de jurnal (form mereu prezent, indiferent de date) primesc scope-ul mobil.
jurnal = client.get("/?tab=jurnal").text
assert 'id="filtre-jurnal"' in jurnal
assert "#jurnal-section #filtre-jurnal button" in jurnal
def test_carduri_trimiteri_5_8_supravietuiesc(client):
"""Regresie R12: scoparea `.tabel-card` (US-007) NU trebuie sa atinga blocul
`.tabel-trimiteri @media(max-width:767px)` din 5.8 — cardurile de trimiteri raman."""
_create_account_user("r58@test.com")
_login(client, "r58@test.com")
html = client.get("/?tab=acasa").text
mobil = html[html.find("@media (max-width:767px)"):]
# Cardurile de trimiteri 5.8 (clasa proprie, separata de `.tabel-card`).
assert ".tabel-trimiteri thead { display:none; }" in mobil
assert ".tabel-trimiteri td::before" in mobil
# Cele doua mecanisme coexista, scopate distinct.
assert ".tabel-card thead" in mobil