From 0a880baef998e18a7356ca61ef3232d22cc8980f Mon Sep 17 00:00:00 2001 From: Claude Agent Date: Sun, 12 Apr 2026 22:25:32 +0000 Subject: [PATCH] =?UTF-8?q?feat(service-auto):=20phase=202=20=E2=80=94=20c?= =?UTF-8?q?omenzi=20browse,=20id=5Fsucursala,=20cache,=20migrare=20SQL?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backend: - GET /api/service-auto/comenzi cu paginare server-side, filtre dată/status - ComandaRequest.id_sucursala (Optional) + FirmaItem.id_mama - get_firme() expune id_mama din V_NOM_FIRME - callproc SP_CREEAZA_COMANDA_PROTOTIP cu 7 argumente (+ p_id_sucursala) - Cache TTL in-process: tip_deviz 24h, masini 5min Frontend: - ComenziBrowseView.vue — DataTable lazy + filtre + status badges - ComandaNoua.vue — company store integration, idSucursala computed - service-auto/stores/sharedStores.js (createCompaniesStore factory) - HamburgerMenu: secțiune Service Auto (Comenzi + Comandă Nouă) - router: /service-auto/comenzi SQL: - migrations/ff_2026_04_12_01_AUTO.sql — idempotent (COLUMNEXIST guard + CREATE OR REPLACE SP) - onboarding_roa_web.sql — versioned, parametrizat cu :SCHEMA_NAME - .claude/rules/oracle-migrations.md — convenție ff_YYYY_MM_DD_NN_MODULE.sql Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/rules/oracle-migrations.md | 71 +++++ .gitignore | 1 + .../modules/service_auto/routers/comanda.py | 27 +- .../modules/service_auto/schemas/comanda.py | 23 ++ .../service_auto/services/comanda_service.py | 115 +++++++- .../service_auto/services/lookup_service.py | 39 ++- docs/service-auto/grants-audit.md | 7 +- .../migrations/ff_2026_04_12_01_AUTO.sql | 63 ++++ docs/service-auto/onboarding_roa_web.sql | 24 ++ .../components/layout/HamburgerMenu.vue | 25 ++ src/modules/service-auto/services/api.js | 1 + .../service-auto/stores/sharedStores.js | 15 + .../service-auto/views/ComandaNoua.vue | 14 +- .../service-auto/views/ComenziBrowseView.vue | 275 ++++++++++++++++++ src/router/index.js | 6 + 15 files changed, 694 insertions(+), 12 deletions(-) create mode 100644 .claude/rules/oracle-migrations.md create mode 100644 docs/service-auto/migrations/ff_2026_04_12_01_AUTO.sql create mode 100644 docs/service-auto/onboarding_roa_web.sql create mode 100644 src/modules/service-auto/stores/sharedStores.js create mode 100644 src/modules/service-auto/views/ComenziBrowseView.vue diff --git a/.claude/rules/oracle-migrations.md b/.claude/rules/oracle-migrations.md new file mode 100644 index 0000000..f1aab01 --- /dev/null +++ b/.claude/rules/oracle-migrations.md @@ -0,0 +1,71 @@ +--- +paths: "**/*.sql,docs/service-auto/migrations/**" +--- + +# Oracle SQL Migration Script Rules + +You are an Oracle SQL migration script writer. Transform raw DDL/DML into idempotent migration scripts following these rules: + +## STRUCTURE +- Header comment: `-- brief description` (e.g. `-- adaugare coloana nom_firme.caen_revizie`) +- Body (idempotency rules below) +- Footer: `exec pack_migrare.UpdateVersiune(''); commit;` + +## IDEMPOTENCY RULES + +1. **ALTER TABLE ADD COLUMN** → wrap in: + ```sql + BEGIN + IF PACK_MIGRARE.COLUMNEXIST('TABLE','COL')=0 THEN + EXECUTE IMMEDIATE '...'; + END IF; + END; + / + ``` + +2. **CREATE OR REPLACE VIEW/PROCEDURE/FUNCTION** → keep as-is (already idempotent) + +3. **INSERT** → replace with: + ```sql + MERGE INTO table USING DUAL ON (key condition) + WHEN NOT MATCHED THEN INSERT (cols) VALUES (vals); + ``` + +4. **UPDATE** → keep as-is + +5. **CREATE TABLE** → wrap in: + ```sql + BEGIN + IF PACK_MIGRARE.OBJECTEXIST('TABLE','TABLE')=0 THEN + EXECUTE IMMEDIATE '...'; + END IF; + END; + / + ``` + +6. **DROP** → wrap in: + ```sql + BEGIN + IF PACK_MIGRARE.OBJECTEXIST('OBJ')=1 THEN + EXECUTE IMMEDIATE 'DROP...'; + END IF; + END; + / + ``` + +7. **COMMENT ON** → keep as plain DDL (not inside EXECUTE IMMEDIATE) + +8. In MERGE/INSERT: omit NULL-valued columns and CLOB columns entirely + +## FILENAME CONVENTION + +``` +ff_YYYY_MM_DD_NN_.sql +``` +- `YYYY_MM_DD` — data migrării +- `NN` — secvență 2 cifre (01, 02...) +- `` — modulul căruia îi aparține migrarea (ex: AUTO, FACTURARE, CONTAB) + +## LANGUAGE +Comments: write in Romanian. +Output: only the SQL script, no explanation. diff --git a/.gitignore b/.gitignore index 6cf2119..6b3f625 100644 --- a/.gitignore +++ b/.gitignore @@ -543,3 +543,4 @@ scripts/ralph/usage.jsonl # Service-auto reference material (local only, not in repo) docs/service-auto/*.pdf +docs/service-auto/HANDOFF.md diff --git a/backend/modules/service_auto/routers/comanda.py b/backend/modules/service_auto/routers/comanda.py index ad35800..b8131a6 100644 --- a/backend/modules/service_auto/routers/comanda.py +++ b/backend/modules/service_auto/routers/comanda.py @@ -1,15 +1,16 @@ import time -from typing import List +from datetime import date +from typing import List, Optional import oracledb -from fastapi import APIRouter, Depends, HTTPException +from fastapi import APIRouter, Depends, HTTPException, Query from shared.auth.dependencies import get_current_user from shared.auth.models import CurrentUser from shared.database.oracle_pool import oracle_pool from ..schemas.comanda import ( - ComandaRequest, ComandaResponse, + ComandaListResponse, ComandaRequest, ComandaResponse, FirmaItem, TipDevizItem, MasinaClientItem, ) from ..services.comanda_service import ComandaService @@ -51,6 +52,26 @@ async def get_masini(_: CurrentUser = Depends(get_current_user)): return await LookupService.get_masini() +@router.get("/comenzi", response_model=ComandaListResponse) +async def list_comenzi( + page: int = Query(default=1, ge=1), + per_page: int = Query(default=20, ge=1, le=100), + validat: Optional[int] = Query(default=None, ge=0, le=1), + data_de_la: Optional[date] = Query(default=None), + data_pana_la: Optional[date] = Query(default=None), + _: CurrentUser = Depends(get_current_user), +): + # NOTE: DEV_ORDL has no id_firma column — firmă filter not available at DB level. + # All comenzi in MARIUSM_AUTO schema are visible (companies 110/167/169 share schema). + return await ComandaService.get_comenzi( + page=page, + per_page=per_page, + validat=validat, + data_de_la=data_de_la, + data_pana_la=data_pana_la, + ) + + @router.post("/comenzi", response_model=ComandaResponse) async def creeaza_comanda( data: ComandaRequest, diff --git a/backend/modules/service_auto/schemas/comanda.py b/backend/modules/service_auto/schemas/comanda.py index 8fe45cd..19b3bb8 100644 --- a/backend/modules/service_auto/schemas/comanda.py +++ b/backend/modules/service_auto/schemas/comanda.py @@ -1,3 +1,5 @@ +from typing import List, Optional + from pydantic import BaseModel @@ -6,6 +8,7 @@ class ComandaRequest(BaseModel): id_masiniclient: int solicitari: str id_firma: int + id_sucursala: Optional[int] = None class ComandaResponse(BaseModel): @@ -18,6 +21,7 @@ class FirmaItem(BaseModel): id_firma: int firma: str schema_name: str + id_mama: Optional[int] = None class TipDevizItem(BaseModel): @@ -29,3 +33,22 @@ class TipDevizItem(BaseModel): class MasinaClientItem(BaseModel): id_masiniclient: int label: str # "PARTENER — MARCA MASINA, NRINMAT (ANFABRICATIE)" + + +class ComandaListItem(BaseModel): + id_ordl: int + nrord: str + datai: Optional[str] # ISO date "YYYY-MM-DD" + validat: int # 0=deschisă, 1=validată + inchis_fortat: int # 1=arhivată fără validare + id_tip: int + tip_denumire: str + vehicul: str # "PARTENER — MARCA MASINA, NRINMAT (AN)" + id_masiniclient: Optional[int] + + +class ComandaListResponse(BaseModel): + comenzi: List[ComandaListItem] + total: int + page: int + per_page: int diff --git a/backend/modules/service_auto/services/comanda_service.py b/backend/modules/service_auto/services/comanda_service.py index 4f32ca2..c7c08b9 100644 --- a/backend/modules/service_auto/services/comanda_service.py +++ b/backend/modules/service_auto/services/comanda_service.py @@ -1,12 +1,17 @@ import re -from typing import NoReturn +from datetime import date +from typing import List, NoReturn, Optional import oracledb from fastapi import HTTPException from shared.database.oracle_pool import oracle_pool -from ..schemas.comanda import ComandaRequest, ComandaResponse +from ..schemas.comanda import ( + ComandaListItem, ComandaListResponse, ComandaRequest, ComandaResponse, +) from .. import logger +_MAX_PER_PAGE = 100 + def _handle_oracle_error(e: Exception) -> NoReturn: """ @@ -81,6 +86,7 @@ class ComandaService: data.id_masiniclient, # p_id_masiniclient IN NUMBER data.solicitari, # p_solicitari IN VARCHAR2 data.id_firma, # p_id_firma IN NUMBER + data.id_sucursala, # p_id_sucursala IN NUMBER (None for parent firm) out_id_ordl, # p_id_ordl OUT NUMBER out_nrord, # p_nrord OUT VARCHAR2 ], @@ -107,3 +113,108 @@ class ComandaService: nrord=nrord, mesaj=f"Comanda {nrord} creata cu succes.", ) + + @staticmethod + async def get_comenzi( + page: int, + per_page: int, + validat: Optional[int], + data_de_la: Optional[date], + data_pana_la: Optional[date], + ) -> ComandaListResponse: + per_page = min(per_page, _MAX_PER_PAGE) + offset = (page - 1) * per_page + + where_parts = ["d.sters = 0"] + filter_params: dict = {} + + if validat is not None: + where_parts.append("d.validat = :validat") + filter_params["validat"] = validat + + if data_de_la is not None: + where_parts.append("d.datai >= :data_de_la") + filter_params["data_de_la"] = data_de_la + + if data_pana_la is not None: + # +1 day range avoids TRUNC (keeps index use on datai) + where_parts.append("d.datai < :data_pana_la + 1") + filter_params["data_pana_la"] = data_pana_la + + where_clause = " AND ".join(where_parts) + + base_from = f""" + FROM MARIUSM_AUTO.DEV_ORDL d + LEFT JOIN MARIUSM_AUTO.NOM_LUCRARI l ON d.id_lucrare = l.id_lucrare + LEFT JOIN MARIUSM_AUTO.AUTO_VMASINICLIENTI mc + ON d.id_masiniclient = mc.id_masiniclient + LEFT JOIN MARIUSM_AUTO.DEV_TIP_DEVIZ t ON d.id_tip = t.id_tip + WHERE {where_clause} + """ + + count_query = f"SELECT COUNT(*) {base_from}" + data_query = f""" + SELECT d.id_ordl, l.nrord, d.datai, d.validat, d.inchis_fortat, + d.id_tip, t.denumire, + d.id_masiniclient, mc.nrinmat, mc.marca, mc.masina, + mc.anfabricatie, mc.partener + {base_from} + ORDER BY d.datai DESC, d.id_ordl DESC + OFFSET :offset ROWS FETCH NEXT :per_page ROWS ONLY + """ + + try: + async with oracle_pool.get_connection("mariusm_test") as conn: + with conn.cursor() as cur: + cur.execute(count_query, filter_params) + total = cur.fetchone()[0] + + cur.execute( + data_query, + {**filter_params, "offset": offset, "per_page": per_page}, + ) + rows = cur.fetchall() + except oracledb.DatabaseError as e: + _handle_oracle_error(e) + + comenzi: List[ComandaListItem] = [] + for r in rows: + (id_ordl, nrord, datai, validat_val, inchis_fortat, + id_tip, tip_denumire, + id_mc, nrinmat, marca, masina, an, partener) = r + + if id_mc: + parts = [] + if marca: + parts.append(marca) + if masina: + parts.append(masina) + vehicul_str = " ".join(parts) if parts else "?" + an_str = f" ({int(an)})" if an else "" + vehicul = f"{partener or '?'} — {vehicul_str}, {nrinmat or '?'}{an_str}" + else: + vehicul = "" + + comenzi.append(ComandaListItem( + id_ordl=int(id_ordl), + nrord=nrord or "", + datai=datai.strftime("%Y-%m-%d") if datai else None, + validat=int(validat_val), + inchis_fortat=int(inchis_fortat or 0), + id_tip=int(id_tip), + tip_denumire=tip_denumire or "", + vehicul=vehicul, + id_masiniclient=int(id_mc) if id_mc else None, + )) + + logger.debug( + "service_auto.get_comenzi page=%d per_page=%d total=%d", + page, per_page, total, + ) + + return ComandaListResponse( + comenzi=comenzi, + total=total, + page=page, + per_page=per_page, + ) diff --git a/backend/modules/service_auto/services/lookup_service.py b/backend/modules/service_auto/services/lookup_service.py index fc7f324..0aa055e 100644 --- a/backend/modules/service_auto/services/lookup_service.py +++ b/backend/modules/service_auto/services/lookup_service.py @@ -3,7 +3,8 @@ Lookup data for service_auto forms — tip deviz, masini, firme. All three endpoints are read-only and infrequently changing. """ -from typing import List +import time +from typing import List, Optional, Tuple import oracledb from fastapi import HTTPException @@ -12,6 +13,23 @@ from shared.database.oracle_pool import oracle_pool from ..schemas.comanda import FirmaItem, MasinaClientItem, TipDevizItem from .. import logger +# In-memory TTL cache: key → (monotonic_timestamp, value) +_cache: dict = {} + +_TTL_TIP_DEVIZ = 86400 # 24 h — tip deviz changes only via DB migration +_TTL_MASINI = 300 # 5 min — vehicle inventory changes regularly + + +def _cache_get(key: str, ttl: float): + entry: Optional[Tuple] = _cache.get(key) + if entry and (time.monotonic() - entry[0]) < ttl: + return entry[1] + return None + + +def _cache_set(key: str, value) -> None: + _cache[key] = (time.monotonic(), value) + class LookupService: @@ -26,7 +44,7 @@ class LookupService: placeholders = ", ".join(f":id{i}" for i in range(len(company_ids))) query = f""" - SELECT id_firma, firma, schema + SELECT id_firma, firma, schema, id_mama FROM CONTAFIN_ORACLE.V_NOM_FIRME WHERE id_firma IN ({placeholders}) ORDER BY id_firma @@ -43,7 +61,7 @@ class LookupService: raise HTTPException(status_code=503, detail="Eroare la încărcarea firmelor") return [ - FirmaItem(id_firma=r[0], firma=r[1], schema_name=r[2] or "") + FirmaItem(id_firma=r[0], firma=r[1], schema_name=r[2] or "", id_mama=r[3]) for r in rows ] @@ -51,8 +69,13 @@ class LookupService: async def get_tip_deviz() -> List[TipDevizItem]: """ Returns all active tip deviz from MARIUSM_AUTO.DEV_TIP_DEVIZ. + Cached in-process for 24 h (changes only via DB migration). ROA_WEB has SELECT grant on this view. """ + cached = _cache_get("tip_deviz", _TTL_TIP_DEVIZ) + if cached is not None: + return cached + query = """ SELECT id_tip, denumire, inch_validare FROM MARIUSM_AUTO.DEV_TIP_DEVIZ @@ -67,18 +90,25 @@ class LookupService: logger.error("get_tip_deviz Oracle error", exc_info=True) raise HTTPException(status_code=503, detail="Eroare la încărcarea tipurilor de deviz") - return [ + result = [ TipDevizItem(id_tip=r[0], denumire=r[1], inch_validare=r[2] or 0) for r in rows ] + _cache_set("tip_deviz", result) + return result @staticmethod async def get_masini() -> List[MasinaClientItem]: """ Returns active masini from MARIUSM_AUTO.AUTO_VMASINICLIENTI. + Cached in-process for 5 min (vehicle inventory changes regularly). ROA_WEB has SELECT grant on this view. Label format: "PARTENER — MARCA MASINA, NRINMAT (ANFABRICATIE)" """ + cached = _cache_get("masini", _TTL_MASINI) + if cached is not None: + return cached + query = """ SELECT id_masiniclient, nrinmat, marca, masina, anfabricatie, partener FROM MARIUSM_AUTO.AUTO_VMASINICLIENTI @@ -107,4 +137,5 @@ class LookupService: label = f"{partener or '?'} — {vehicul}, {nrinmat or '?'}{an_str}" result.append(MasinaClientItem(id_masiniclient=int(id_mc), label=label)) + _cache_set("masini", result) return result diff --git a/docs/service-auto/grants-audit.md b/docs/service-auto/grants-audit.md index 0159e58..1843d79 100644 --- a/docs/service-auto/grants-audit.md +++ b/docs/service-auto/grants-audit.md @@ -117,7 +117,11 @@ GRANT EXECUTE ON FIRMA_NOUA.SP_CREEAZA_COMANDA_PROTOTIP TO ROA_WEB; GRANT SELECT ON FIRMA_NOUA.AUTO_VMASINICLIENTI TO ROA_WEB; GRANT SELECT ON FIRMA_NOUA.DEV_TIP_DEVIZ TO ROA_WEB; GRANT SELECT ON FIRMA_NOUA.CALENDAR TO ROA_WEB; --- CALENDAR folosit de shared/routes/calendar.py (period selector AppHeader) +-- CALENDAR: period selector AppHeader (shared/routes/calendar.py) +GRANT SELECT ON FIRMA_NOUA.DEV_ORDL TO ROA_WEB; +-- DEV_ORDL: GET /api/service-auto/comenzi (list comenzi) +GRANT SELECT ON FIRMA_NOUA.NOM_LUCRARI TO ROA_WEB; +-- NOM_LUCRARI: JOIN cu DEV_ORDL pentru nrord (get_comenzi) -- adaugă orice alte SP/view-uri noi apărute de la ultimul onboarding ``` @@ -197,6 +201,7 @@ ROLLBACK; | SP nou în toate schemele | `migration_YYYYMMDD_sp_noua_grants.sql` (loop V_NOM_FIRME) | 1 script per migrare | | View/tabelă nouă expusă | același pattern ca SP | 1 script per migrare | | Expunere `CALENDAR` pentru period selector | `GRANT SELECT {SCHEMA}.CALENDAR TO ROA_WEB` per schemă | 1 linie per schemă (parte din onboarding §4.1) | +| Expunere `DEV_ORDL` + `NOM_LUCRARI` pentru GET /comenzi | `GRANT SELECT {SCHEMA}.DEV_ORDL/NOM_LUCRARI TO ROA_WEB` per schemă | 2 linii per schemă (parte din onboarding §4.1) | --- diff --git a/docs/service-auto/migrations/ff_2026_04_12_01_AUTO.sql b/docs/service-auto/migrations/ff_2026_04_12_01_AUTO.sql new file mode 100644 index 0000000..b0cacfc --- /dev/null +++ b/docs/service-auto/migrations/ff_2026_04_12_01_AUTO.sql @@ -0,0 +1,63 @@ +-- adaugare coloana DEV_ORDL.id_sucursala + upgrade SP_CREEAZA_COMANDA_PROTOTIP + +-- Rulat conectat ca schema tinta (ex: MARIUSM_AUTO), O SINGURA DATA per schema + +BEGIN + IF PACK_MIGRARE.COLUMNEXIST('DEV_ORDL','ID_SUCURSALA')=0 THEN + EXECUTE IMMEDIATE 'ALTER TABLE DEV_ORDL ADD (id_sucursala NUMBER(10))'; + END IF; +END; +/ + +CREATE OR REPLACE PROCEDURE SP_CREEAZA_COMANDA_PROTOTIP( + p_tip IN NUMBER, + p_id_masiniclient IN NUMBER, + p_solicitari IN VARCHAR2, + p_id_firma IN NUMBER, + p_id_sucursala IN NUMBER DEFAULT NULL, + p_id_ordl OUT NUMBER, + p_nrord OUT VARCHAR2 +) AS + v_id_lucrare NUMBER; + v_seq NUMBER; + v_exists NUMBER; + v_now DATE := SYSDATE; +BEGIN + v_seq := SEQ_NR_LUCRARE.NEXTVAL; + p_nrord := 'P' || LPAD(p_id_firma, 2, '0') || '-' || v_seq; + + SELECT COUNT(*) INTO v_exists + FROM NOM_LUCRARI + WHERE sters = 0 AND nrord = p_nrord; + IF v_exists > 0 THEN + RAISE_APPLICATION_ERROR(-20001, + 'Mai exista o comanda cu numarul ' || p_nrord); + END IF; + + INSERT INTO NOM_LUCRARI (nrord, id_mod) + VALUES (p_nrord, 1200) + RETURNING id_lucrare INTO v_id_lucrare; + + INSERT INTO DEV_ORDL ( + an, luna, + id_lucrare, + datai, dataoraad, + id_util_ad, + id_masiniclient, + id_tip, + solicitari_client, + id_sucursala + ) VALUES ( + EXTRACT(YEAR FROM v_now), EXTRACT(MONTH FROM v_now), + v_id_lucrare, + v_now, v_now, + 0, + p_id_masiniclient, + p_tip, + p_solicitari, + p_id_sucursala + ) RETURNING id_ordl INTO p_id_ordl; +END SP_CREEAZA_COMANDA_PROTOTIP; +/ + +exec pack_migrare.UpdateVersiune('ff_2026_04_12_01_AUTO'); commit; diff --git a/docs/service-auto/onboarding_roa_web.sql b/docs/service-auto/onboarding_roa_web.sql new file mode 100644 index 0000000..d9b3ad4 --- /dev/null +++ b/docs/service-auto/onboarding_roa_web.sql @@ -0,0 +1,24 @@ +-- ============================================================================= +-- File purpose : Script de onboarding ROA_WEB pentru o schemă nouă +-- When to run : Rulat ca CONTAFIN_ORACLE după impdp pentru fiecare firmă nouă +-- Usage : Înlocuiește :SCHEMA_NAME cu schema reală (ex: MARIUSM_AUTO) +-- Version : 2026-04-12 +-- Prerequisite : ROA_WEB user creat (onboarding_roa_web_user.sql) +-- ============================================================================= + +GRANT EXECUTE ON :SCHEMA_NAME.SP_CREEAZA_COMANDA_PROTOTIP TO ROA_WEB; +GRANT SELECT ON :SCHEMA_NAME.AUTO_VMASINICLIENTI TO ROA_WEB; +GRANT SELECT ON :SCHEMA_NAME.DEV_TIP_DEVIZ TO ROA_WEB; +GRANT SELECT ON :SCHEMA_NAME.CALENDAR TO ROA_WEB; -- period selector AppHeader +GRANT SELECT ON :SCHEMA_NAME.DEV_ORDL TO ROA_WEB; -- GET /api/service-auto/comenzi +GRANT SELECT ON :SCHEMA_NAME.NOM_LUCRARI TO ROA_WEB; -- JOIN cu DEV_ORDL pentru nrord + +-- ============================================================================= +-- ROA_WEB user creation (one-time, run as SYS or CONTAFIN_ORACLE) +-- ============================================================================= +-- Rulat O SINGURĂ DATĂ la setup inițial, NU pentru fiecare firmă nouă. +-- Pentru fiecare firmă nouă se rulează doar secțiunea de GRANT-uri de mai sus. + +CREATE USER ROA_WEB IDENTIFIED BY ""; +GRANT CREATE SESSION TO ROA_WEB; +-- Fără alte privilegii sistem. Accesul la date = exclusiv prin granturi per-obiect. diff --git a/src/modules/reports/components/layout/HamburgerMenu.vue b/src/modules/reports/components/layout/HamburgerMenu.vue index 09a90eb..b625949 100644 --- a/src/modules/reports/components/layout/HamburgerMenu.vue +++ b/src/modules/reports/components/layout/HamburgerMenu.vue @@ -63,6 +63,24 @@ + + +