@router.get("/list") async def list_settings(db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)): """All settings with current values (DB overrides) and metadata for the UI.""" rows = (await db.execute( select(ApplicationSettings).where(ApplicationSettings.key.in_(SETTING_DEFS.keys())) )).scalars().all() stored = {r.key: r.value for r in rows} out = [] for key, d in SETTING_DEFS.items(): out.append({ "key": key, "label": d["label"], "category": d["category"], "type": d["type"], "value": stored.get(key, d["default"]), "default": d["default"], "is_default": key not in stored, }) return {"settings": out} @router.post("/save") async def save_setting(payload: SettingIn, db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user), _: None = Depends(require_csrf)): ok, value = _validate_value(payload.key, payload.value) if not ok: raise HTTPException(status_code=400, detail=value) row = await db.execute( select(ApplicationSettings).where(ApplicationSettings.key == payload.key)) row = row.scalar() if row: row.value = value row.description = payload.description or row.description else: db.add(ApplicationSettings(key=payload.key, value=value, description=payload.description or SETTING_DEFS[payload.key]["label"], category=SETTING_DEFS[payload.key]["category"])) await db.commit() await log_audit(db, user.id, "settings.save", "application_setting", None, {"key": payload.key}) return {"key": payload.key, "value": value} @router.post("/save-batch") async def save_batch(payload: BatchSettingsIn, db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user), _: None = Depends(require_csrf)): """Save multiple settings at once (Settings page form).""" applied, errors = {}, {} ===PHONES-IMPORT=== @router.post("/import") async def import_phones(file: UploadFile = None, db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user), _: None = Depends(require_csrf)): """Import phone numbers from CSV (phone[,label]) or TXT (one per line). Supports bulk import of 10+ numbers at once.""" if file is None: raise HTTPException(status_code=400, detail="No file uploaded") raw = await file.read() if len(raw) > 2 * 1024 * 1024: raise HTTPException(status_code=413, detail="File exceeds 2MB") try: text = raw.decode("utf-8-sig") except UnicodeDecodeError: raise HTTPException(status_code=400, detail="File must be UTF-8 text") # reject JSON-style input explicitly (CSV/TXT only per spec) stripped = text.lstrip() if stripped.startswith("{") or stripped.startswith("["): raise HTTPException(status_code=400, detail="Only CSV or TXT files are accepted") entries: list[dict] = [] invalid: list[str] = [] if "," in "\n".join(text.splitlines()[:50]) or file.filename.lower().endswith(".csv"): reader = csv.reader(io.StringIO(text)) for row in reader: if not row or not any(c.strip() for c in row): continue phone = (row[0] or "").strip() label = (row[1] or "").strip() if len(row) > 1 else "" if not phone or phone.lower().startswith(("phone", "number", "#")): continue # header if PHONE_RE.match(phone): entries.append({"phone": phone, "label": label}) else: invalid.append(phone) else: for line in text.splitlines(): phone = line.strip() if not phone or phone.lower().startswith(("phone", "number", "#")): continue if PHONE_RE.match(phone): entries.append({"phone": phone, "label": ""}) else: invalid.append(phone) if not entries: raise HTTPException(status_code=400, detail=f"No valid phone numbers found. Invalid: {invalid[:10]}") existing = set((await db.execute( select(CompanyPhoneNumber.phone).where(CompanyPhoneNumber.user_id == user.id))).scalars()) added, duplicates = 0, 0 for e in entries: if e["phone"] in existing: duplicates += 1 continue db.add(CompanyPhoneNumber(user_id=user.id, phone=e["phone"], label=e["label"])) existing.add(e["phone"]) added += 1 await db.commit() await log_audit(db, user.id, "phone.import", "company_phone", None, {"added": added, "duplicates": duplicates, "invalid": len(invalid)}) return {"added": added, "duplicates": duplicates, "invalid": invalid[:20], "total_valid": len(entries)} @router.post("/{pid}/patch")