diff --git a/app/main.py b/app/main.py index f63f67a..32cd17b 100644 --- a/app/main.py +++ b/app/main.py @@ -18,11 +18,15 @@ from sqlalchemy import delete, select, text, update from sqlalchemy.orm import Session from starlette.responses import HTMLResponse as _HR +import urllib.request as _urllib_request +import urllib.parse as _urllib_parse +import json as _json from config import ( COOKIE_NAME, CSRF_COOKIE, GO_POOL_LOCK_TIMEOUT_SECONDS, GO_USER_LOCK_TIMEOUT_SECONDS, LOG_LEVEL, LOG_SLOW_REQUEST_MS, MAX_ACTIVE_SERVICES_PER_USER, PUBLIC_HOST, SESSION_IDLE_SECONDS, WEB_POOL_BUFFER, WEB_POOL_SIZE, + TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID, TELEGRAM_API_URL, ) from database import get_db from models import ( @@ -394,6 +398,90 @@ def admin_page(request: Request, admin: User = Depends(require_admin), db: Sessi ) + +@app.get("/api/public/services-by-category") +def public_services_by_category(db: Session = Depends(get_db)): + services = db.execute( + select(Service).where(Service.active == True).order_by(Service.name) + ).scalars().all() + categories = db.execute(select(Category).order_by(Category.name)).scalars().all() + cat_map = {c.id: c.name for c in categories} + + svc_cats: dict[int, list[str]] = {} + links = db.execute(select(ServiceCategory)).scalars().all() + for lnk in links: + svc_cats.setdefault(lnk.service_id, []).append(cat_map.get(lnk.category_id, "")) + + result: dict[str, list[dict]] = {"Без категории": []} + for svc in services: + cats = svc_cats.get(svc.id, []) + entry = {"id": svc.id, "name": svc.name} + if cats: + for cat in cats: + result.setdefault(cat, []).append(entry) + else: + result["Без категории"].append(entry) + if not result["Без категории"]: + del result["Без категории"] + return result + + +@app.post("/api/request-access") +async def request_access(request: Request, db: Session = Depends(get_db)): + try: + data = await request.json() + except Exception: + raise HTTPException(status_code=400, detail="Invalid JSON") + + name = str(data.get("name", "")).strip() + company = str(data.get("company", "")).strip() + email = str(data.get("email", "")).strip() + phone = str(data.get("phone", "")).strip() + manager = str(data.get("manager", "")).strip() + products = data.get("products", []) + + if not name or not company or not email or not phone: + raise HTTPException(status_code=422, detail="Заполните все обязательные поля") + + products_text = "" + if products: + items = "\n".join(f" • {p}" for p in products) + products_text = f"\n\n🖥 *Интересующие продукты:*\n{items}" + + divider = "━━━━━━━━━━━━━━━━━━━━━━" + manager_text = f"\n🤝 *Менеджер МОНТ:* {manager}" if manager else "" + text = ( + f"🔔 *Новый запрос доступа к полигону МОНТ*\n" + f"{divider}\n\n" + f"👤 *Имя:* {name}\n" + f"🏢 *Компания:* {company}\n" + f"📧 *Email:* {email}\n" + f"📱 *Телефон:* {phone}" + f"{manager_text}" + f"{products_text}" + ) + + if not TELEGRAM_BOT_TOKEN or not TELEGRAM_CHAT_ID: + log_event("telegram_not_configured", {}) + return {"ok": True} + + try: + payload = _json.dumps({ + "chat_id": TELEGRAM_CHAT_ID, + "text": text, + "parse_mode": "Markdown", + }).encode() + url = f"{TELEGRAM_API_URL}{TELEGRAM_BOT_TOKEN}/sendMessage" + req = _urllib_request.Request(url, data=payload, headers={"Content-Type": "application/json"}) + with _urllib_request.urlopen(req, timeout=10) as resp: + resp.read() + except Exception as e: + log_event("telegram_send_error", {"error": str(e)}) + raise HTTPException(status_code=502, detail="Ошибка отправки запроса") + + return {"ok": True} + + @app.post("/login") def login( request: Request, diff --git a/app/static/style.css b/app/static/style.css index 7e3e6c4..389860e 100644 --- a/app/static/style.css +++ b/app/static/style.css @@ -1328,3 +1328,188 @@ button { color: #6bbfff; font-weight: 600; } + +/* ========== Request Access Modal ========== */ +.access-modal-overlay { + position: fixed; + inset: 0; + background: rgba(3, 8, 18, 0.82); + backdrop-filter: blur(6px); + z-index: 9000; + display: flex; + align-items: center; + justify-content: center; + padding: 1rem; +} + +.access-modal { + background: linear-gradient(150deg, #0b1a2e 0%, #0d2040 100%); + border: 1px solid rgba(255,255,255,0.1); + border-radius: 16px; + box-shadow: 0 24px 80px rgba(0,0,0,0.65), 0 0 0 1px rgba(42,130,210,0.15); + width: 100%; + max-width: 520px; + max-height: 90vh; + overflow-y: auto; + scrollbar-width: thin; + scrollbar-color: rgba(42,130,210,0.3) transparent; +} + +.access-modal-header { + padding: 1.75rem 1.75rem 0; +} +.access-modal-title { + font-size: 1.25rem; + font-weight: 700; + color: #e0f0ff; + margin-bottom: 0.3rem; +} +.access-modal-sub { + font-size: 0.85rem; + color: rgba(160,205,238,0.65); +} + +.access-modal-body { + padding: 1.25rem 1.75rem; + display: flex; + flex-direction: column; + gap: 1rem; +} + +.access-field { + display: flex; + flex-direction: column; + gap: 0.35rem; +} +.access-field label { + font-size: 0.82rem; + font-weight: 600; + color: rgba(160,205,238,0.8); + letter-spacing: 0.02em; +} +.access-field .req { + color: #5aadee; +} +.access-field input[type=text], +.access-field input[type=email], +.access-field input[type=tel] { + background: rgba(255,255,255,0.05); + border: 1px solid rgba(255,255,255,0.12); + border-radius: 8px; + padding: 0.6rem 0.85rem; + color: #daeeff; + font-size: 0.92rem; + outline: none; + transition: border-color 0.15s, box-shadow 0.15s; +} +.access-field input[type=text]::placeholder, +.access-field input[type=email]::placeholder, +.access-field input[type=tel]::placeholder { + color: rgba(120,170,210,0.35); +} +.access-field input[type=text]:focus, +.access-field input[type=email]:focus, +.access-field input[type=tel]:focus { + border-color: rgba(42,130,210,0.55); + box-shadow: 0 0 0 3px rgba(42,130,210,0.12); +} + +.access-products-wrap { + background: rgba(255,255,255,0.03); + border: 1px solid rgba(255,255,255,0.08); + border-radius: 8px; + padding: 0.75rem; + max-height: 200px; + overflow-y: auto; + scrollbar-width: thin; + scrollbar-color: rgba(42,130,210,0.3) transparent; +} +.access-products-loading { + color: rgba(140,190,228,0.55); + font-size: 0.85rem; +} +.access-products-group { + margin-bottom: 0.6rem; +} +.access-products-group:last-child { + margin-bottom: 0; +} +.access-products-cat { + font-size: 0.75rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.06em; + color: rgba(100,165,215,0.6); + margin-bottom: 0.35rem; +} +.access-product-item { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.2rem 0; + cursor: pointer; + font-size: 0.875rem; + color: rgba(200,228,255,0.8); +} +.access-product-item input[type=checkbox] { + accent-color: #1e7dc8; + width: 14px; + height: 14px; + flex-shrink: 0; +} +.access-product-item:hover span { + color: #daeeff; +} + +.access-modal-error { + background: rgba(220,60,60,0.12); + border: 1px solid rgba(220,60,60,0.3); + border-radius: 6px; + padding: 0.55rem 0.85rem; + color: #f08080; + font-size: 0.85rem; +} + +.access-modal-footer { + display: flex; + justify-content: flex-end; + gap: 0.75rem; + padding: 0 1.75rem 1.5rem; +} + +.access-btn-cancel { + background: rgba(255,255,255,0.05); + border: 1px solid rgba(255,255,255,0.12); + border-radius: 8px; + padding: 0.6rem 1.25rem; + color: rgba(180,215,240,0.75); + font-size: 0.9rem; + font-weight: 500; + cursor: pointer; + transition: background 0.15s, border-color 0.15s; +} +.access-btn-cancel:hover { + background: rgba(255,255,255,0.09); + border-color: rgba(255,255,255,0.2); +} + +.access-btn-submit { + background: linear-gradient(135deg, #1e7dc8 0%, #1360a0 100%); + border: none; + border-radius: 8px; + padding: 0.6rem 1.5rem; + color: #fff; + font-size: 0.9rem; + font-weight: 600; + cursor: pointer; + transition: opacity 0.15s, box-shadow 0.15s; + box-shadow: 0 2px 12px rgba(20,96,160,0.4); +} +.access-btn-submit:hover:not(:disabled) { + opacity: 0.9; + box-shadow: 0 4px 18px rgba(20,96,160,0.55); +} +.access-btn-submit:disabled { + opacity: 0.55; + cursor: default; +} diff --git a/app/templates/login.html b/app/templates/login.html index 810726e..884513e 100644 --- a/app/templates/login.html +++ b/app/templates/login.html @@ -71,12 +71,157 @@ - Запросить доступ + + + +
+ +