feat(ui): order totals, decimals, mobile modal cards, set editing

- Dashboard/Logs: Total column with 2 decimals (order_total)
- Order detail modal: totals summary row (items total + order total)
- Order detail modal mobile: compact article cards (d-md-none)
- Mappings: openEditModal loads all CODMATs for SKU, saveMapping
  replaces entire set via delete-all + batch POST
- Add project-specific team agents: ui-templates, ui-js, ui-verify,
  backend-api
- CLAUDE.md: mandatory preview approval before implementation,
  fix-loop after verification, server must start via start.sh

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-03-15 21:55:58 +00:00
parent ac8a01eb3e
commit 137c4a8b0b
11 changed files with 438 additions and 41 deletions

View File

@@ -0,0 +1,72 @@
---
name: backend-api
description: Team agent pentru modificari backend FastAPI — routers, services, modele Pydantic, integrare Oracle/SQLite. Folosit in TeamCreate pentru Task-uri care implica logica server-side, endpoint-uri noi, sau schimbari in servicii.
model: sonnet
---
# Backend API Agent
Esti un teammate specializat pe backend FastAPI in proiectul GoMag Import Manager.
## Responsabilitati
- Modificari in `api/app/routers/*.py` — endpoint-uri FastAPI
- Modificari in `api/app/services/*.py` — logica business
- Modificari in `api/app/models/` sau scheme Pydantic
- Integrare Oracle (oracledb) si SQLite (aiosqlite)
- Migrari schema SQLite (adaugare coloane, tabele noi)
## Fisiere cheie
- `api/app/main.py` — entry point, middleware, router include
- `api/app/config.py` — setari Pydantic (env vars)
- `api/app/database.py` — Oracle pool + SQLite connections
- `api/app/routers/dashboard.py` — comenzi dashboard
- `api/app/routers/sync.py` — sync, history, order detail
- `api/app/routers/mappings.py` — CRUD mapari SKU
- `api/app/routers/articles.py` — cautare articole Oracle
- `api/app/routers/validation.py` — validare comenzi
- `api/app/services/sync_service.py` — orchestrator sync
- `api/app/services/gomag_client.py` — client API GoMag
- `api/app/services/sqlite_service.py` — tracking local SQLite
- `api/app/services/mapping_service.py` — logica mapari
- `api/app/services/import_service.py` — import Oracle PL/SQL
## Patterns importante
- **Dual DB**: Oracle pentru date ERP (read/write), SQLite pentru tracking local
- **`from .. import database`** — importa modulul, nu `pool` direct (pool e None la import)
- **`asyncio.to_thread()`** — wrapeaza apeluri Oracle blocante
- **CLOB**: `cursor.var(oracledb.DB_TYPE_CLOB)` + `setvalue(0, json_string)`
- **Paginare**: OFFSET/FETCH (Oracle 12c+)
- **Pre-validare**: valideaza TOATE SKU-urile inainte de creat partener/adresa/comanda
## Environment
```
ORACLE_USER=CONTAFIN_ORACLE
ORACLE_DSN=ROA_ROMFAST
TNS_ADMIN=/app
APP_PORT=5003
SQLITE_DB_PATH=...
```
## Workflow in echipa
1. Citeste task-ul cu `TaskGet` sa intelegi exact ce trebuie facut
2. Marcheaza task-ul ca `in_progress` cu `TaskUpdate`
3. Citeste fisierele afectate inainte sa le modifici
4. Implementeaza modificarile
5. Ruleaza testele de baza: `cd /workspace/gomag-vending && python api/test_app_basic.py`
6. Marcheaza task-ul ca `completed` cu `TaskUpdate`
7. Trimite mesaj la `team-lead` cu:
- Endpoint-uri create/modificate (metoda HTTP + path)
- Schimbari in schema SQLite (daca exista)
- Contracte API noi pe care frontend-ul trebuie sa le stie
## Principii
- Nu modifica fisiere HTML/CSS/JS (sunt ale agentilor UI)
- Pastreaza backward compatibility la endpoint-uri existente
- Adauga campuri noi in raspunsuri JSON fara sa le stergi pe cele vechi
- Logheaza erorile Oracle cu detalii suficiente pentru debug

50
.claude/agents/ui-js.md Normal file
View File

@@ -0,0 +1,50 @@
---
name: ui-js
description: Team agent pentru modificari JavaScript (dashboard.js, logs.js, mappings.js, shared.js). Folosit in TeamCreate pentru Task-uri care implica logica client-side, API calls, si interactivitate UI.
model: sonnet
---
# UI JavaScript Agent
Esti un teammate specializat pe JavaScript client-side in proiectul GoMag Import Manager.
## Responsabilitati
- Modificari in `api/app/static/js/*.js`
- Fetch API calls catre backend (`/api/...`)
- Rendering dinamic HTML (tabele, liste, modals)
- Paginare, sortare, filtrare client-side
- Mobile vs desktop rendering logic
## Fisiere cheie
- `api/app/static/js/shared.js` - utilitare comune (fmtDate, statusDot, renderUnifiedPagination, renderMobileSegmented, esc)
- `api/app/static/js/dashboard.js` - logica dashboard comenzi
- `api/app/static/js/logs.js` - logica jurnale import
- `api/app/static/js/mappings.js` - CRUD mapari SKU
## Functii utilitare disponibile (din shared.js)
- `fmtDate(dateStr)` - formateaza data
- `statusDot(status)` - dot colorat pentru status
- `orderStatusBadge(status)` - badge Bootstrap pentru status
- `renderUnifiedPagination(page, totalPages, goPageFn, opts)` - paginare
- `renderMobileSegmented(containerId, items, onSelect)` - segmented control mobil
- `esc(s)` / `escHtml(s)` - escape HTML
## Workflow in echipa
1. Citeste task-ul cu `TaskGet` sa intelegi exact ce trebuie facut
2. Marcheaza task-ul ca `in_progress` cu `TaskUpdate`
3. Citeste fisierele afectate inainte sa le modifici
4. Implementeaza modificarile
5. Marcheaza task-ul ca `completed` cu `TaskUpdate`
6. Trimite mesaj la `team-lead` cu summary-ul modificarilor
## Principii
- Nu modifica fisiere HTML/CSS (sunt ale ui-templates agent)
- `Math.round(x)``Number(x).toFixed(2)` pentru valori monetare
- Verifica intotdeauna null/undefined inainte de operatii numerice: `x != null ? Number(x).toFixed(2) : '-'`
- Reset elementele din modal la inceputul fiecarei deschideri (loading state)
- Foloseste `esc()` pe orice valoare inserata in HTML

View File

@@ -0,0 +1,42 @@
---
name: ui-templates
description: Team agent pentru modificari HTML templates (dashboard.html, logs.html, mappings.html, base.html) si CSS (style.css). Folosit in TeamCreate pentru Task-uri care implica template-uri Jinja2 si stilizare.
model: sonnet
---
# UI Templates Agent
Esti un teammate specializat pe templates HTML si CSS in proiectul GoMag Import Manager.
## Responsabilitati
- Modificari in `api/app/templates/*.html` (Jinja2)
- Modificari in `api/app/static/css/style.css`
- Cache-bust: incrementeaza `?v=N` pe toate tag-urile `<script>` si `<link>` la fiecare modificare
- Structura modala Bootstrap 5.3
- Responsive: `d-none d-md-block` pentru desktop-only, `d-md-none` pentru mobile-only
## Fisiere cheie
- `api/app/templates/base.html` - layout de baza cu navigatie
- `api/app/templates/dashboard.html` - dashboard comenzi
- `api/app/templates/logs.html` - jurnale import
- `api/app/templates/mappings.html` - CRUD mapari SKU
- `api/app/templates/missing_skus.html` - SKU-uri lipsa
- `api/app/static/css/style.css` - stiluri aplicatie
## Workflow in echipa
1. Citeste task-ul cu `TaskGet` sa intelegi exact ce trebuie facut
2. Marcheaza task-ul ca `in_progress` cu `TaskUpdate`
3. Citeste fisierele afectate inainte sa le modifici
4. Implementeaza modificarile
5. Marcheaza task-ul ca `completed` cu `TaskUpdate`
6. Trimite mesaj la `team-lead` cu summary-ul modificarilor
## Principii
- Nu modifica fisiere JS (sunt ale ui-js agent)
- Desktop layout-ul nu se schimba cand se adauga imbunatatiri mobile
- Foloseste clasele Bootstrap existente, nu adauga CSS custom decat daca e necesar
- Pastreaza consistenta cu designul existent

View File

@@ -0,0 +1,61 @@
---
name: ui-verify
description: Team agent de verificare Playwright pentru UI. Captureaza screenshots after-implementation, compara cu preview-urile aprobate, si raporteaza discrepante la team lead. Folosit intotdeauna dupa implementare.
model: sonnet
---
# UI Verify Agent
Esti un teammate specializat pe verificare vizuala Playwright in proiectul GoMag Import Manager.
## Responsabilitati
- Capturare screenshots post-implementare → `screenshots/after/`
- Comparare vizuala `after/` vs `preview/`
- Verificare ca desktop-ul ramane neschimbat unde nu s-a modificat intentionat
- Raportare discrepante la team lead cu descriere exacta
## Server
App ruleaza la `http://localhost:5003`. Verifica cu `curl -s http://localhost:5003/health` inainte de screenshots.
**IMPORTANT**: NU restarteaza serverul singur. Serverul trebuie pornit de user via `./start.sh` care seteaza variabilele de mediu Oracle (`LD_LIBRARY_PATH`, `TNS_ADMIN`). Daca serverul nu raspunde sau Oracle e `"error"`, raporteaza la team-lead si asteapta ca userul sa-l reporneasca.
## Viewports
- **Mobile:** 375x812 — `browser_resize width=375 height=812`
- **Desktop:** 1440x900 — `browser_resize width=1440 height=900`
## Pagini de verificat
- `http://localhost:5003/` — Dashboard
- `http://localhost:5003/logs?run=<run_id>` — Logs cu run selectat
- `http://localhost:5003/mappings` — Mapari SKU
- `http://localhost:5003/missing-skus` — SKU-uri lipsa
## Workflow in echipa
1. Citeste task-ul cu `TaskGet` pentru lista exacta de pagini si criterii de verificat
2. Marcheaza task-ul ca `in_progress` cu `TaskUpdate`
3. Restarteza serverul daca e necesar
4. Captureaza screenshots la ambele viewports pentru fiecare pagina
5. Verifica vizual fiecare screenshot vs criteriile din task
6. Marcheaza task-ul ca `completed` cu `TaskUpdate`
7. Trimite raport detaliat la `team-lead`:
- ✅ Ce e corect
- ❌ Ce e gresit / lipseste (cu descriere exacta)
- Sugestii de fix daca e cazul
## Naming convention screenshots
```
screenshots/after/dashboard_desktop.png
screenshots/after/dashboard_mobile.png
screenshots/after/dashboard_modal_desktop.png
screenshots/after/dashboard_modal_mobile.png
screenshots/after/logs_desktop.png
screenshots/after/logs_mobile.png
screenshots/after/logs_modal_desktop.png
screenshots/after/logs_modal_mobile.png
screenshots/after/mappings_desktop.png
```

View File

@@ -20,8 +20,9 @@ Importa automat comenzi din GoMag in sistemul ERP ROA Oracle. Stack complet Pyth
## Development Commands ## Development Commands
```bash ```bash
# Run FastAPI server # Run FastAPI server — INTOTDEAUNA via start.sh (seteaza Oracle env vars)
cd api && uvicorn app.main:app --host 0.0.0.0 --port 5003 --reload ./start.sh
# NU folosi uvicorn direct — lipsesc LD_LIBRARY_PATH si TNS_ADMIN pentru Oracle
# Tests # Tests
python api/test_app_basic.py # Test A - fara Oracle python api/test_app_basic.py # Test A - fara Oracle
@@ -30,18 +31,21 @@ python api/test_integration.py # Test C - cu Oracle
## UI Development Workflow: Before → Preview → After ## UI Development Workflow: Before → Preview → After
For UI/frontend changes, follow this visual verification workflow: **OBLIGATORIU**: Respecta ordinea exacta. NU treci la pasul urmator fara aprobare explicita.
### 1. Before Screenshots ### 1. Before Screenshots
Capture current state with Playwright MCP at target viewports: Captureaza starea curenta cu Playwright MCP:
- **Mobile:** 375x812 - **Mobile:** 375x812
- **Desktop:** 1440x900 - **Desktop:** 1440x900
Save to `screenshots/before/` Salveaza in `screenshots/before/`
### 2. Plan & Preview ### 2. Plan & Preview — ASTEAPTA APROBARE
- Write implementation plan with design decisions 1. Citeste TOATE fisierele implicate
- Generate preview mockups if needed → save to `screenshots/preview/` 2. Scrie planul de implementare cu decizii de design
- Get user approval on previews before implementation 3. Genereaza mockup-uri HTML/CSS statice care arata rezultatul asteptat → salveaza in `screenshots/preview/`
4. **Prezinta mockup-urile userului si ASTEAPTA aprobare explicita**
5. Rafineaza planul daca userul cere modificari
6. **NU trece la implementare pana userul nu spune explicit "ok", "aprob", "executa" sau similar**
### 3. Implementation cu TeamCreate (Agent Teams) ### 3. Implementation cu TeamCreate (Agent Teams)
@@ -57,7 +61,12 @@ Folosim **TeamCreate** (team agents), NU superpowers subagents. Diferenta:
- Task 1: Templates + CSS (HTML templates, style.css, cache-bust) - Task 1: Templates + CSS (HTML templates, style.css, cache-bust)
- Task 2: JavaScript (shared.js, dashboard.js, logs.js, mappings.js) - Task 2: JavaScript (shared.js, dashboard.js, logs.js, mappings.js)
- Task 3: Verificare Playwright (depinde de Task 1 + Task 2) - Task 3: Verificare Playwright (depinde de Task 1 + Task 2)
4. **Agent tool** cu `team_name` spawneaza teammates care isi iau task-uri din lista 4. **Agent tool** cu `team_name` spawneaza teammates folosind agentii predefiniti din `.claude/agents/`:
- `subagent_type: ui-templates` → pentru Task 1 (templates + CSS)
- `subagent_type: ui-js` → pentru Task 2 (JavaScript)
- `subagent_type: ui-verify` → pentru Task 3 (Playwright verification)
- `subagent_type: backend-api` → pentru modificari backend/API (routers, services, Oracle/SQLite)
- `subagent_type: qa-tester` → pentru teste de integrare
5. Teammates lucreaza in paralel, comunica intre ei, marcheaza task-uri completate 5. Teammates lucreaza in paralel, comunica intre ei, marcheaza task-uri completate
6. Cand Task 1 + Task 2 sunt complete, teammate-ul de verificare preia Task 3 6. Cand Task 1 + Task 2 sunt complete, teammate-ul de verificare preia Task 3
@@ -68,6 +77,13 @@ Folosim **TeamCreate** (team agents), NU superpowers subagents. Diferenta:
4. Raporteaza discrepante la team lead 4. Raporteaza discrepante la team lead
5. Verifica ca desktop-ul ramane neschimbat 5. Verifica ca desktop-ul ramane neschimbat
#### Bucla de corectie (responsabilitatea team lead-ului):
1. Dupa ce verify-agent raporteaza, **team lead-ul analizeaza discrepantele**
2. Pentru fiecare discrepanta, creeaza un nou task de fix si spawneaza un agent sa-l rezolve
3. Dupa fix, spawneaza din nou verify-agent pentru re-verificare
4. **Repeta bucla** pana cand toate verificarile trec (after ≈ preview)
5. Abia atunci declara task-ul complet
``` ```
screenshots/ screenshots/
├── before/ # Starea inainte de modificari ├── before/ # Starea inainte de modificari

View File

@@ -713,3 +713,26 @@ tr.mapping-deleted td {
/* Hide per-page selector on mobile */ /* Hide per-page selector on mobile */
.per-page-label { display: none; } .per-page-label { display: none; }
} }
/* Mobile article cards in order detail modal */
.detail-item-card {
border: 1px solid #e5e7eb;
border-radius: 6px;
padding: 0.5rem 0.75rem;
margin-bottom: 0.5rem;
font-size: 0.875rem;
}
.detail-item-card .card-sku {
font-family: monospace;
font-size: 0.8rem;
color: #6b7280;
}
.detail-item-card .card-name {
font-weight: 500;
margin-bottom: 0.25rem;
}
.detail-item-card .card-details {
display: flex;
gap: 1rem;
color: #374151;
}

View File

@@ -295,7 +295,6 @@ async function loadDashOrders() {
// Invoice info // Invoice info
let invoiceBadge = ''; let invoiceBadge = '';
let invoiceTotal = '';
if (o.status !== 'IMPORTED' && o.status !== 'ALREADY_IMPORTED') { if (o.status !== 'IMPORTED' && o.status !== 'ALREADY_IMPORTED') {
invoiceBadge = '<span class="text-muted">-</span>'; invoiceBadge = '<span class="text-muted">-</span>';
} else if (o.invoice && o.invoice.facturat) { } else if (o.invoice && o.invoice.facturat) {
@@ -303,11 +302,12 @@ async function loadDashOrders() {
if (o.invoice.serie_act || o.invoice.numar_act) { if (o.invoice.serie_act || o.invoice.numar_act) {
invoiceBadge += `<br><small>${esc(o.invoice.serie_act || '')} ${esc(String(o.invoice.numar_act || ''))}</small>`; invoiceBadge += `<br><small>${esc(o.invoice.serie_act || '')} ${esc(String(o.invoice.numar_act || ''))}</small>`;
} }
invoiceTotal = o.invoice.total_cu_tva ? Number(o.invoice.total_cu_tva).toFixed(2) : '-';
} else { } else {
invoiceBadge = `<span style="color:#dc2626">Nefacturat</span>`; invoiceBadge = `<span style="color:#dc2626">Nefacturat</span>`;
} }
const orderTotal = o.order_total != null ? Number(o.order_total).toFixed(2) : '-';
return `<tr style="cursor:pointer" onclick="openDashOrderDetail('${esc(o.order_number)}')"> return `<tr style="cursor:pointer" onclick="openDashOrderDetail('${esc(o.order_number)}')">
<td><code>${esc(o.order_number)}</code></td> <td><code>${esc(o.order_number)}</code></td>
<td>${dateStr}</td> <td>${dateStr}</td>
@@ -316,7 +316,7 @@ async function loadDashOrders() {
<td class="text-nowrap">${statusDot(o.status)} ${statusLabelText(o.status)}</td> <td class="text-nowrap">${statusDot(o.status)} ${statusLabelText(o.status)}</td>
<td>${o.id_comanda || '-'}</td> <td>${o.id_comanda || '-'}</td>
<td>${invoiceBadge}</td> <td>${invoiceBadge}</td>
<td>${invoiceTotal}</td> <td>${orderTotal}</td>
</tr>`; </tr>`;
}).join(''); }).join('');
} }
@@ -335,7 +335,7 @@ async function loadDashOrders() {
if (d.length >= 16) dateFmt += ' ' + d.slice(11, 16); if (d.length >= 16) dateFmt += ' ' + d.slice(11, 16);
} }
const name = o.shipping_name || o.customer_name || o.billing_name || '\u2014'; const name = o.shipping_name || o.customer_name || o.billing_name || '\u2014';
const totalStr = o.order_total ? Math.round(o.order_total) : ''; const totalStr = o.order_total ? Number(o.order_total).toFixed(2) : '';
return `<div class="flat-row" onclick="openDashOrderDetail('${esc(o.order_number)}')" style="font-size:0.875rem"> return `<div class="flat-row" onclick="openDashOrderDetail('${esc(o.order_number)}')" style="font-size:0.875rem">
${statusDot(o.status)} ${statusDot(o.status)}
<span style="color:#6b7280" class="text-nowrap">${dateFmt}</span> <span style="color:#6b7280" class="text-nowrap">${dateFmt}</span>
@@ -475,6 +475,12 @@ async function openDashOrderDetail(orderNumber) {
document.getElementById('detailIdAdresaLivr').textContent = '-'; document.getElementById('detailIdAdresaLivr').textContent = '-';
document.getElementById('detailItemsBody').innerHTML = '<tr><td colspan="8" class="text-center">Se incarca...</td></tr>'; document.getElementById('detailItemsBody').innerHTML = '<tr><td colspan="8" class="text-center">Se incarca...</td></tr>';
document.getElementById('detailError').style.display = 'none'; document.getElementById('detailError').style.display = 'none';
const detailItemsTotal = document.getElementById('detailItemsTotal');
if (detailItemsTotal) detailItemsTotal.textContent = '-';
const detailOrderTotal = document.getElementById('detailOrderTotal');
if (detailOrderTotal) detailOrderTotal.textContent = '-';
const mobileContainer = document.getElementById('detailItemsMobile');
if (mobileContainer) mobileContainer.innerHTML = '';
const modalEl = document.getElementById('orderDetailModal'); const modalEl = document.getElementById('orderDetailModal');
const existing = bootstrap.Modal.getInstance(modalEl); const existing = bootstrap.Modal.getInstance(modalEl);
@@ -510,6 +516,36 @@ async function openDashOrderDetail(orderNumber) {
return; return;
} }
// Update totals row
const itemsTotal = items.reduce((sum, item) => sum + (Number(item.price || 0) * Number(item.quantity || 0)), 0);
document.getElementById('detailItemsTotal').textContent = itemsTotal.toFixed(2) + ' lei';
document.getElementById('detailOrderTotal').textContent = order.order_total != null ? Number(order.order_total).toFixed(2) + ' lei' : '-';
// Mobile article cards
const mobileContainer = document.getElementById('detailItemsMobile');
if (mobileContainer) {
mobileContainer.innerHTML = items.map(item => {
let statusLabel = '';
switch (item.mapping_status) {
case 'mapped': statusLabel = '<span class="badge bg-success">Mapat</span>'; break;
case 'direct': statusLabel = '<span class="badge bg-info">Direct</span>'; break;
case 'missing': statusLabel = '<span class="badge bg-warning">Lipsa</span>'; break;
default: statusLabel = '<span class="badge bg-secondary">?</span>';
}
const codmat = item.codmat || '-';
return `<div class="detail-item-card">
<div class="card-sku">${esc(item.sku)}</div>
<div class="card-name">${esc(item.product_name || '-')}</div>
<div class="card-details">
<span>x${item.quantity || 0}</span>
<span>${item.price != null ? Number(item.price).toFixed(2) : '-'} lei</span>
<span><code>${esc(codmat)}</code></span>
<span>${statusLabel}</span>
</div>
</div>`;
}).join('');
}
document.getElementById('detailItemsBody').innerHTML = items.map(item => { document.getElementById('detailItemsBody').innerHTML = items.map(item => {
let statusBadge; let statusBadge;
switch (item.mapping_status) { switch (item.mapping_status) {

View File

@@ -151,10 +151,11 @@ async function loadRunOrders(runId, statusFilter, page) {
const orders = data.orders || []; const orders = data.orders || [];
if (orders.length === 0) { if (orders.length === 0) {
tbody.innerHTML = '<tr><td colspan="6" class="text-center text-muted py-3">Nicio comanda</td></tr>'; tbody.innerHTML = '<tr><td colspan="7" class="text-center text-muted py-3">Nicio comanda</td></tr>';
} else { } else {
tbody.innerHTML = orders.map((o, i) => { tbody.innerHTML = orders.map((o, i) => {
const dateStr = fmtDate(o.order_date); const dateStr = fmtDate(o.order_date);
const orderTotal = o.order_total != null ? Number(o.order_total).toFixed(2) : '-';
return `<tr style="cursor:pointer" onclick="openOrderDetail('${esc(o.order_number)}')"> return `<tr style="cursor:pointer" onclick="openOrderDetail('${esc(o.order_number)}')">
<td>${(ordersPage - 1) * 50 + i + 1}</td> <td>${(ordersPage - 1) * 50 + i + 1}</td>
<td>${dateStr}</td> <td>${dateStr}</td>
@@ -162,6 +163,7 @@ async function loadRunOrders(runId, statusFilter, page) {
<td>${esc(o.customer_name)}</td> <td>${esc(o.customer_name)}</td>
<td>${o.items_count || 0}</td> <td>${o.items_count || 0}</td>
<td class="text-nowrap">${statusDot(o.status)} ${logStatusText(o.status)}</td> <td class="text-nowrap">${statusDot(o.status)} ${logStatusText(o.status)}</td>
<td>${orderTotal}</td>
</tr>`; </tr>`;
}).join(''); }).join('');
} }
@@ -179,7 +181,7 @@ async function loadRunOrders(runId, statusFilter, page) {
dateFmt = d.slice(8, 10) + '.' + d.slice(5, 7) + '.' + d.slice(2, 4); dateFmt = d.slice(8, 10) + '.' + d.slice(5, 7) + '.' + d.slice(2, 4);
if (d.length >= 16) dateFmt += ' ' + d.slice(11, 16); if (d.length >= 16) dateFmt += ' ' + d.slice(11, 16);
} }
const totalStr = o.order_total ? Math.round(o.order_total) : ''; const totalStr = o.order_total ? Number(o.order_total).toFixed(2) : '';
return `<div class="flat-row" onclick="openOrderDetail('${esc(o.order_number)}')" style="font-size:0.875rem"> return `<div class="flat-row" onclick="openOrderDetail('${esc(o.order_number)}')" style="font-size:0.875rem">
${statusDot(o.status)} ${statusDot(o.status)}
<span style="color:#6b7280" class="text-nowrap">${dateFmt}</span> <span style="color:#6b7280" class="text-nowrap">${dateFmt}</span>
@@ -318,6 +320,12 @@ async function openOrderDetail(orderNumber) {
document.getElementById('detailIdAdresaLivr').textContent = '-'; document.getElementById('detailIdAdresaLivr').textContent = '-';
document.getElementById('detailItemsBody').innerHTML = '<tr><td colspan="8" class="text-center">Se incarca...</td></tr>'; document.getElementById('detailItemsBody').innerHTML = '<tr><td colspan="8" class="text-center">Se incarca...</td></tr>';
document.getElementById('detailError').style.display = 'none'; document.getElementById('detailError').style.display = 'none';
const detailItemsTotal = document.getElementById('detailItemsTotal');
if (detailItemsTotal) detailItemsTotal.textContent = '-';
const detailOrderTotal = document.getElementById('detailOrderTotal');
if (detailOrderTotal) detailOrderTotal.textContent = '-';
const mobileContainer = document.getElementById('detailItemsMobile');
if (mobileContainer) mobileContainer.innerHTML = '';
const modalEl = document.getElementById('orderDetailModal'); const modalEl = document.getElementById('orderDetailModal');
const existing = bootstrap.Modal.getInstance(modalEl); const existing = bootstrap.Modal.getInstance(modalEl);
@@ -353,6 +361,36 @@ async function openOrderDetail(orderNumber) {
return; return;
} }
// Update totals row
const itemsTotal = items.reduce((sum, item) => sum + (Number(item.price || 0) * Number(item.quantity || 0)), 0);
document.getElementById('detailItemsTotal').textContent = itemsTotal.toFixed(2) + ' lei';
document.getElementById('detailOrderTotal').textContent = order.order_total != null ? Number(order.order_total).toFixed(2) + ' lei' : '-';
// Mobile article cards
const mobileContainer = document.getElementById('detailItemsMobile');
if (mobileContainer) {
mobileContainer.innerHTML = items.map(item => {
let statusLabel = '';
switch (item.mapping_status) {
case 'mapped': statusLabel = '<span class="badge bg-success">Mapat</span>'; break;
case 'direct': statusLabel = '<span class="badge bg-info">Direct</span>'; break;
case 'missing': statusLabel = '<span class="badge bg-warning">Lipsa</span>'; break;
default: statusLabel = '<span class="badge bg-secondary">?</span>';
}
const codmat = item.codmat || '-';
return `<div class="detail-item-card">
<div class="card-sku">${esc(item.sku)}</div>
<div class="card-name">${esc(item.product_name || '-')}</div>
<div class="card-details">
<span>x${item.quantity || 0}</span>
<span>${item.price != null ? Number(item.price).toFixed(2) : '-'} lei</span>
<span><code>${esc(codmat)}</code></span>
<span>${statusLabel}</span>
</div>
</div>`;
}).join('');
}
document.getElementById('detailItemsBody').innerHTML = items.map(item => { document.getElementById('detailItemsBody').innerHTML = items.map(item => {
let statusBadge; let statusBadge;
switch (item.mapping_status) { switch (item.mapping_status) {

View File

@@ -276,24 +276,51 @@ function clearAddForm() {
addCodmatLine(); addCodmatLine();
} }
function openEditModal(sku, codmat, cantitate, procent) { async function openEditModal(sku, codmat, cantitate, procent) {
editingMapping = { sku, codmat }; editingMapping = { sku, codmat };
document.getElementById('addModalTitle').textContent = 'Editare Mapare'; document.getElementById('addModalTitle').textContent = 'Editare Mapare';
document.getElementById('inputSku').value = sku; document.getElementById('inputSku').value = sku;
document.getElementById('inputSku').readOnly = false; document.getElementById('inputSku').readOnly = true;
document.getElementById('pctWarning').style.display = 'none'; document.getElementById('pctWarning').style.display = 'none';
const container = document.getElementById('codmatLines'); const container = document.getElementById('codmatLines');
container.innerHTML = ''; container.innerHTML = '';
addCodmatLine();
// Pre-fill the CODMAT line try {
// Fetch all CODMATs for this SKU
const res = await fetch(`/api/mappings?search=${encodeURIComponent(sku)}&per_page=100`);
const data = await res.json();
const allMappings = (data.mappings || []).filter(m => m.sku === sku && !m.sters);
if (allMappings.length === 0) {
// Fallback to single line with passed values
addCodmatLine();
const line = container.querySelector('.codmat-line'); const line = container.querySelector('.codmat-line');
if (line) { if (line) {
line.querySelector('.cl-codmat').value = codmat; line.querySelector('.cl-codmat').value = codmat;
line.querySelector('.cl-cantitate').value = cantitate; line.querySelector('.cl-cantitate').value = cantitate;
line.querySelector('.cl-procent').value = procent; line.querySelector('.cl-procent').value = procent;
} }
} else {
for (const m of allMappings) {
addCodmatLine();
const lines = container.querySelectorAll('.codmat-line');
const line = lines[lines.length - 1];
line.querySelector('.cl-codmat').value = m.codmat;
line.querySelector('.cl-cantitate').value = m.cantitate_roa;
line.querySelector('.cl-procent').value = m.procent_pret;
}
}
} catch (e) {
// Fallback on error
addCodmatLine();
const line = container.querySelector('.codmat-line');
if (line) {
line.querySelector('.cl-codmat').value = codmat;
line.querySelector('.cl-cantitate').value = cantitate;
line.querySelector('.cl-procent').value = procent;
}
}
new bootstrap.Modal(document.getElementById('addModal')).show(); new bootstrap.Modal(document.getElementById('addModal')).show();
} }
@@ -395,7 +422,8 @@ async function saveMapping() {
let res; let res;
if (editingMapping) { if (editingMapping) {
// Edit mode: use PUT /api/mappings/{old_sku}/{old_codmat}/edit if (mappings.length === 1) {
// Single CODMAT edit: use existing PUT endpoint
res = await fetch(`/api/mappings/${encodeURIComponent(editingMapping.sku)}/${encodeURIComponent(editingMapping.codmat)}/edit`, { res = await fetch(`/api/mappings/${encodeURIComponent(editingMapping.sku)}/${encodeURIComponent(editingMapping.codmat)}/edit`, {
method: 'PUT', method: 'PUT',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
@@ -406,6 +434,26 @@ async function saveMapping() {
procent_pret: mappings[0].procent_pret procent_pret: mappings[0].procent_pret
}) })
}); });
} else {
// Multi-CODMAT set: delete all existing then create new batch
const existRes = await fetch(`/api/mappings?search=${encodeURIComponent(editingMapping.sku)}&per_page=100`);
const existData = await existRes.json();
const existing = (existData.mappings || []).filter(m => m.sku === editingMapping.sku && !m.sters);
// Delete each existing CODMAT
for (const m of existing) {
await fetch(`/api/mappings/${encodeURIComponent(m.sku)}/${encodeURIComponent(m.codmat)}`, {
method: 'DELETE'
});
}
// Create new batch
res = await fetch('/api/mappings/batch', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ sku, mappings })
});
}
} else if (mappings.length === 1) { } else if (mappings.length === 1) {
res = await fetch('/api/mappings', { res = await fetch('/api/mappings', {
method: 'POST', method: 'POST',

View File

@@ -121,7 +121,11 @@
<small class="text-muted">ID Adr. Livrare:</small> <span id="detailIdAdresaLivr">-</span> <small class="text-muted">ID Adr. Livrare:</small> <span id="detailIdAdresaLivr">-</span>
</div> </div>
</div> </div>
<div class="table-responsive"> <div class="d-flex justify-content-end gap-3 mb-2" id="detailTotals">
<span><small class="text-muted">Valoare articole:</small> <strong id="detailItemsTotal">-</strong></span>
<span><small class="text-muted">Total comanda:</small> <strong id="detailOrderTotal">-</strong></span>
</div>
<div class="table-responsive d-none d-md-block">
<table class="table table-sm table-bordered mb-0"> <table class="table table-sm table-bordered mb-0">
<thead class="table-light"> <thead class="table-light">
<tr> <tr>
@@ -139,6 +143,7 @@
</tbody> </tbody>
</table> </table>
</div> </div>
<div class="d-md-none" id="detailItemsMobile"></div>
<div id="detailError" class="alert alert-danger mt-3" style="display:none;"></div> <div id="detailError" class="alert alert-danger mt-3" style="display:none;"></div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
@@ -178,5 +183,5 @@
{% endblock %} {% endblock %}
{% block scripts %} {% block scripts %}
<script src="/static/js/dashboard.js?v=5"></script> <script src="/static/js/dashboard.js?v=6"></script>
{% endblock %} {% endblock %}

View File

@@ -73,10 +73,11 @@
<th class="sortable" onclick="sortOrdersBy('customer_name')">Client <span class="sort-icon" data-col="customer_name"></span></th> <th class="sortable" onclick="sortOrdersBy('customer_name')">Client <span class="sort-icon" data-col="customer_name"></span></th>
<th class="sortable" onclick="sortOrdersBy('items_count')">Articole <span class="sort-icon" data-col="items_count"></span></th> <th class="sortable" onclick="sortOrdersBy('items_count')">Articole <span class="sort-icon" data-col="items_count"></span></th>
<th class="sortable" onclick="sortOrdersBy('status')">Status <span class="sort-icon" data-col="status"></span></th> <th class="sortable" onclick="sortOrdersBy('status')">Status <span class="sort-icon" data-col="status"></span></th>
<th>Total</th>
</tr> </tr>
</thead> </thead>
<tbody id="runOrdersBody"> <tbody id="runOrdersBody">
<tr><td colspan="6" class="text-center text-muted py-3">Selecteaza un sync run</td></tr> <tr><td colspan="7" class="text-center text-muted py-3">Selecteaza un sync run</td></tr>
</tbody> </tbody>
</table> </table>
</div> </div>
@@ -115,7 +116,11 @@
<small class="text-muted">ID Adr. Livrare:</small> <span id="detailIdAdresaLivr">-</span> <small class="text-muted">ID Adr. Livrare:</small> <span id="detailIdAdresaLivr">-</span>
</div> </div>
</div> </div>
<div class="table-responsive"> <div class="d-flex justify-content-end gap-3 mb-2" id="detailTotals">
<span><small class="text-muted">Valoare articole:</small> <strong id="detailItemsTotal">-</strong></span>
<span><small class="text-muted">Total comanda:</small> <strong id="detailOrderTotal">-</strong></span>
</div>
<div class="table-responsive d-none d-md-block">
<table class="table table-sm table-bordered mb-0"> <table class="table table-sm table-bordered mb-0">
<thead class="table-light"> <thead class="table-light">
<tr> <tr>
@@ -133,6 +138,7 @@
</tbody> </tbody>
</table> </table>
</div> </div>
<div class="d-md-none" id="detailItemsMobile"></div>
<div id="detailError" class="alert alert-danger mt-3" style="display:none;"></div> <div id="detailError" class="alert alert-danger mt-3" style="display:none;"></div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
@@ -175,5 +181,5 @@
{% endblock %} {% endblock %}
{% block scripts %} {% block scripts %}
<script src="/static/js/logs.js?v=5"></script> <script src="/static/js/logs.js?v=6"></script>
{% endblock %} {% endblock %}