chore: commit all pending changes including deploy scripts and Windows config
- deploy.ps1, iis-web.config: Windows Server deployment scripts - api/app/routers/sync.py, dashboard.py: router updates - api/app/services/import_service.py, sync_service.py: service updates - api/app/static/css/style.css, js/*.js: UI updates - api/database-scripts/08_PACK_FACTURARE.pck: Oracle package - .gitignore: add .gittoken - CLAUDE.md, agent configs: documentation updates Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
name: oracle-dba
|
name: oracle-dba
|
||||||
description: Oracle PL/SQL specialist for database scripts, packages, and schema changes in the ROA ERP system
|
description: Oracle PL/SQL specialist for database scripts, packages, and schema changes in the ROA ERP system
|
||||||
model: opus
|
model: sonnet
|
||||||
---
|
---
|
||||||
|
|
||||||
# Oracle DBA Agent
|
# Oracle DBA Agent
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
name: python-backend
|
name: python-backend
|
||||||
description: FastAPI backend developer for services, routes, Oracle/SQLite integration, and API logic
|
description: FastAPI backend developer for services, routes, Oracle/SQLite integration, and API logic
|
||||||
model: opus
|
model: sonnet
|
||||||
---
|
---
|
||||||
|
|
||||||
# Python Backend Agent
|
# Python Backend Agent
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -24,6 +24,7 @@ __pycache__/
|
|||||||
# Settings files with secrets
|
# Settings files with secrets
|
||||||
settings.ini
|
settings.ini
|
||||||
vfp/settings.ini
|
vfp/settings.ini
|
||||||
|
.gittoken
|
||||||
output/
|
output/
|
||||||
vfp/*.json
|
vfp/*.json
|
||||||
*.~pck
|
*.~pck
|
||||||
|
|||||||
28
CLAUDE.md
28
CLAUDE.md
@@ -29,25 +29,19 @@ python api/test_app_basic.py # Test A - fara Oracle
|
|||||||
python api/test_integration.py # Test C - cu Oracle
|
python api/test_integration.py # Test C - cu Oracle
|
||||||
```
|
```
|
||||||
|
|
||||||
## UI Development Workflow: Before → Preview → After
|
## UI Development Workflow: Preview → Implement → Verify
|
||||||
|
|
||||||
**OBLIGATORIU**: Respecta ordinea exacta. NU treci la pasul urmator fara aprobare explicita.
|
**OBLIGATORIU**: Respecta ordinea exacta. NU treci la pasul urmator fara aprobare explicita.
|
||||||
|
|
||||||
### 1. Before Screenshots
|
### 1. Plan & Preview — ASTEAPTA APROBARE
|
||||||
Captureaza starea curenta cu Playwright MCP:
|
|
||||||
- **Mobile:** 375x812
|
|
||||||
- **Desktop:** 1440x900
|
|
||||||
Salveaza in `screenshots/before/`
|
|
||||||
|
|
||||||
### 2. Plan & Preview — ASTEAPTA APROBARE
|
|
||||||
1. Citeste TOATE fisierele implicate
|
1. Citeste TOATE fisierele implicate
|
||||||
2. Scrie planul de implementare cu decizii de design
|
2. Scrie planul de implementare cu decizii de design
|
||||||
3. Genereaza mockup-uri HTML/CSS statice care arata rezultatul asteptat → salveaza in `screenshots/preview/`
|
3. Genereaza **mockup-uri Markdown** care descriu rezultatul asteptat (tabele, liste, cod pseudo-CSS) — NU HTML static
|
||||||
4. **Prezinta mockup-urile userului si ASTEAPTA aprobare explicita**
|
4. **Prezinta mockup-urile userului si ASTEAPTA aprobare explicita**
|
||||||
5. Rafineaza planul daca userul cere modificari
|
5. Rafineaza planul daca userul cere modificari
|
||||||
6. **NU trece la implementare pana userul nu spune explicit "ok", "aprob", "executa" sau similar**
|
6. **NU trece la implementare pana userul nu spune explicit "ok", "aprob", "executa" sau similar**
|
||||||
|
|
||||||
### 3. Implementation cu TeamCreate (Agent Teams)
|
### 2. Implementation cu TeamCreate (Agent Teams)
|
||||||
|
|
||||||
Folosim **TeamCreate** (team agents), NU superpowers subagents. Diferenta:
|
Folosim **TeamCreate** (team agents), NU superpowers subagents. Diferenta:
|
||||||
- **TeamCreate**: agenti independenti cu task list partajat, comunicare directa intre ei, context propriu
|
- **TeamCreate**: agenti independenti cu task list partajat, comunicare directa intre ei, context propriu
|
||||||
@@ -72,23 +66,21 @@ Folosim **TeamCreate** (team agents), NU superpowers subagents. Diferenta:
|
|||||||
|
|
||||||
#### Teammate-ul de verificare (Task 3):
|
#### Teammate-ul de verificare (Task 3):
|
||||||
1. Navigheaza la fiecare pagina cu Playwright MCP la 375x812 (mobile) si 1440x900 (desktop)
|
1. Navigheaza la fiecare pagina cu Playwright MCP la 375x812 (mobile) si 1440x900 (desktop)
|
||||||
2. Screenshot-uri → `screenshots/after/`
|
2. **Foloseste browser_snapshot** (NU screenshot-uri) pentru a inspecta structura DOM
|
||||||
3. Compara `after/` vs `preview/` vizual
|
3. Verifica ca implementarea respecta fiecare punct din preview-ul aprobat (structura coloane, bold, dots, filtre etc.)
|
||||||
4. Raporteaza discrepante la team lead
|
4. Raporteaza discrepante concrete la team lead (ce e diferit fata de preview)
|
||||||
5. Verifica ca desktop-ul ramane neschimbat
|
5. NU salveaza screenshot-uri after/
|
||||||
|
|
||||||
#### Bucla de corectie (responsabilitatea team lead-ului):
|
#### Bucla de corectie (responsabilitatea team lead-ului):
|
||||||
1. Dupa ce verify-agent raporteaza, **team lead-ul analizeaza discrepantele**
|
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
|
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
|
3. Dupa fix, spawneaza din nou verify-agent pentru re-verificare
|
||||||
4. **Repeta bucla** pana cand toate verificarile trec (after ≈ preview)
|
4. **Repeta bucla** pana cand toate verificarile trec (implementare ≈ preview)
|
||||||
5. Abia atunci declara task-ul complet
|
5. Abia atunci declara task-ul complet
|
||||||
|
|
||||||
```
|
```
|
||||||
screenshots/
|
screenshots/
|
||||||
├── before/ # Starea inainte de modificari
|
└── preview/ # Mockup-uri Markdown aprobate de user (referinta pentru verificare)
|
||||||
├── preview/ # Mockup-uri aprobate de user
|
|
||||||
└── after/ # Verificare post-implementare
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Principii
|
### Principii
|
||||||
|
|||||||
@@ -1,75 +0,0 @@
|
|||||||
SPECIFICATIE PROIECT - IMPORT COMENZI WEB IN ROA ORACLE
|
|
||||||
Data: 5 martie 2026
|
|
||||||
|
|
||||||
================================================================================
|
|
||||||
DESCRIERE SCOP
|
|
||||||
================================================================================
|
|
||||||
|
|
||||||
Implementarea unui sistem automat de import a comenzilor de pe platforme web
|
|
||||||
(GoMag si altele) in sistemul ERP ROA Oracle. Sistemul va prelua comenzi,
|
|
||||||
va realiza mapari de articole, va converte unitati de masura si va crea
|
|
||||||
comenzi in ROA automat.
|
|
||||||
|
|
||||||
|
|
||||||
================================================================================
|
|
||||||
DELIVERABLES
|
|
||||||
================================================================================
|
|
||||||
|
|
||||||
1. Logica de import completa in baza de date ROA Oracle
|
|
||||||
2. Orchestrator automat (cron job) pentru sincronizare comenzi
|
|
||||||
3. Interfata web de configurare mapari SKU-uri
|
|
||||||
4. Suport pentru articole compuse (mapari complexe)
|
|
||||||
5. Conversii unitati de masura intre platforme
|
|
||||||
6. Documentatie tehnica si handover
|
|
||||||
7. Support 3 luni pentru bug fixes
|
|
||||||
|
|
||||||
|
|
||||||
================================================================================
|
|
||||||
EFORTURI SI COSTURI
|
|
||||||
================================================================================
|
|
||||||
|
|
||||||
Lucrat deja: 20h 1,200 EUR
|
|
||||||
De lucrat: 60h 3,600 EUR
|
|
||||||
Support 3 luni: 24h 1,440 EUR
|
|
||||||
|
|
||||||
TOTAL IMPLEMENTARE: 80h 4,800 EUR
|
|
||||||
TOTAL CU SUPPORT: 104h 6,240 EUR
|
|
||||||
|
|
||||||
Tarif orar: 60 EUR/h
|
|
||||||
|
|
||||||
|
|
||||||
================================================================================
|
|
||||||
INCLUS IN PRET
|
|
||||||
================================================================================
|
|
||||||
|
|
||||||
- Analiza si integrare cu baza de date client
|
|
||||||
- Testare completa cu date reale
|
|
||||||
- Integrare in sistemul ROA Oracle
|
|
||||||
- Validari si controale de integritate
|
|
||||||
- Documentation si training
|
|
||||||
- Support de 3 luni pentru probleme critice
|
|
||||||
|
|
||||||
|
|
||||||
================================================================================
|
|
||||||
CONDITII GENERALE
|
|
||||||
================================================================================
|
|
||||||
|
|
||||||
Duratie proiect: 2-4 saptamani
|
|
||||||
Payment terms: 50% avans, 50% la finalizare
|
|
||||||
Garantie: 3 luni (bug fixes gratuit)
|
|
||||||
Suport suplimentar: 60 EUR/h (dupa perioada garantie)
|
|
||||||
|
|
||||||
Buffer estimare: 50% (pentru integrare ROA + incertitudini)
|
|
||||||
|
|
||||||
|
|
||||||
================================================================================
|
|
||||||
RESPONSABILITATI CLIENT
|
|
||||||
================================================================================
|
|
||||||
|
|
||||||
- Acces la baza de date client si ROA Oracle
|
|
||||||
- Accesul la comenzile din platforma web
|
|
||||||
- Clarificarea logicii maparii articole compuse
|
|
||||||
- Testing si validare in mediu pilot
|
|
||||||
|
|
||||||
|
|
||||||
================================================================================
|
|
||||||
@@ -15,3 +15,7 @@ async def dashboard(request: Request):
|
|||||||
@router.get("/missing-skus", response_class=HTMLResponse)
|
@router.get("/missing-skus", response_class=HTMLResponse)
|
||||||
async def missing_skus_page(request: Request):
|
async def missing_skus_page(request: Request):
|
||||||
return templates.TemplateResponse("missing_skus.html", {"request": request})
|
return templates.TemplateResponse("missing_skus.html", {"request": request})
|
||||||
|
|
||||||
|
@router.get("/settings", response_class=HTMLResponse)
|
||||||
|
async def settings_page(request: Request):
|
||||||
|
return templates.TemplateResponse("settings.html", {"request": request})
|
||||||
|
|||||||
@@ -24,6 +24,11 @@ class AppSettingsUpdate(BaseModel):
|
|||||||
transport_codmat: str = ""
|
transport_codmat: str = ""
|
||||||
transport_vat: str = "21"
|
transport_vat: str = "21"
|
||||||
discount_codmat: str = ""
|
discount_codmat: str = ""
|
||||||
|
transport_id_pol: str = ""
|
||||||
|
discount_vat: str = "21"
|
||||||
|
discount_id_pol: str = ""
|
||||||
|
id_pol: str = ""
|
||||||
|
id_sectie: str = ""
|
||||||
|
|
||||||
|
|
||||||
# API endpoints
|
# API endpoints
|
||||||
@@ -332,11 +337,12 @@ async def dashboard_orders(page: int = 1, per_page: int = 50,
|
|||||||
period_days=0 without dates means all time.
|
period_days=0 without dates means all time.
|
||||||
"""
|
"""
|
||||||
is_uninvoiced_filter = (status == "UNINVOICED")
|
is_uninvoiced_filter = (status == "UNINVOICED")
|
||||||
|
is_invoiced_filter = (status == "INVOICED")
|
||||||
|
|
||||||
# For UNINVOICED: fetch all IMPORTED orders, then filter post-invoice-check
|
# For UNINVOICED/INVOICED: fetch all IMPORTED orders, then filter post-invoice-check
|
||||||
fetch_status = "IMPORTED" if is_uninvoiced_filter else status
|
fetch_status = "IMPORTED" if (is_uninvoiced_filter or is_invoiced_filter) else status
|
||||||
fetch_per_page = 10000 if is_uninvoiced_filter else per_page
|
fetch_per_page = 10000 if (is_uninvoiced_filter or is_invoiced_filter) else per_page
|
||||||
fetch_page = 1 if is_uninvoiced_filter else page
|
fetch_page = 1 if (is_uninvoiced_filter or is_invoiced_filter) else page
|
||||||
|
|
||||||
result = await sqlite_service.get_orders(
|
result = await sqlite_service.get_orders(
|
||||||
page=fetch_page, per_page=fetch_per_page, search=search,
|
page=fetch_page, per_page=fetch_per_page, search=search,
|
||||||
@@ -391,6 +397,8 @@ async def dashboard_orders(page: int = 1, per_page: int = 50,
|
|||||||
1 for o in all_orders
|
1 for o in all_orders
|
||||||
if o.get("status") in ("IMPORTED", "ALREADY_IMPORTED") and not o.get("invoice")
|
if o.get("status") in ("IMPORTED", "ALREADY_IMPORTED") and not o.get("invoice")
|
||||||
))
|
))
|
||||||
|
imported_total = counts.get("imported_all") or counts.get("imported", 0)
|
||||||
|
counts["facturate"] = max(0, imported_total - counts["nefacturate"])
|
||||||
counts.setdefault("total", counts.get("imported", 0) + counts.get("skipped", 0) + counts.get("error", 0))
|
counts.setdefault("total", counts.get("imported", 0) + counts.get("skipped", 0) + counts.get("error", 0))
|
||||||
|
|
||||||
# For UNINVOICED filter: apply server-side filtering + pagination
|
# For UNINVOICED filter: apply server-side filtering + pagination
|
||||||
@@ -403,6 +411,15 @@ async def dashboard_orders(page: int = 1, per_page: int = 50,
|
|||||||
result["page"] = page
|
result["page"] = page
|
||||||
result["per_page"] = per_page
|
result["per_page"] = per_page
|
||||||
result["pages"] = (total + per_page - 1) // per_page if total > 0 else 0
|
result["pages"] = (total + per_page - 1) // per_page if total > 0 else 0
|
||||||
|
elif is_invoiced_filter:
|
||||||
|
filtered = [o for o in all_orders if o.get("status") in ("IMPORTED", "ALREADY_IMPORTED") and o.get("invoice")]
|
||||||
|
total = len(filtered)
|
||||||
|
offset = (page - 1) * per_page
|
||||||
|
result["orders"] = filtered[offset:offset + per_page]
|
||||||
|
result["total"] = total
|
||||||
|
result["page"] = page
|
||||||
|
result["per_page"] = per_page
|
||||||
|
result["pages"] = (total + per_page - 1) // per_page if total > 0 else 0
|
||||||
|
|
||||||
# Reshape response
|
# Reshape response
|
||||||
return {
|
return {
|
||||||
@@ -445,6 +462,11 @@ async def get_app_settings():
|
|||||||
"transport_codmat": settings.get("transport_codmat", ""),
|
"transport_codmat": settings.get("transport_codmat", ""),
|
||||||
"transport_vat": settings.get("transport_vat", "21"),
|
"transport_vat": settings.get("transport_vat", "21"),
|
||||||
"discount_codmat": settings.get("discount_codmat", ""),
|
"discount_codmat": settings.get("discount_codmat", ""),
|
||||||
|
"transport_id_pol": settings.get("transport_id_pol", ""),
|
||||||
|
"discount_vat": settings.get("discount_vat", "19"),
|
||||||
|
"discount_id_pol": settings.get("discount_id_pol", ""),
|
||||||
|
"id_pol": settings.get("id_pol", ""),
|
||||||
|
"id_sectie": settings.get("id_sectie", ""),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -454,4 +476,9 @@ async def update_app_settings(config: AppSettingsUpdate):
|
|||||||
await sqlite_service.set_app_setting("transport_codmat", config.transport_codmat)
|
await sqlite_service.set_app_setting("transport_codmat", config.transport_codmat)
|
||||||
await sqlite_service.set_app_setting("transport_vat", config.transport_vat)
|
await sqlite_service.set_app_setting("transport_vat", config.transport_vat)
|
||||||
await sqlite_service.set_app_setting("discount_codmat", config.discount_codmat)
|
await sqlite_service.set_app_setting("discount_codmat", config.discount_codmat)
|
||||||
|
await sqlite_service.set_app_setting("transport_id_pol", config.transport_id_pol)
|
||||||
|
await sqlite_service.set_app_setting("discount_vat", config.discount_vat)
|
||||||
|
await sqlite_service.set_app_setting("discount_id_pol", config.discount_id_pol)
|
||||||
|
await sqlite_service.set_app_setting("id_pol", config.id_pol)
|
||||||
|
await sqlite_service.set_app_setting("id_sectie", config.id_sectie)
|
||||||
return {"success": True}
|
return {"success": True}
|
||||||
|
|||||||
@@ -80,22 +80,29 @@ def build_articles_json(items, order=None, settings=None) -> str:
|
|||||||
|
|
||||||
# Transport as article with quantity +1
|
# Transport as article with quantity +1
|
||||||
if order.delivery_cost > 0 and transport_codmat:
|
if order.delivery_cost > 0 and transport_codmat:
|
||||||
articles.append({
|
article_dict = {
|
||||||
"sku": transport_codmat,
|
"sku": transport_codmat,
|
||||||
"quantity": "1",
|
"quantity": "1",
|
||||||
"price": str(order.delivery_cost),
|
"price": str(order.delivery_cost),
|
||||||
"vat": transport_vat,
|
"vat": transport_vat,
|
||||||
"name": "Transport"
|
"name": "Transport"
|
||||||
})
|
}
|
||||||
|
if settings.get("transport_id_pol"):
|
||||||
|
article_dict["id_pol"] = settings["transport_id_pol"]
|
||||||
|
articles.append(article_dict)
|
||||||
# Discount total with quantity -1 (positive price)
|
# Discount total with quantity -1 (positive price)
|
||||||
if order.discount_total > 0 and discount_codmat:
|
if order.discount_total > 0 and discount_codmat:
|
||||||
articles.append({
|
discount_vat = settings.get("discount_vat", "19")
|
||||||
|
article_dict = {
|
||||||
"sku": discount_codmat,
|
"sku": discount_codmat,
|
||||||
"quantity": "-1",
|
"quantity": "-1",
|
||||||
"price": str(order.discount_total),
|
"price": str(order.discount_total),
|
||||||
"vat": "21",
|
"vat": discount_vat,
|
||||||
"name": "Discount"
|
"name": "Discount"
|
||||||
})
|
}
|
||||||
|
if settings.get("discount_id_pol"):
|
||||||
|
article_dict["id_pol"] = settings["discount_id_pol"]
|
||||||
|
articles.append(article_dict)
|
||||||
|
|
||||||
return json.dumps(articles)
|
return json.dumps(articles)
|
||||||
|
|
||||||
|
|||||||
@@ -232,8 +232,10 @@ async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Step 2d: Pre-validate prices for importable articles
|
# Step 2d: Pre-validate prices for importable articles
|
||||||
id_pol = id_pol or settings.ID_POL
|
# Load app settings (for transport/discount CODMAT config AND id_pol/id_sectie override)
|
||||||
id_sectie = id_sectie or settings.ID_SECTIE
|
app_settings = await sqlite_service.get_app_settings()
|
||||||
|
id_pol = id_pol or int(app_settings.get("id_pol") or 0) or settings.ID_POL
|
||||||
|
id_sectie = id_sectie or int(app_settings.get("id_sectie") or 0) or settings.ID_SECTIE
|
||||||
logger.info(f"Sync params: ID_POL={id_pol}, ID_SECTIE={id_sectie}")
|
logger.info(f"Sync params: ID_POL={id_pol}, ID_SECTIE={id_sectie}")
|
||||||
_log_line(run_id, f"Parametri import: ID_POL={id_pol}, ID_SECTIE={id_sectie}")
|
_log_line(run_id, f"Parametri import: ID_POL={id_pol}, ID_SECTIE={id_sectie}")
|
||||||
if id_pol and (truly_importable or already_in_roa):
|
if id_pol and (truly_importable or already_in_roa):
|
||||||
@@ -331,9 +333,6 @@ async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None
|
|||||||
imported_count = 0
|
imported_count = 0
|
||||||
error_count = 0
|
error_count = 0
|
||||||
|
|
||||||
# Load app settings for transport/discount CODMAT config
|
|
||||||
app_settings = await sqlite_service.get_app_settings()
|
|
||||||
|
|
||||||
for i, order in enumerate(truly_importable):
|
for i, order in enumerate(truly_importable):
|
||||||
shipping_name, billing_name, customer, payment_method, delivery_method = _derive_customer_info(order)
|
shipping_name, billing_name, customer, payment_method, delivery_method = _derive_customer_info(order)
|
||||||
|
|
||||||
|
|||||||
@@ -100,6 +100,9 @@ body {
|
|||||||
padding-right: 1.5rem;
|
padding-right: 1.5rem;
|
||||||
padding-bottom: 1.5rem;
|
padding-bottom: 1.5rem;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
|
max-width: 1280px;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Cards ───────────────────────────────────────── */
|
/* ── Cards ───────────────────────────────────────── */
|
||||||
@@ -140,6 +143,10 @@ body {
|
|||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Zebra striping */
|
||||||
|
.table tbody tr:nth-child(even) td { background-color: #f7f8fa; }
|
||||||
|
.table-hover tbody tr:hover td { background-color: #eef2ff !important; }
|
||||||
|
|
||||||
/* ── Badges — soft pill style ────────────────────── */
|
/* ── Badges — soft pill style ────────────────────── */
|
||||||
.badge {
|
.badge {
|
||||||
font-size: 0.8125rem;
|
font-size: 0.8125rem;
|
||||||
@@ -736,3 +743,22 @@ tr.mapping-deleted td {
|
|||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
color: #374151;
|
color: #374151;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Clickable CODMAT link in order detail modal */
|
||||||
|
.codmat-link { color: #0d6efd; cursor: pointer; text-decoration: underline; }
|
||||||
|
.codmat-link:hover { color: #0a58ca; }
|
||||||
|
|
||||||
|
/* Mobile article flat list in order detail modal */
|
||||||
|
.detail-item-flat { font-size: 0.85rem; }
|
||||||
|
.detail-item-flat .dif-item { }
|
||||||
|
.detail-item-flat .dif-item:nth-child(even) .dif-row { background: #f7f8fa; }
|
||||||
|
.detail-item-flat .dif-row {
|
||||||
|
display: flex; align-items: baseline; gap: 0.5rem;
|
||||||
|
padding: 0.2rem 0.75rem; flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.dif-sku { font-family: monospace; font-size: 0.78rem; color: #6b7280; }
|
||||||
|
.dif-name { font-weight: 500; flex: 1; }
|
||||||
|
.dif-qty { white-space: nowrap; color: #6b7280; }
|
||||||
|
.dif-val { white-space: nowrap; font-weight: 600; }
|
||||||
|
.dif-codmat-link { color: #0d6efd; cursor: pointer; font-size: 0.78rem; font-family: monospace; }
|
||||||
|
.dif-codmat-link:hover { color: #0a58ca; text-decoration: underline; }
|
||||||
|
|||||||
@@ -281,42 +281,29 @@ async function loadDashOrders() {
|
|||||||
if (el('cntImp')) el('cntImp').textContent = c.imported_all || c.imported || 0;
|
if (el('cntImp')) el('cntImp').textContent = c.imported_all || c.imported || 0;
|
||||||
if (el('cntSkip')) el('cntSkip').textContent = c.skipped || 0;
|
if (el('cntSkip')) el('cntSkip').textContent = c.skipped || 0;
|
||||||
if (el('cntErr')) el('cntErr').textContent = c.error || c.errors || 0;
|
if (el('cntErr')) el('cntErr').textContent = c.error || c.errors || 0;
|
||||||
if (el('cntNef')) el('cntNef').textContent = c.uninvoiced || c.nefacturate || 0;
|
if (el('cntFact')) el('cntFact').textContent = c.facturate || 0;
|
||||||
|
if (el('cntNef')) el('cntNef').textContent = c.nefacturate || c.uninvoiced || 0;
|
||||||
|
|
||||||
const tbody = document.getElementById('dashOrdersBody');
|
const tbody = document.getElementById('dashOrdersBody');
|
||||||
const orders = data.orders || [];
|
const orders = data.orders || [];
|
||||||
|
|
||||||
if (orders.length === 0) {
|
if (orders.length === 0) {
|
||||||
tbody.innerHTML = '<tr><td colspan="8" class="text-center text-muted py-3">Nicio comanda</td></tr>';
|
tbody.innerHTML = '<tr><td colspan="9" class="text-center text-muted py-3">Nicio comanda</td></tr>';
|
||||||
} else {
|
} else {
|
||||||
tbody.innerHTML = orders.map(o => {
|
tbody.innerHTML = orders.map(o => {
|
||||||
const dateStr = fmtDate(o.order_date);
|
const dateStr = fmtDate(o.order_date);
|
||||||
const statusBadge = orderStatusBadge(o.status);
|
|
||||||
|
|
||||||
// Invoice info
|
|
||||||
let invoiceBadge = '';
|
|
||||||
if (o.status !== 'IMPORTED' && o.status !== 'ALREADY_IMPORTED') {
|
|
||||||
invoiceBadge = '<span class="text-muted">-</span>';
|
|
||||||
} else if (o.invoice && o.invoice.facturat) {
|
|
||||||
invoiceBadge = `<span style="color:#16a34a;font-weight:500">Facturat</span>`;
|
|
||||||
if (o.invoice.serie_act || o.invoice.numar_act) {
|
|
||||||
invoiceBadge += `<br><small>${esc(o.invoice.serie_act || '')} ${esc(String(o.invoice.numar_act || ''))}</small>`;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
invoiceBadge = `<span style="color:#dc2626">Nefacturat</span>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const orderTotal = o.order_total != null ? Number(o.order_total).toFixed(2) : '-';
|
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>${statusDot(o.status)}</td>
|
||||||
<td>${dateStr}</td>
|
<td class="text-nowrap">${dateStr}</td>
|
||||||
${renderClientCell(o)}
|
${renderClientCell(o)}
|
||||||
|
<td><code>${esc(o.order_number)}</code></td>
|
||||||
<td>${o.items_count || 0}</td>
|
<td>${o.items_count || 0}</td>
|
||||||
<td class="text-end">${orderTotal}</td>
|
<td class="text-end text-muted">${fmtCost(o.delivery_cost)}</td>
|
||||||
<td class="text-nowrap">${statusDot(o.status)} ${statusLabelText(o.status)}</td>
|
<td class="text-end text-muted">${fmtCost(o.discount_total)}</td>
|
||||||
<td>${o.id_comanda || '-'}</td>
|
<td class="text-end fw-bold">${orderTotal}</td>
|
||||||
<td>${invoiceBadge}</td>
|
<td class="text-center">${invoiceDot(o)}</td>
|
||||||
</tr>`;
|
</tr>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
}
|
}
|
||||||
@@ -339,8 +326,8 @@ async function loadDashOrders() {
|
|||||||
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>
|
||||||
<span class="grow truncate">${esc(name)}</span>
|
<span class="grow truncate fw-bold">${esc(name)}</span>
|
||||||
<span class="text-nowrap">x${o.items_count || 0}${totalStr ? ' · ' + totalStr : ''}</span>
|
<span class="text-nowrap">x${o.items_count || 0}${totalStr ? ' · <strong>' + totalStr + '</strong>' : ''}</span>
|
||||||
</div>`;
|
</div>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
}
|
}
|
||||||
@@ -352,7 +339,8 @@ async function loadDashOrders() {
|
|||||||
{ label: 'Imp.', count: c.imported_all || c.imported || 0, value: 'IMPORTED', active: activeStatus === 'IMPORTED', colorClass: 'fc-green' },
|
{ label: 'Imp.', count: c.imported_all || c.imported || 0, value: 'IMPORTED', active: activeStatus === 'IMPORTED', colorClass: 'fc-green' },
|
||||||
{ label: 'Omise', count: c.skipped || 0, value: 'SKIPPED', active: activeStatus === 'SKIPPED', colorClass: 'fc-yellow' },
|
{ label: 'Omise', count: c.skipped || 0, value: 'SKIPPED', active: activeStatus === 'SKIPPED', colorClass: 'fc-yellow' },
|
||||||
{ label: 'Erori', count: c.error || c.errors || 0, value: 'ERROR', active: activeStatus === 'ERROR', colorClass: 'fc-red' },
|
{ label: 'Erori', count: c.error || c.errors || 0, value: 'ERROR', active: activeStatus === 'ERROR', colorClass: 'fc-red' },
|
||||||
{ label: 'Nefact.', count: c.uninvoiced || c.nefacturate || 0, value: 'UNINVOICED', active: activeStatus === 'UNINVOICED', colorClass: 'fc-neutral' }
|
{ label: 'Fact.', count: c.facturate || 0, value: 'INVOICED', active: activeStatus === 'INVOICED', colorClass: 'fc-green' },
|
||||||
|
{ label: 'Nefact.', count: c.nefacturate || c.uninvoiced || 0, value: 'UNINVOICED', active: activeStatus === 'UNINVOICED', colorClass: 'fc-red' }
|
||||||
], (val) => {
|
], (val) => {
|
||||||
document.querySelectorAll('.filter-pill[data-status]').forEach(b => b.classList.remove('active'));
|
document.querySelectorAll('.filter-pill[data-status]').forEach(b => b.classList.remove('active'));
|
||||||
const pill = document.querySelector(`.filter-pill[data-status="${val}"]`);
|
const pill = document.querySelector(`.filter-pill[data-status="${val}"]`);
|
||||||
@@ -380,7 +368,7 @@ async function loadDashOrders() {
|
|||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
document.getElementById('dashOrdersBody').innerHTML =
|
document.getElementById('dashOrdersBody').innerHTML =
|
||||||
`<tr><td colspan="8" class="text-center text-danger">${esc(err.message)}</td></tr>`;
|
`<tr><td colspan="9" class="text-center text-danger">${esc(err.message)}</td></tr>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -402,9 +390,9 @@ function renderClientCell(order) {
|
|||||||
const billing = (order.billing_name || '').trim();
|
const billing = (order.billing_name || '').trim();
|
||||||
const isDiff = order.is_different_person && billing && shipping !== billing;
|
const isDiff = order.is_different_person && billing && shipping !== billing;
|
||||||
if (isDiff) {
|
if (isDiff) {
|
||||||
return `<td class="tooltip-cont" data-tooltip="Cont: ${escHtml(billing)}">${escHtml(shipping)} <sup style="color:#6b7280;font-size:0.65rem">▲</sup></td>`;
|
return `<td class="tooltip-cont fw-bold" data-tooltip="Cont: ${escHtml(billing)}">${escHtml(shipping)} <sup style="color:#6b7280;font-size:0.65rem">▲</sup></td>`;
|
||||||
}
|
}
|
||||||
return `<td>${escHtml(shipping || billing || '\u2014')}</td>`;
|
return `<td class="fw-bold">${escHtml(shipping || billing || '\u2014')}</td>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Helper functions ──────────────────────────────
|
// ── Helper functions ──────────────────────────────
|
||||||
@@ -428,6 +416,10 @@ function escHtml(s) {
|
|||||||
// Alias kept for backward compat with inline handlers in modal
|
// Alias kept for backward compat with inline handlers in modal
|
||||||
function esc(s) { return escHtml(s); }
|
function esc(s) { return escHtml(s); }
|
||||||
|
|
||||||
|
function fmtCost(v) {
|
||||||
|
return v > 0 ? Number(v).toFixed(2) : '–';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function statusLabelText(status) {
|
function statusLabelText(status) {
|
||||||
switch ((status || '').toUpperCase()) {
|
switch ((status || '').toUpperCase()) {
|
||||||
@@ -449,6 +441,12 @@ function orderStatusBadge(status) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function invoiceDot(order) {
|
||||||
|
if (order.status !== 'IMPORTED' && order.status !== 'ALREADY_IMPORTED') return '–';
|
||||||
|
if (order.invoice && order.invoice.facturat) return '<span class="dot dot-green" title="Facturat"></span>';
|
||||||
|
return '<span class="dot dot-red" title="Nefacturat"></span>';
|
||||||
|
}
|
||||||
|
|
||||||
function renderCodmatCell(item) {
|
function renderCodmatCell(item) {
|
||||||
if (!item.codmat_details || item.codmat_details.length === 0) {
|
if (!item.codmat_details || item.codmat_details.length === 0) {
|
||||||
return `<code>${esc(item.codmat || '-')}</code>`;
|
return `<code>${esc(item.codmat || '-')}</code>`;
|
||||||
@@ -473,16 +471,12 @@ async function openDashOrderDetail(orderNumber) {
|
|||||||
document.getElementById('detailIdPartener').textContent = '-';
|
document.getElementById('detailIdPartener').textContent = '-';
|
||||||
document.getElementById('detailIdAdresaFact').textContent = '-';
|
document.getElementById('detailIdAdresaFact').textContent = '-';
|
||||||
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="6" class="text-center">Se incarca...</td></tr>';
|
||||||
document.getElementById('detailError').style.display = 'none';
|
document.getElementById('detailError').style.display = 'none';
|
||||||
const detailItemsTotal = document.getElementById('detailItemsTotal');
|
const detailItemsTotal = document.getElementById('detailItemsTotal');
|
||||||
if (detailItemsTotal) detailItemsTotal.textContent = '-';
|
if (detailItemsTotal) detailItemsTotal.textContent = '-';
|
||||||
const detailOrderTotal = document.getElementById('detailOrderTotal');
|
const detailOrderTotal = document.getElementById('detailOrderTotal');
|
||||||
if (detailOrderTotal) detailOrderTotal.textContent = '-';
|
if (detailOrderTotal) detailOrderTotal.textContent = '-';
|
||||||
const deliveryWrap = document.getElementById('detailDeliveryWrap');
|
|
||||||
if (deliveryWrap) deliveryWrap.style.display = 'none';
|
|
||||||
const discountWrap = document.getElementById('detailDiscountWrap');
|
|
||||||
if (discountWrap) discountWrap.style.display = 'none';
|
|
||||||
const mobileContainer = document.getElementById('detailItemsMobile');
|
const mobileContainer = document.getElementById('detailItemsMobile');
|
||||||
if (mobileContainer) mobileContainer.innerHTML = '';
|
if (mobileContainer) mobileContainer.innerHTML = '';
|
||||||
|
|
||||||
@@ -514,25 +508,15 @@ async function openDashOrderDetail(orderNumber) {
|
|||||||
document.getElementById('detailError').style.display = '';
|
document.getElementById('detailError').style.display = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show delivery cost
|
|
||||||
const dlvWrap = document.getElementById('detailDeliveryWrap');
|
|
||||||
const dlvEl = document.getElementById('detailDeliveryCost');
|
const dlvEl = document.getElementById('detailDeliveryCost');
|
||||||
if (order.delivery_cost && Number(order.delivery_cost) > 0) {
|
if (dlvEl) dlvEl.textContent = order.delivery_cost > 0 ? Number(order.delivery_cost).toFixed(2) + ' lei' : '–';
|
||||||
if (dlvEl) dlvEl.textContent = Number(order.delivery_cost).toFixed(2) + ' lei';
|
|
||||||
if (dlvWrap) dlvWrap.style.display = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show discount
|
|
||||||
const dscWrap = document.getElementById('detailDiscountWrap');
|
|
||||||
const dscEl = document.getElementById('detailDiscount');
|
const dscEl = document.getElementById('detailDiscount');
|
||||||
if (order.discount_total && Number(order.discount_total) > 0) {
|
if (dscEl) dscEl.textContent = order.discount_total > 0 ? '–' + Number(order.discount_total).toFixed(2) + ' lei' : '–';
|
||||||
if (dscEl) dscEl.textContent = '-' + Number(order.discount_total).toFixed(2) + ' lei';
|
|
||||||
if (dscWrap) dscWrap.style.display = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
const items = data.items || [];
|
const items = data.items || [];
|
||||||
if (items.length === 0) {
|
if (items.length === 0) {
|
||||||
document.getElementById('detailItemsBody').innerHTML = '<tr><td colspan="8" class="text-center text-muted">Niciun articol</td></tr>';
|
document.getElementById('detailItemsBody').innerHTML = '<tr><td colspan="6" class="text-center text-muted">Niciun articol</td></tr>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -541,53 +525,38 @@ async function openDashOrderDetail(orderNumber) {
|
|||||||
document.getElementById('detailItemsTotal').textContent = itemsTotal.toFixed(2) + ' lei';
|
document.getElementById('detailItemsTotal').textContent = itemsTotal.toFixed(2) + ' lei';
|
||||||
document.getElementById('detailOrderTotal').textContent = order.order_total != null ? Number(order.order_total).toFixed(2) + ' lei' : '-';
|
document.getElementById('detailOrderTotal').textContent = order.order_total != null ? Number(order.order_total).toFixed(2) + ' lei' : '-';
|
||||||
|
|
||||||
// Mobile article cards
|
// Mobile article flat list
|
||||||
const mobileContainer = document.getElementById('detailItemsMobile');
|
const mobileContainer = document.getElementById('detailItemsMobile');
|
||||||
if (mobileContainer) {
|
if (mobileContainer) {
|
||||||
mobileContainer.innerHTML = items.map(item => {
|
mobileContainer.innerHTML = '<div class="detail-item-flat">' + items.map((item, idx) => {
|
||||||
let statusLabel = '';
|
const codmatList = item.codmat_details?.length
|
||||||
switch (item.mapping_status) {
|
? item.codmat_details.map(d => `<span class="dif-codmat-link" onclick="openQuickMap('${esc(item.sku)}','${esc(item.product_name||'')}','${esc(orderNumber)}')">${esc(d.codmat)}</span>`).join(' ')
|
||||||
case 'mapped': statusLabel = '<span class="badge bg-success">Mapat</span>'; break;
|
: `<span class="dif-codmat-link" onclick="openQuickMap('${esc(item.sku)}','${esc(item.product_name||'')}','${esc(orderNumber)}')">${esc(item.codmat || '–')}</span>`;
|
||||||
case 'direct': statusLabel = '<span class="badge bg-info">Direct</span>'; break;
|
const valoare = (Number(item.price || 0) * Number(item.quantity || 0)).toFixed(2);
|
||||||
case 'missing': statusLabel = '<span class="badge bg-warning">Lipsa</span>'; break;
|
return `<div class="dif-item">
|
||||||
default: statusLabel = '<span class="badge bg-secondary">?</span>';
|
<div class="dif-row">
|
||||||
}
|
<span class="dif-sku">${esc(item.sku)}</span>
|
||||||
const codmat = item.codmat || '-';
|
${codmatList}
|
||||||
return `<div class="detail-item-card">
|
</div>
|
||||||
<div class="card-sku">${esc(item.sku)}</div>
|
<div class="dif-row">
|
||||||
<div class="card-name">${esc(item.product_name || '-')}</div>
|
<span class="dif-name">${esc(item.product_name || '–')}</span>
|
||||||
<div class="card-details">
|
<span class="dif-qty">x${item.quantity || 0}</span>
|
||||||
<span>x${item.quantity || 0}</span>
|
<span class="dif-val">${valoare} lei</span>
|
||||||
<span>${item.price != null ? Number(item.price).toFixed(2) : '-'} lei</span>
|
|
||||||
<span><code>${esc(codmat)}</code></span>
|
|
||||||
<span>${statusLabel}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
}).join('');
|
}).join('') + '</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById('detailItemsBody').innerHTML = items.map(item => {
|
document.getElementById('detailItemsBody').innerHTML = items.map(item => {
|
||||||
let statusBadge;
|
const valoare = (Number(item.price || 0) * Number(item.quantity || 0)).toFixed(2);
|
||||||
switch (item.mapping_status) {
|
const codmatCell = `<span class="codmat-link" onclick="openQuickMap('${esc(item.sku)}', '${esc(item.product_name || '')}', '${esc(orderNumber)}')" title="Click pentru mapare">${renderCodmatCell(item)}</span>`;
|
||||||
case 'mapped': statusBadge = '<span class="badge bg-success">Mapat</span>'; break;
|
|
||||||
case 'direct': statusBadge = '<span class="badge bg-info">Direct</span>'; break;
|
|
||||||
case 'missing': statusBadge = '<span class="badge bg-warning">Lipsa</span>'; break;
|
|
||||||
default: statusBadge = '<span class="badge bg-secondary">?</span>';
|
|
||||||
}
|
|
||||||
|
|
||||||
const action = item.mapping_status === 'missing'
|
|
||||||
? `<a href="#" class="btn-map-icon" onclick="openQuickMap('${esc(item.sku)}', '${esc(item.product_name || '')}', '${esc(orderNumber)}'); return false;" title="Mapeaza"><i class="bi bi-link-45deg"></i></a>`
|
|
||||||
: '';
|
|
||||||
|
|
||||||
return `<tr>
|
return `<tr>
|
||||||
<td><code>${esc(item.sku)}</code></td>
|
<td><code>${esc(item.sku)}</code></td>
|
||||||
<td>${esc(item.product_name || '-')}</td>
|
<td>${esc(item.product_name || '-')}</td>
|
||||||
|
<td>${codmatCell}</td>
|
||||||
<td>${item.quantity || 0}</td>
|
<td>${item.quantity || 0}</td>
|
||||||
<td>${item.price != null ? Number(item.price).toFixed(2) : '-'}</td>
|
<td>${item.price != null ? Number(item.price).toFixed(2) : '-'}</td>
|
||||||
<td>${item.vat != null ? Number(item.vat).toFixed(2) : '-'}</td>
|
<td class="text-end">${valoare}</td>
|
||||||
<td>${renderCodmatCell(item)}</td>
|
|
||||||
<td>${statusBadge}</td>
|
|
||||||
<td>${action}</td>
|
|
||||||
</tr>`;
|
</tr>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -730,80 +699,3 @@ async function saveQuickMapping() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── App Settings ─────────────────────────────────
|
|
||||||
|
|
||||||
let settAcTimeout = null;
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
loadAppSettings();
|
|
||||||
wireSettingsAutocomplete('settTransportCodmat', 'settTransportAc');
|
|
||||||
wireSettingsAutocomplete('settDiscountCodmat', 'settDiscountAc');
|
|
||||||
});
|
|
||||||
|
|
||||||
async function loadAppSettings() {
|
|
||||||
try {
|
|
||||||
const res = await fetch('/api/settings');
|
|
||||||
const data = await res.json();
|
|
||||||
const el = (id) => document.getElementById(id);
|
|
||||||
if (el('settTransportCodmat')) el('settTransportCodmat').value = data.transport_codmat || '';
|
|
||||||
if (el('settTransportVat')) el('settTransportVat').value = data.transport_vat || '21';
|
|
||||||
if (el('settDiscountCodmat')) el('settDiscountCodmat').value = data.discount_codmat || '';
|
|
||||||
} catch (err) {
|
|
||||||
console.error('loadAppSettings error:', err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function saveAppSettings() {
|
|
||||||
const transport_codmat = document.getElementById('settTransportCodmat')?.value?.trim() || '';
|
|
||||||
const transport_vat = document.getElementById('settTransportVat')?.value || '21';
|
|
||||||
const discount_codmat = document.getElementById('settDiscountCodmat')?.value?.trim() || '';
|
|
||||||
try {
|
|
||||||
const res = await fetch('/api/settings', {
|
|
||||||
method: 'PUT',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ transport_codmat, transport_vat, discount_codmat })
|
|
||||||
});
|
|
||||||
const data = await res.json();
|
|
||||||
if (data.success) {
|
|
||||||
alert('Setari salvate!');
|
|
||||||
} else {
|
|
||||||
alert('Eroare: ' + JSON.stringify(data));
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
alert('Eroare salvare setari: ' + err.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function wireSettingsAutocomplete(inputId, dropdownId) {
|
|
||||||
const input = document.getElementById(inputId);
|
|
||||||
const dropdown = document.getElementById(dropdownId);
|
|
||||||
if (!input || !dropdown) return;
|
|
||||||
|
|
||||||
input.addEventListener('input', () => {
|
|
||||||
clearTimeout(settAcTimeout);
|
|
||||||
settAcTimeout = setTimeout(async () => {
|
|
||||||
const q = input.value.trim();
|
|
||||||
if (q.length < 2) { dropdown.classList.add('d-none'); return; }
|
|
||||||
try {
|
|
||||||
const res = await fetch(`/api/articles/search?q=${encodeURIComponent(q)}`);
|
|
||||||
const data = await res.json();
|
|
||||||
if (!data.results || data.results.length === 0) { dropdown.classList.add('d-none'); return; }
|
|
||||||
dropdown.innerHTML = data.results.map(r =>
|
|
||||||
`<div class="autocomplete-item" onmousedown="settSelectArticle('${inputId}', '${dropdownId}', '${esc(r.codmat)}')">
|
|
||||||
<span class="codmat">${esc(r.codmat)}</span> — <span class="denumire">${esc(r.denumire)}</span>
|
|
||||||
</div>`
|
|
||||||
).join('');
|
|
||||||
dropdown.classList.remove('d-none');
|
|
||||||
} catch { dropdown.classList.add('d-none'); }
|
|
||||||
}, 250);
|
|
||||||
});
|
|
||||||
|
|
||||||
input.addEventListener('blur', () => {
|
|
||||||
setTimeout(() => dropdown.classList.add('d-none'), 200);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function settSelectArticle(inputId, dropdownId, codmat) {
|
|
||||||
document.getElementById(inputId).value = codmat;
|
|
||||||
document.getElementById(dropdownId).classList.add('d-none');
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ let currentQmOrderNumber = '';
|
|||||||
let ordersSortColumn = 'order_date';
|
let ordersSortColumn = 'order_date';
|
||||||
let ordersSortDirection = 'desc';
|
let ordersSortDirection = 'desc';
|
||||||
|
|
||||||
|
function fmtCost(v) {
|
||||||
|
return v > 0 ? Number(v).toFixed(2) : '–';
|
||||||
|
}
|
||||||
|
|
||||||
function fmtDuration(startedAt, finishedAt) {
|
function fmtDuration(startedAt, finishedAt) {
|
||||||
if (!startedAt || !finishedAt) return '-';
|
if (!startedAt || !finishedAt) return '-';
|
||||||
const diffMs = new Date(finishedAt) - new Date(startedAt);
|
const diffMs = new Date(finishedAt) - new Date(startedAt);
|
||||||
@@ -151,19 +155,21 @@ 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="7" class="text-center text-muted py-3">Nicio comanda</td></tr>';
|
tbody.innerHTML = '<tr><td colspan="9" 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) : '-';
|
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>${statusDot(o.status)}</td>
|
||||||
<td>${(ordersPage - 1) * 50 + i + 1}</td>
|
<td>${(ordersPage - 1) * 50 + i + 1}</td>
|
||||||
<td>${dateStr}</td>
|
<td class="text-nowrap">${dateStr}</td>
|
||||||
<td><code>${esc(o.order_number)}</code></td>
|
<td><code>${esc(o.order_number)}</code></td>
|
||||||
<td>${esc(o.customer_name)}</td>
|
<td class="fw-bold">${esc(o.customer_name)}</td>
|
||||||
<td>${o.items_count || 0}</td>
|
<td>${o.items_count || 0}</td>
|
||||||
<td class="text-end">${orderTotal}</td>
|
<td class="text-end text-muted">${fmtCost(o.delivery_cost)}</td>
|
||||||
<td class="text-nowrap">${statusDot(o.status)} ${logStatusText(o.status)}</td>
|
<td class="text-end text-muted">${fmtCost(o.discount_total)}</td>
|
||||||
|
<td class="text-end fw-bold">${orderTotal}</td>
|
||||||
</tr>`;
|
</tr>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
}
|
}
|
||||||
@@ -185,8 +191,8 @@ async function loadRunOrders(runId, statusFilter, page) {
|
|||||||
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>
|
||||||
<span class="grow truncate">${esc(o.customer_name || '—')}</span>
|
<span class="grow truncate fw-bold">${esc(o.customer_name || '—')}</span>
|
||||||
<span class="text-nowrap">x${o.items_count || 0}${totalStr ? ' · ' + totalStr : ''}</span>
|
<span class="text-nowrap">x${o.items_count || 0}${totalStr ? ' · <strong>' + totalStr + '</strong>' : ''}</span>
|
||||||
</div>`;
|
</div>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
}
|
}
|
||||||
@@ -222,7 +228,7 @@ async function loadRunOrders(runId, statusFilter, page) {
|
|||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
document.getElementById('runOrdersBody').innerHTML =
|
document.getElementById('runOrdersBody').innerHTML =
|
||||||
`<tr><td colspan="6" class="text-center text-danger">${esc(err.message)}</td></tr>`;
|
`<tr><td colspan="9" class="text-center text-danger">${esc(err.message)}</td></tr>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -318,16 +324,12 @@ async function openOrderDetail(orderNumber) {
|
|||||||
document.getElementById('detailIdPartener').textContent = '-';
|
document.getElementById('detailIdPartener').textContent = '-';
|
||||||
document.getElementById('detailIdAdresaFact').textContent = '-';
|
document.getElementById('detailIdAdresaFact').textContent = '-';
|
||||||
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="6" class="text-center">Se incarca...</td></tr>';
|
||||||
document.getElementById('detailError').style.display = 'none';
|
document.getElementById('detailError').style.display = 'none';
|
||||||
const detailItemsTotal = document.getElementById('detailItemsTotal');
|
const detailItemsTotal = document.getElementById('detailItemsTotal');
|
||||||
if (detailItemsTotal) detailItemsTotal.textContent = '-';
|
if (detailItemsTotal) detailItemsTotal.textContent = '-';
|
||||||
const detailOrderTotal = document.getElementById('detailOrderTotal');
|
const detailOrderTotal = document.getElementById('detailOrderTotal');
|
||||||
if (detailOrderTotal) detailOrderTotal.textContent = '-';
|
if (detailOrderTotal) detailOrderTotal.textContent = '-';
|
||||||
const deliveryWrap = document.getElementById('detailDeliveryWrap');
|
|
||||||
if (deliveryWrap) deliveryWrap.style.display = 'none';
|
|
||||||
const discountWrap = document.getElementById('detailDiscountWrap');
|
|
||||||
if (discountWrap) discountWrap.style.display = 'none';
|
|
||||||
const mobileContainer = document.getElementById('detailItemsMobile');
|
const mobileContainer = document.getElementById('detailItemsMobile');
|
||||||
if (mobileContainer) mobileContainer.innerHTML = '';
|
if (mobileContainer) mobileContainer.innerHTML = '';
|
||||||
|
|
||||||
@@ -359,25 +361,15 @@ async function openOrderDetail(orderNumber) {
|
|||||||
document.getElementById('detailError').style.display = '';
|
document.getElementById('detailError').style.display = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show delivery cost
|
|
||||||
const dlvWrap = document.getElementById('detailDeliveryWrap');
|
|
||||||
const dlvEl = document.getElementById('detailDeliveryCost');
|
const dlvEl = document.getElementById('detailDeliveryCost');
|
||||||
if (order.delivery_cost && Number(order.delivery_cost) > 0) {
|
if (dlvEl) dlvEl.textContent = order.delivery_cost > 0 ? Number(order.delivery_cost).toFixed(2) + ' lei' : '–';
|
||||||
if (dlvEl) dlvEl.textContent = Number(order.delivery_cost).toFixed(2) + ' lei';
|
|
||||||
if (dlvWrap) dlvWrap.style.display = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show discount
|
|
||||||
const dscWrap = document.getElementById('detailDiscountWrap');
|
|
||||||
const dscEl = document.getElementById('detailDiscount');
|
const dscEl = document.getElementById('detailDiscount');
|
||||||
if (order.discount_total && Number(order.discount_total) > 0) {
|
if (dscEl) dscEl.textContent = order.discount_total > 0 ? '–' + Number(order.discount_total).toFixed(2) + ' lei' : '–';
|
||||||
if (dscEl) dscEl.textContent = '-' + Number(order.discount_total).toFixed(2) + ' lei';
|
|
||||||
if (dscWrap) dscWrap.style.display = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
const items = data.items || [];
|
const items = data.items || [];
|
||||||
if (items.length === 0) {
|
if (items.length === 0) {
|
||||||
document.getElementById('detailItemsBody').innerHTML = '<tr><td colspan="8" class="text-center text-muted">Niciun articol</td></tr>';
|
document.getElementById('detailItemsBody').innerHTML = '<tr><td colspan="6" class="text-center text-muted">Niciun articol</td></tr>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -386,53 +378,38 @@ async function openOrderDetail(orderNumber) {
|
|||||||
document.getElementById('detailItemsTotal').textContent = itemsTotal.toFixed(2) + ' lei';
|
document.getElementById('detailItemsTotal').textContent = itemsTotal.toFixed(2) + ' lei';
|
||||||
document.getElementById('detailOrderTotal').textContent = order.order_total != null ? Number(order.order_total).toFixed(2) + ' lei' : '-';
|
document.getElementById('detailOrderTotal').textContent = order.order_total != null ? Number(order.order_total).toFixed(2) + ' lei' : '-';
|
||||||
|
|
||||||
// Mobile article cards
|
// Mobile article flat list
|
||||||
const mobileContainer = document.getElementById('detailItemsMobile');
|
const mobileContainer = document.getElementById('detailItemsMobile');
|
||||||
if (mobileContainer) {
|
if (mobileContainer) {
|
||||||
mobileContainer.innerHTML = items.map(item => {
|
mobileContainer.innerHTML = '<div class="detail-item-flat">' + items.map((item, idx) => {
|
||||||
let statusLabel = '';
|
const codmatList = item.codmat_details?.length
|
||||||
switch (item.mapping_status) {
|
? item.codmat_details.map(d => `<span class="dif-codmat-link" onclick="openQuickMap('${esc(item.sku)}','${esc(item.product_name||'')}','${esc(orderNumber)}')">${esc(d.codmat)}</span>`).join(' ')
|
||||||
case 'mapped': statusLabel = '<span class="badge bg-success">Mapat</span>'; break;
|
: `<span class="dif-codmat-link" onclick="openQuickMap('${esc(item.sku)}','${esc(item.product_name||'')}','${esc(orderNumber)}')">${esc(item.codmat || '–')}</span>`;
|
||||||
case 'direct': statusLabel = '<span class="badge bg-info">Direct</span>'; break;
|
const valoare = (Number(item.price || 0) * Number(item.quantity || 0)).toFixed(2);
|
||||||
case 'missing': statusLabel = '<span class="badge bg-warning">Lipsa</span>'; break;
|
return `<div class="dif-item">
|
||||||
default: statusLabel = '<span class="badge bg-secondary">?</span>';
|
<div class="dif-row">
|
||||||
}
|
<span class="dif-sku">${esc(item.sku)}</span>
|
||||||
const codmat = item.codmat || '-';
|
${codmatList}
|
||||||
return `<div class="detail-item-card">
|
</div>
|
||||||
<div class="card-sku">${esc(item.sku)}</div>
|
<div class="dif-row">
|
||||||
<div class="card-name">${esc(item.product_name || '-')}</div>
|
<span class="dif-name">${esc(item.product_name || '–')}</span>
|
||||||
<div class="card-details">
|
<span class="dif-qty">x${item.quantity || 0}</span>
|
||||||
<span>x${item.quantity || 0}</span>
|
<span class="dif-val">${valoare} lei</span>
|
||||||
<span>${item.price != null ? Number(item.price).toFixed(2) : '-'} lei</span>
|
|
||||||
<span><code>${esc(codmat)}</code></span>
|
|
||||||
<span>${statusLabel}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
}).join('');
|
}).join('') + '</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById('detailItemsBody').innerHTML = items.map(item => {
|
document.getElementById('detailItemsBody').innerHTML = items.map(item => {
|
||||||
let statusBadge;
|
const valoare = (Number(item.price || 0) * Number(item.quantity || 0)).toFixed(2);
|
||||||
switch (item.mapping_status) {
|
const codmatCell = `<span class="codmat-link" onclick="openQuickMap('${esc(item.sku)}', '${esc(item.product_name || '')}', '${esc(orderNumber)}')" title="Click pentru mapare">${renderCodmatCell(item)}</span>`;
|
||||||
case 'mapped': statusBadge = '<span class="badge bg-success">Mapat</span>'; break;
|
|
||||||
case 'direct': statusBadge = '<span class="badge bg-info">Direct</span>'; break;
|
|
||||||
case 'missing': statusBadge = '<span class="badge bg-warning">Lipsa</span>'; break;
|
|
||||||
default: statusBadge = '<span class="badge bg-secondary">?</span>';
|
|
||||||
}
|
|
||||||
|
|
||||||
const action = item.mapping_status === 'missing'
|
|
||||||
? `<a href="#" class="btn-map-icon" onclick="openQuickMap('${esc(item.sku)}', '${esc(item.product_name || '')}', '${esc(orderNumber)}'); return false;" title="Mapeaza"><i class="bi bi-link-45deg"></i></a>`
|
|
||||||
: '';
|
|
||||||
|
|
||||||
return `<tr>
|
return `<tr>
|
||||||
<td><code>${esc(item.sku)}</code></td>
|
<td><code>${esc(item.sku)}</code></td>
|
||||||
<td>${esc(item.product_name || '-')}</td>
|
<td>${esc(item.product_name || '-')}</td>
|
||||||
|
<td>${codmatCell}</td>
|
||||||
<td>${item.quantity || 0}</td>
|
<td>${item.quantity || 0}</td>
|
||||||
<td>${item.price != null ? Number(item.price).toFixed(2) : '-'}</td>
|
<td>${item.price != null ? Number(item.price).toFixed(2) : '-'}</td>
|
||||||
<td>${item.vat != null ? Number(item.vat).toFixed(2) : '-'}</td>
|
<td class="text-end">${valoare}</td>
|
||||||
<td>${renderCodmatCell(item)}</td>
|
|
||||||
<td>${statusBadge}</td>
|
|
||||||
<td>${action}</td>
|
|
||||||
</tr>`;
|
</tr>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
101
api/app/static/js/settings.js
Normal file
101
api/app/static/js/settings.js
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
let settAcTimeout = null;
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
loadSettings();
|
||||||
|
wireAutocomplete('settTransportCodmat', 'settTransportAc');
|
||||||
|
wireAutocomplete('settDiscountCodmat', 'settDiscountAc');
|
||||||
|
});
|
||||||
|
|
||||||
|
async function loadSettings() {
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/settings');
|
||||||
|
const data = await res.json();
|
||||||
|
const el = (id) => document.getElementById(id);
|
||||||
|
if (el('settTransportCodmat')) el('settTransportCodmat').value = data.transport_codmat || '';
|
||||||
|
if (el('settTransportVat')) el('settTransportVat').value = data.transport_vat || '21';
|
||||||
|
if (el('settTransportIdPol')) el('settTransportIdPol').value = data.transport_id_pol || '';
|
||||||
|
if (el('settDiscountCodmat')) el('settDiscountCodmat').value = data.discount_codmat || '';
|
||||||
|
if (el('settDiscountVat')) el('settDiscountVat').value = data.discount_vat || '19';
|
||||||
|
if (el('settDiscountIdPol')) el('settDiscountIdPol').value = data.discount_id_pol || '';
|
||||||
|
if (el('settIdPol')) el('settIdPol').value = data.id_pol || '';
|
||||||
|
if (el('settIdSectie')) el('settIdSectie').value = data.id_sectie || '';
|
||||||
|
} catch (err) {
|
||||||
|
console.error('loadSettings error:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveSettings() {
|
||||||
|
const el = (id) => document.getElementById(id);
|
||||||
|
const payload = {
|
||||||
|
transport_codmat: el('settTransportCodmat')?.value?.trim() || '',
|
||||||
|
transport_vat: el('settTransportVat')?.value || '21',
|
||||||
|
transport_id_pol: el('settTransportIdPol')?.value?.trim() || '',
|
||||||
|
discount_codmat: el('settDiscountCodmat')?.value?.trim() || '',
|
||||||
|
discount_vat: el('settDiscountVat')?.value || '19',
|
||||||
|
discount_id_pol: el('settDiscountIdPol')?.value?.trim() || '',
|
||||||
|
id_pol: el('settIdPol')?.value?.trim() || '',
|
||||||
|
id_sectie: el('settIdSectie')?.value?.trim() || '',
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/settings', {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
const resultEl = document.getElementById('settSaveResult');
|
||||||
|
if (data.success) {
|
||||||
|
if (resultEl) { resultEl.textContent = 'Salvat!'; resultEl.style.color = '#16a34a'; }
|
||||||
|
setTimeout(() => { if (resultEl) resultEl.textContent = ''; }, 3000);
|
||||||
|
} else {
|
||||||
|
if (resultEl) { resultEl.textContent = 'Eroare: ' + JSON.stringify(data); resultEl.style.color = '#dc2626'; }
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
const resultEl = document.getElementById('settSaveResult');
|
||||||
|
if (resultEl) { resultEl.textContent = 'Eroare: ' + err.message; resultEl.style.color = '#dc2626'; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function wireAutocomplete(inputId, dropdownId) {
|
||||||
|
const input = document.getElementById(inputId);
|
||||||
|
const dropdown = document.getElementById(dropdownId);
|
||||||
|
if (!input || !dropdown) return;
|
||||||
|
|
||||||
|
input.addEventListener('input', () => {
|
||||||
|
clearTimeout(settAcTimeout);
|
||||||
|
settAcTimeout = setTimeout(async () => {
|
||||||
|
const q = input.value.trim();
|
||||||
|
if (q.length < 2) { dropdown.classList.add('d-none'); return; }
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/articles/search?q=${encodeURIComponent(q)}`);
|
||||||
|
const data = await res.json();
|
||||||
|
if (!data.results || data.results.length === 0) { dropdown.classList.add('d-none'); return; }
|
||||||
|
dropdown.innerHTML = data.results.map(r =>
|
||||||
|
`<div class="autocomplete-item" onmousedown="settSelectArticle('${inputId}', '${dropdownId}', '${escHtml(r.codmat)}')">
|
||||||
|
<span class="codmat">${escHtml(r.codmat)}</span> — <span class="denumire">${escHtml(r.denumire)}</span>
|
||||||
|
</div>`
|
||||||
|
).join('');
|
||||||
|
dropdown.classList.remove('d-none');
|
||||||
|
} catch { dropdown.classList.add('d-none'); }
|
||||||
|
}, 250);
|
||||||
|
});
|
||||||
|
|
||||||
|
input.addEventListener('blur', () => {
|
||||||
|
setTimeout(() => dropdown.classList.add('d-none'), 200);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function settSelectArticle(inputId, dropdownId, codmat) {
|
||||||
|
document.getElementById(inputId).value = codmat;
|
||||||
|
document.getElementById(dropdownId).classList.add('d-none');
|
||||||
|
}
|
||||||
|
|
||||||
|
function escHtml(s) {
|
||||||
|
if (s == null) return '';
|
||||||
|
return String(s)
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''');
|
||||||
|
}
|
||||||
16928
api/database-scripts/08_PACK_FACTURARE.pck
Normal file
16928
api/database-scripts/08_PACK_FACTURARE.pck
Normal file
File diff suppressed because it is too large
Load Diff
528
deploy.ps1
Normal file
528
deploy.ps1
Normal file
@@ -0,0 +1,528 @@
|
|||||||
|
#Requires -RunAsAdministrator
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Deploy / update GoMag Import Manager pe Windows Server cu IIS.
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
- Prima rulare: clone repo, setup venv, genereaza start.bat, configureaza IIS
|
||||||
|
- Rulari ulterioare: git pull, reinstaleaza deps, restarteaza serviciul
|
||||||
|
|
||||||
|
.PARAMETER RepoPath
|
||||||
|
Calea locala unde se cloneaza repo-ul. Default: C:\gomag-vending
|
||||||
|
|
||||||
|
.PARAMETER Port
|
||||||
|
Portul pe care ruleaza FastAPI. Default: 5003
|
||||||
|
|
||||||
|
.PARAMETER IisSiteName
|
||||||
|
Numele site-ului IIS parinte. Default: "Default Web Site"
|
||||||
|
|
||||||
|
.PARAMETER SkipIIS
|
||||||
|
Sarit configurarea IIS (util daca nu ai ARR/URLRewrite instalate inca)
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
.\deploy.ps1
|
||||||
|
.\deploy.ps1 -RepoPath "D:\apps\gomag-vending" -Port 5003
|
||||||
|
.\deploy.ps1 -SkipIIS
|
||||||
|
#>
|
||||||
|
|
||||||
|
param(
|
||||||
|
[string]$RepoPath = "C:\gomag-vending",
|
||||||
|
[int] $Port = 5003,
|
||||||
|
[string]$IisSiteName = "Default Web Site",
|
||||||
|
[switch]$SkipIIS
|
||||||
|
)
|
||||||
|
|
||||||
|
Set-StrictMode -Version Latest
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Helpers
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
function Write-Step { param([string]$msg) Write-Host "`n==> $msg" -ForegroundColor Cyan }
|
||||||
|
function Write-OK { param([string]$msg) Write-Host " [OK] $msg" -ForegroundColor Green }
|
||||||
|
function Write-Warn { param([string]$msg) Write-Host " [WARN] $msg" -ForegroundColor Yellow }
|
||||||
|
function Write-Fail { param([string]$msg) Write-Host " [FAIL] $msg" -ForegroundColor Red }
|
||||||
|
function Write-Info { param([string]$msg) Write-Host " $msg" -ForegroundColor Gray }
|
||||||
|
|
||||||
|
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# 1. Citire token Gitea
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
Write-Step "Citire token Gitea"
|
||||||
|
|
||||||
|
$TokenFile = Join-Path $ScriptDir ".gittoken"
|
||||||
|
$GitToken = ""
|
||||||
|
|
||||||
|
if (Test-Path $TokenFile) {
|
||||||
|
$GitToken = (Get-Content $TokenFile -Raw).Trim()
|
||||||
|
Write-OK "Token citit din $TokenFile"
|
||||||
|
} else {
|
||||||
|
Write-Warn ".gittoken nu exista langa deploy.ps1"
|
||||||
|
Write-Info "Creeaza fisierul $TokenFile cu token-ul tau Gitea (fara newline)"
|
||||||
|
Write-Info "Ex: echo -n 'ghp_xxxx' > .gittoken"
|
||||||
|
Write-Info ""
|
||||||
|
Write-Info "Continui fara token (merge doar daca repo-ul e public sau deja clonat)"
|
||||||
|
}
|
||||||
|
|
||||||
|
$RepoUrl = if ($GitToken) {
|
||||||
|
"https://$GitToken@gitea.romfast.ro/romfast/gomag-vending.git"
|
||||||
|
} else {
|
||||||
|
"https://gitea.romfast.ro/romfast/gomag-vending.git"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# 2. Git clone / pull
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
Write-Step "Git clone / pull"
|
||||||
|
|
||||||
|
# Verifica git instalat
|
||||||
|
if (-not (Get-Command git -ErrorAction SilentlyContinue)) {
|
||||||
|
Write-Fail "Git nu este instalat!"
|
||||||
|
Write-Info "Descarca Git for Windows de la: https://git-scm.com/download/win"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Test-Path (Join-Path $RepoPath ".git")) {
|
||||||
|
Write-Info "Repo exista, fac git pull..."
|
||||||
|
Push-Location $RepoPath
|
||||||
|
try {
|
||||||
|
# Update remote URL cu tokenul curent (in caz ca s-a schimbat)
|
||||||
|
if ($GitToken) {
|
||||||
|
git remote set-url origin $RepoUrl 2>$null
|
||||||
|
}
|
||||||
|
git pull --ff-only
|
||||||
|
Write-OK "git pull OK"
|
||||||
|
} finally {
|
||||||
|
Pop-Location
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Write-Info "Clonez in $RepoPath ..."
|
||||||
|
$ParentDir = Split-Path -Parent $RepoPath
|
||||||
|
if (-not (Test-Path $ParentDir)) {
|
||||||
|
New-Item -ItemType Directory -Path $ParentDir -Force | Out-Null
|
||||||
|
}
|
||||||
|
git clone $RepoUrl $RepoPath
|
||||||
|
Write-OK "git clone OK"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# 3. Verificare Python
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
Write-Step "Verificare Python"
|
||||||
|
|
||||||
|
$PythonCmd = $null
|
||||||
|
foreach ($candidate in @("python", "python3", "py")) {
|
||||||
|
try {
|
||||||
|
$ver = & $candidate --version 2>&1
|
||||||
|
if ($ver -match "Python 3\.(\d+)") {
|
||||||
|
$minor = [int]$Matches[1]
|
||||||
|
if ($minor -ge 11) {
|
||||||
|
$PythonCmd = $candidate
|
||||||
|
Write-OK "Python gasit: $ver ($candidate)"
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
Write-Warn "Python $ver prea vechi (necesar 3.11+)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $PythonCmd) {
|
||||||
|
Write-Fail "Python 3.11+ nu este instalat sau nu e in PATH!"
|
||||||
|
Write-Info "Descarca de la: https://www.python.org/downloads/"
|
||||||
|
Write-Info "IMPORTANT: Bifeaza 'Add Python to PATH' la instalare"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# 4. Creare venv si instalare dependinte
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
Write-Step "Virtual environment + dependinte"
|
||||||
|
|
||||||
|
$VenvDir = Join-Path $RepoPath "venv"
|
||||||
|
$VenvPip = Join-Path $VenvDir "Scripts\pip.exe"
|
||||||
|
$VenvPy = Join-Path $VenvDir "Scripts\python.exe"
|
||||||
|
$ReqFile = Join-Path $RepoPath "api\requirements.txt"
|
||||||
|
$DepsFlag = Join-Path $VenvDir ".deps_installed"
|
||||||
|
|
||||||
|
if (-not (Test-Path $VenvDir)) {
|
||||||
|
Write-Info "Creez venv..."
|
||||||
|
& $PythonCmd -m venv $VenvDir
|
||||||
|
Write-OK "venv creat"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Reinstaleaza daca requirements.txt e mai nou decat flag-ul
|
||||||
|
$needInstall = $true
|
||||||
|
if (Test-Path $DepsFlag) {
|
||||||
|
$reqTime = (Get-Item $ReqFile).LastWriteTime
|
||||||
|
$flagTime = (Get-Item $DepsFlag).LastWriteTime
|
||||||
|
if ($flagTime -ge $reqTime) { $needInstall = $false }
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($needInstall) {
|
||||||
|
Write-Info "Instalez dependinte din requirements.txt..."
|
||||||
|
& $VenvPip install --upgrade pip --quiet
|
||||||
|
& $VenvPip install -r $ReqFile
|
||||||
|
New-Item -ItemType File -Path $DepsFlag -Force | Out-Null
|
||||||
|
Write-OK "Dependinte instalate"
|
||||||
|
} else {
|
||||||
|
Write-OK "Dependinte deja up-to-date"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# 5. Detectare Oracle Home → sugestie INSTANTCLIENTPATH
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
Write-Step "Detectare Oracle"
|
||||||
|
|
||||||
|
$OracleHome = $env:ORACLE_HOME
|
||||||
|
$OracleBinPath = ""
|
||||||
|
|
||||||
|
if ($OracleHome -and (Test-Path $OracleHome)) {
|
||||||
|
$OracleBinPath = Join-Path $OracleHome "bin"
|
||||||
|
Write-OK "ORACLE_HOME detectat: $OracleHome"
|
||||||
|
Write-Info "Seteaza in api\.env: INSTANTCLIENTPATH=$OracleBinPath"
|
||||||
|
} else {
|
||||||
|
# Cauta Oracle in locatii comune
|
||||||
|
$commonPaths = @(
|
||||||
|
"C:\oracle\product\19c\dbhome_1\bin",
|
||||||
|
"C:\oracle\product\21c\dbhome_1\bin",
|
||||||
|
"C:\app\oracle\product\19.0.0\dbhome_1\bin",
|
||||||
|
"C:\oracle\instantclient_19_15",
|
||||||
|
"C:\oracle\instantclient_21_3"
|
||||||
|
)
|
||||||
|
foreach ($p in $commonPaths) {
|
||||||
|
if (Test-Path "$p\oci.dll") {
|
||||||
|
$OracleBinPath = $p
|
||||||
|
Write-OK "Oracle gasit la: $p"
|
||||||
|
Write-Info "Seteaza in api\.env: INSTANTCLIENTPATH=$p"
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (-not $OracleBinPath) {
|
||||||
|
Write-Warn "Oracle Instant Client nu a fost gasit automat"
|
||||||
|
Write-Info "Optiuni:"
|
||||||
|
Write-Info " 1. Thick mode: seteaza INSTANTCLIENTPATH=<cale_oracle_bin> in api\.env"
|
||||||
|
Write-Info " 2. Thin mode: seteaza FORCE_THIN_MODE=true in api\.env"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# 6. Creare .env din template daca lipseste
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
Write-Step "Fisier configurare api\.env"
|
||||||
|
|
||||||
|
$EnvFile = Join-Path $RepoPath "api\.env"
|
||||||
|
$EnvExample = Join-Path $RepoPath "api\.env.example"
|
||||||
|
|
||||||
|
if (-not (Test-Path $EnvFile)) {
|
||||||
|
if (Test-Path $EnvExample) {
|
||||||
|
Copy-Item $EnvExample $EnvFile
|
||||||
|
Write-OK "api\.env creat din .env.example"
|
||||||
|
|
||||||
|
# Actualizeaza TNS_ADMIN cu calea reala
|
||||||
|
$ApiDir = Join-Path $RepoPath "api"
|
||||||
|
(Get-Content $EnvFile) -replace "TNS_ADMIN=.*", "TNS_ADMIN=$ApiDir" |
|
||||||
|
Set-Content $EnvFile
|
||||||
|
|
||||||
|
# Seteaza INSTANTCLIENTPATH daca am gasit Oracle
|
||||||
|
if ($OracleBinPath) {
|
||||||
|
(Get-Content $EnvFile) -replace "INSTANTCLIENTPATH=.*", "INSTANTCLIENTPATH=$OracleBinPath" |
|
||||||
|
Set-Content $EnvFile
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Warn "IMPORTANT: Editeaza $EnvFile cu credentialele Oracle si GoMag API!"
|
||||||
|
Write-Info " ORACLE_USER, ORACLE_PASSWORD, ORACLE_DSN"
|
||||||
|
Write-Info " GOMAG_API_KEY, GOMAG_API_SHOP"
|
||||||
|
} else {
|
||||||
|
Write-Warn ".env.example nu exista, sari pasul"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Write-OK "api\.env exista deja"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# 7. Creare directoare necesare
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
Write-Step "Directoare date"
|
||||||
|
|
||||||
|
foreach ($dir in @("data", "output", "logs")) {
|
||||||
|
$fullPath = Join-Path $RepoPath $dir
|
||||||
|
if (-not (Test-Path $fullPath)) {
|
||||||
|
New-Item -ItemType Directory -Path $fullPath -Force | Out-Null
|
||||||
|
Write-OK "Creat: $dir\"
|
||||||
|
} else {
|
||||||
|
Write-OK "Exista: $dir\"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# 8. Generare start.bat
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
Write-Step "Generare start.bat"
|
||||||
|
|
||||||
|
$StartBat = Join-Path $RepoPath "start.bat"
|
||||||
|
|
||||||
|
# Citeste TNS_ADMIN si INSTANTCLIENTPATH din .env daca exista
|
||||||
|
$TnsAdmin = Join-Path $RepoPath "api"
|
||||||
|
$InstantClient = ""
|
||||||
|
if (Test-Path $EnvFile) {
|
||||||
|
Get-Content $EnvFile | ForEach-Object {
|
||||||
|
if ($_ -match "^TNS_ADMIN=(.+)") {
|
||||||
|
$TnsAdmin = $Matches[1].Trim()
|
||||||
|
}
|
||||||
|
if ($_ -match "^INSTANTCLIENTPATH=(.+)" -and $_ -notmatch "^#") {
|
||||||
|
$InstantClient = $Matches[1].Trim()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$OraclePathLine = ""
|
||||||
|
if ($InstantClient) {
|
||||||
|
$OraclePathLine = "set PATH=$InstantClient;%PATH%"
|
||||||
|
}
|
||||||
|
|
||||||
|
$StartBatContent = @"
|
||||||
|
@echo off
|
||||||
|
:: GoMag Import Manager - Windows Launcher
|
||||||
|
:: Generat de deploy.ps1 - nu edita manual, ruleaza deploy.ps1 din nou
|
||||||
|
|
||||||
|
cd /d "$RepoPath"
|
||||||
|
set TNS_ADMIN=$TnsAdmin
|
||||||
|
$OraclePathLine
|
||||||
|
|
||||||
|
echo Starting GoMag Import Manager on http://0.0.0.0:$Port (prefix /gomag)
|
||||||
|
"$VenvPy" -m uvicorn app.main:app --host 0.0.0.0 --port $Port --root-path /gomag --app-dir api
|
||||||
|
"@
|
||||||
|
|
||||||
|
Set-Content -Path $StartBat -Value $StartBatContent -Encoding UTF8
|
||||||
|
Write-OK "start.bat generat: $StartBat"
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# 9. IIS — Verificare ARR + URL Rewrite
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
Write-Step "Verificare module IIS"
|
||||||
|
|
||||||
|
if ($SkipIIS) {
|
||||||
|
Write-Warn "SkipIIS activ — configurare IIS sarita"
|
||||||
|
} else {
|
||||||
|
$ArrPath = "$env:SystemRoot\System32\inetsrv\arr.dll"
|
||||||
|
$UrlRewritePath = "$env:SystemRoot\System32\inetsrv\rewrite.dll"
|
||||||
|
|
||||||
|
$ArrOk = Test-Path $ArrPath
|
||||||
|
$UrlRwOk = Test-Path $UrlRewritePath
|
||||||
|
|
||||||
|
if ($ArrOk) {
|
||||||
|
Write-OK "Application Request Routing (ARR) instalat"
|
||||||
|
} else {
|
||||||
|
Write-Warn "ARR 3.0 NU este instalat"
|
||||||
|
Write-Info "Descarca: https://www.iis.net/downloads/microsoft/application-request-routing"
|
||||||
|
Write-Info "Sau: winget install Microsoft.ARR"
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($UrlRwOk) {
|
||||||
|
Write-OK "URL Rewrite 2.1 instalat"
|
||||||
|
} else {
|
||||||
|
Write-Warn "URL Rewrite 2.1 NU este instalat"
|
||||||
|
Write-Info "Descarca: https://www.iis.net/downloads/microsoft/url-rewrite"
|
||||||
|
Write-Info "Sau: winget install Microsoft.URLRewrite"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────
|
||||||
|
# 10. Configurare IIS — copiere web.config
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────
|
||||||
|
if ($ArrOk -and $UrlRwOk) {
|
||||||
|
Write-Step "Configurare IIS reverse proxy"
|
||||||
|
|
||||||
|
# Activeaza proxy in ARR (necesar o singura data)
|
||||||
|
try {
|
||||||
|
Import-Module WebAdministration -ErrorAction SilentlyContinue
|
||||||
|
$proxyEnabled = (Get-WebConfigurationProperty `
|
||||||
|
-pspath "MACHINE/WEBROOT/APPHOST" `
|
||||||
|
-filter "system.webServer/proxy" `
|
||||||
|
-name "enabled" `
|
||||||
|
-ErrorAction SilentlyContinue).Value
|
||||||
|
if (-not $proxyEnabled) {
|
||||||
|
Set-WebConfigurationProperty `
|
||||||
|
-pspath "MACHINE/WEBROOT/APPHOST" `
|
||||||
|
-filter "system.webServer/proxy" `
|
||||||
|
-name "enabled" `
|
||||||
|
-value $true
|
||||||
|
Write-OK "ARR proxy activat global"
|
||||||
|
} else {
|
||||||
|
Write-OK "ARR proxy deja activ"
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
Write-Warn "Nu am putut activa ARR proxy automat: $($_.Exception.Message)"
|
||||||
|
Write-Info "Activeaza manual din IIS Manager → server root → Application Request Routing Cache → Enable Proxy"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Determina wwwroot site-ului IIS
|
||||||
|
$IisRootPath = $null
|
||||||
|
try {
|
||||||
|
Import-Module WebAdministration -ErrorAction SilentlyContinue
|
||||||
|
$site = Get-Website -Name $IisSiteName -ErrorAction SilentlyContinue
|
||||||
|
if ($site) {
|
||||||
|
$IisRootPath = [System.Environment]::ExpandEnvironmentVariables($site.PhysicalPath)
|
||||||
|
Write-OK "Site IIS '$IisSiteName' gasit: $IisRootPath"
|
||||||
|
} else {
|
||||||
|
Write-Warn "Site IIS '$IisSiteName' nu a fost gasit"
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
# Fallback la locatia standard
|
||||||
|
$IisRootPath = "$env:SystemDrive\inetpub\wwwroot"
|
||||||
|
Write-Warn "WebAdministration unavailable, folosesc fallback: $IisRootPath"
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($IisRootPath) {
|
||||||
|
$SourceWebConfig = Join-Path $RepoPath "iis-web.config"
|
||||||
|
$DestWebConfig = Join-Path $IisRootPath "web.config"
|
||||||
|
|
||||||
|
if (Test-Path $SourceWebConfig) {
|
||||||
|
# Inlocuieste portul in web.config cu cel configurat
|
||||||
|
$wcContent = Get-Content $SourceWebConfig -Raw
|
||||||
|
$wcContent = $wcContent -replace "localhost:5003", "localhost:$Port"
|
||||||
|
|
||||||
|
if (Test-Path $DestWebConfig) {
|
||||||
|
# Backup web.config existent
|
||||||
|
$backup = "$DestWebConfig.bak_$(Get-Date -Format 'yyyyMMdd_HHmmss')"
|
||||||
|
Copy-Item $DestWebConfig $backup
|
||||||
|
Write-Info "Backup web.config: $backup"
|
||||||
|
}
|
||||||
|
|
||||||
|
Set-Content -Path $DestWebConfig -Value $wcContent -Encoding UTF8
|
||||||
|
Write-OK "web.config copiat in $IisRootPath"
|
||||||
|
} else {
|
||||||
|
Write-Warn "iis-web.config nu exista in repo, sarit"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Restart IIS
|
||||||
|
try {
|
||||||
|
iisreset /noforce 2>&1 | Out-Null
|
||||||
|
Write-OK "IIS restartat"
|
||||||
|
} catch {
|
||||||
|
Write-Warn "IIS restart esuat: $($_.Exception.Message)"
|
||||||
|
Write-Info "Ruleaza manual: iisreset"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Write-Warn "IIS nu e configurat complet — instaleaza ARR si URL Rewrite, apoi ruleaza deploy.ps1 din nou"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# 11. Serviciu Windows (NSSM sau Task Scheduler)
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
Write-Step "Serviciu Windows"
|
||||||
|
|
||||||
|
$ServiceName = "GoMagVending"
|
||||||
|
$NssmExe = ""
|
||||||
|
|
||||||
|
# Cauta NSSM
|
||||||
|
foreach ($p in @("nssm", "C:\nssm\win64\nssm.exe", "C:\tools\nssm\nssm.exe")) {
|
||||||
|
if (Get-Command $p -ErrorAction SilentlyContinue) {
|
||||||
|
$NssmExe = $p
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($NssmExe) {
|
||||||
|
Write-Info "NSSM gasit: $NssmExe"
|
||||||
|
|
||||||
|
$existingService = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue
|
||||||
|
|
||||||
|
if ($existingService) {
|
||||||
|
Write-Info "Serviciu existent, restarteaza..."
|
||||||
|
& $NssmExe restart $ServiceName
|
||||||
|
Write-OK "Serviciu $ServiceName restartat"
|
||||||
|
} else {
|
||||||
|
Write-Info "Instalez serviciu $ServiceName cu NSSM..."
|
||||||
|
& $NssmExe install $ServiceName (Join-Path $RepoPath "start.bat")
|
||||||
|
& $NssmExe set $ServiceName AppDirectory $RepoPath
|
||||||
|
& $NssmExe set $ServiceName DisplayName "GoMag Vending Import Manager"
|
||||||
|
& $NssmExe set $ServiceName Description "Import comenzi web GoMag -> ROA Oracle"
|
||||||
|
& $NssmExe set $ServiceName Start SERVICE_AUTO_START
|
||||||
|
& $NssmExe set $ServiceName AppStdout (Join-Path $RepoPath "logs\service_stdout.log")
|
||||||
|
& $NssmExe set $ServiceName AppStderr (Join-Path $RepoPath "logs\service_stderr.log")
|
||||||
|
& $NssmExe set $ServiceName AppRotateFiles 1
|
||||||
|
& $NssmExe set $ServiceName AppRotateOnline 1
|
||||||
|
& $NssmExe set $ServiceName AppRotateBytes 10485760
|
||||||
|
& $NssmExe start $ServiceName
|
||||||
|
Write-OK "Serviciu $ServiceName instalat si pornit"
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
# Fallback: Task Scheduler
|
||||||
|
Write-Warn "NSSM nu este instalat"
|
||||||
|
Write-Info "Optiuni:"
|
||||||
|
Write-Info " 1. Descarca NSSM: https://nssm.cc/download si pune nssm.exe in PATH"
|
||||||
|
Write-Info " 2. Sau foloseste Task Scheduler (creat mai jos)"
|
||||||
|
|
||||||
|
# Verifica daca task-ul exista deja
|
||||||
|
$taskExists = Get-ScheduledTask -TaskName $ServiceName -ErrorAction SilentlyContinue
|
||||||
|
|
||||||
|
if (-not $taskExists) {
|
||||||
|
Write-Info "Creez Task Scheduler task '$ServiceName'..."
|
||||||
|
try {
|
||||||
|
$action = New-ScheduledTaskAction -Execute (Join-Path $RepoPath "start.bat")
|
||||||
|
$trigger = New-ScheduledTaskTrigger -AtStartup
|
||||||
|
$settings = New-ScheduledTaskSettingsSet `
|
||||||
|
-ExecutionTimeLimit (New-TimeSpan -Days 365) `
|
||||||
|
-RestartCount 3 `
|
||||||
|
-RestartInterval (New-TimeSpan -Minutes 1)
|
||||||
|
$principal = New-ScheduledTaskPrincipal `
|
||||||
|
-UserId "SYSTEM" `
|
||||||
|
-LogonType ServiceAccount `
|
||||||
|
-RunLevel Highest
|
||||||
|
|
||||||
|
Register-ScheduledTask `
|
||||||
|
-TaskName $ServiceName `
|
||||||
|
-Action $action `
|
||||||
|
-Trigger $trigger `
|
||||||
|
-Settings $settings `
|
||||||
|
-Principal $principal `
|
||||||
|
-Description "GoMag Vending Import Manager" `
|
||||||
|
-Force | Out-Null
|
||||||
|
|
||||||
|
Start-ScheduledTask -TaskName $ServiceName
|
||||||
|
Write-OK "Task Scheduler '$ServiceName' creat si pornit"
|
||||||
|
} catch {
|
||||||
|
Write-Warn "Task Scheduler esuat: $($_.Exception.Message)"
|
||||||
|
Write-Info "Porneste manual: .\start.bat"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
# Restart task
|
||||||
|
Stop-ScheduledTask -TaskName $ServiceName -ErrorAction SilentlyContinue
|
||||||
|
Start-ScheduledTask -TaskName $ServiceName
|
||||||
|
Write-OK "Task '$ServiceName' restartat"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Sumar final
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "══════════════════════════════════════════════════════" -ForegroundColor Cyan
|
||||||
|
Write-Host " GoMag Vending Deploy — Sumar" -ForegroundColor Cyan
|
||||||
|
Write-Host "══════════════════════════════════════════════════════" -ForegroundColor Cyan
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host " Repo: $RepoPath" -ForegroundColor White
|
||||||
|
Write-Host " FastAPI: http://localhost:$Port/gomag" -ForegroundColor White
|
||||||
|
Write-Host " start.bat generat" -ForegroundColor White
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
if (-not (Test-Path $EnvFile)) {
|
||||||
|
Write-Host " [!] api\.env lipseste — configureaza inainte de start!" -ForegroundColor Red
|
||||||
|
} else {
|
||||||
|
Write-Host " api\.env: OK" -ForegroundColor Green
|
||||||
|
# Verifica daca mai are valori placeholder
|
||||||
|
$envContent = Get-Content $EnvFile -Raw
|
||||||
|
if ($envContent -match "your_api_key_here|USER_ORACLE|parola_oracle|TNS_ALIAS") {
|
||||||
|
Write-Host " [!] api\.env contine valori placeholder — editeaza!" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host " Acces app: http://SERVER/gomag" -ForegroundColor Cyan
|
||||||
|
Write-Host " Test local: http://localhost:$Port/gomag/health" -ForegroundColor Cyan
|
||||||
|
Write-Host ""
|
||||||
62
iis-web.config
Normal file
62
iis-web.config
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!--
|
||||||
|
IIS web.config pentru GoMag Vending — URL Rewrite + ARR Reverse Proxy
|
||||||
|
Copiat automat de deploy.ps1 in wwwroot site-ului IIS.
|
||||||
|
|
||||||
|
Prerequisite:
|
||||||
|
- Application Request Routing (ARR) 3.0
|
||||||
|
- URL Rewrite 2.1
|
||||||
|
Ambele gratuite de la iis.net.
|
||||||
|
|
||||||
|
Configuratie:
|
||||||
|
Browser → http://SERVER/gomag/...
|
||||||
|
↓
|
||||||
|
IIS (port 80)
|
||||||
|
↓ (URL Rewrite)
|
||||||
|
http://localhost:5003/...
|
||||||
|
FastAPI/uvicorn
|
||||||
|
-->
|
||||||
|
<configuration>
|
||||||
|
<system.webServer>
|
||||||
|
|
||||||
|
<!-- Activeaza proxy (ARR) -->
|
||||||
|
<proxy enabled="true" preserveHostHeader="false" reverseRewriteHostInResponseHeaders="false" />
|
||||||
|
|
||||||
|
<rewrite>
|
||||||
|
<rules>
|
||||||
|
<!--
|
||||||
|
Regula principala: /gomag/* → http://localhost:5003/*
|
||||||
|
FastAPI ruleaza cu --root-path /gomag deci stie de prefix.
|
||||||
|
-->
|
||||||
|
<rule name="GoMag Reverse Proxy" stopProcessing="true">
|
||||||
|
<match url="^gomag(.*)" />
|
||||||
|
<conditions>
|
||||||
|
<add input="{CACHE_URL}" pattern="^(https?)://" />
|
||||||
|
</conditions>
|
||||||
|
<action type="Rewrite" url="http://localhost:5003{R:1}" />
|
||||||
|
</rule>
|
||||||
|
</rules>
|
||||||
|
|
||||||
|
<!-- Rescrie Location header-ele din raspunsurile FastAPI -->
|
||||||
|
<outboundRules>
|
||||||
|
<rule name="GoMag Fix Location Header" preCondition="IsRedirect">
|
||||||
|
<match serverVariable="RESPONSE_Location" pattern="^http://localhost:5003/(.*)" />
|
||||||
|
<action type="Rewrite" value="/gomag/{R:1}" />
|
||||||
|
</rule>
|
||||||
|
<preConditions>
|
||||||
|
<preCondition name="IsRedirect">
|
||||||
|
<add input="{RESPONSE_STATUS}" pattern="3\d\d" />
|
||||||
|
</preCondition>
|
||||||
|
</preConditions>
|
||||||
|
</outboundRules>
|
||||||
|
</rewrite>
|
||||||
|
|
||||||
|
<!-- Securitate: ascunde versiunea IIS -->
|
||||||
|
<httpProtocol>
|
||||||
|
<customHeaders>
|
||||||
|
<remove name="X-Powered-By" />
|
||||||
|
</customHeaders>
|
||||||
|
</httpProtocol>
|
||||||
|
|
||||||
|
</system.webServer>
|
||||||
|
</configuration>
|
||||||
Reference in New Issue
Block a user