Spaces:
Sleeping
Sleeping
| """ | |
| BluePrint Engineering Consultancy - AI-Powered Engineering OS | |
| Backend (FastAPI) - نسخة محدثة بهيكل استجابات موحد ودعم CORS | |
| """ | |
| import sys | |
| import os | |
| import base64 | |
| import io | |
| import logging | |
| import json | |
| import shutil | |
| from datetime import datetime, timedelta | |
| from typing import List, Optional | |
| from sqlalchemy import func | |
| from fastapi import FastAPI, UploadFile, File, Form, HTTPException, Depends, status | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from fastapi.responses import Response, FileResponse, JSONResponse | |
| from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm | |
| from jose import JWTError, jwt | |
| from passlib.context import CryptContext | |
| import pandas as pd | |
| import tempfile | |
| import exifread | |
| from geopy.geocoders import Nominatim | |
| import redis | |
| import json | |
| from apscheduler.schedulers.background import BackgroundScheduler | |
| from apscheduler.triggers.cron import CronTrigger | |
| import gc | |
| import sys | |
| import os | |
| gc.enable() | |
| gc.set_threshold(500, 10, 10) | |
| sys.stdout.reconfigure(encoding='utf-8') | |
| try: | |
| from azure.identity import DefaultAzureCredential | |
| from azure.keyvault.secrets import SecretClient | |
| AZURE_AVAILABLE = True | |
| except ImportError: | |
| AZURE_AVAILABLE = False | |
| from database import ( | |
| init_db, SessionLocal, | |
| Project, Analysis, SiteReport, BOQItem, | |
| UploadedFile, Defect, ProjectSettings, | |
| User, ProjectUser, SiteVisit, SiteVisitImage, | |
| MemoryEntry, Workflow, WorkflowLog, Task, # أضفنا Task هنا | |
| get_password_hash, verify_password | |
| ) | |
| from orchestrator import route_request | |
| from pdf_generator import generate_project_report | |
| logging.basicConfig(level=logging.INFO) | |
| logger = logging.getLogger("main") | |
| def get_secret_from_vault(secret_name): | |
| vault_url = os.getenv("AZURE_VAULT_URL") | |
| if vault_url and AZURE_AVAILABLE: | |
| try: | |
| credential = DefaultAzureCredential() | |
| secret_client = SecretClient(vault_url=vault_url, credential=credential) | |
| secret = secret_client.get_secret(secret_name) | |
| return secret.value | |
| except Exception as e: | |
| logger.warning(f"فشل جلب {secret_name} من Vault: {e}") | |
| return None | |
| SECRET_KEY = get_secret_from_vault("SECRET-KEY") or os.getenv("SECRET_KEY", "fallback-secret-key-change-in-production") | |
| OPENAI_API_KEY = get_secret_from_vault("OPENAI-API-KEY") or os.getenv("OPENAI_API_KEY") | |
| GEMINI_API_KEY = get_secret_from_vault("GEMINI-API-KEY") or os.getenv("GEMINI_API_KEY") | |
| DEEPSEEK_API_KEY = get_secret_from_vault("DEEPSEEK-API-KEY") or os.getenv("DEEPSEEK_API_KEY") | |
| GROK_API_KEY = get_secret_from_vault("GROK-API-KEY") or os.getenv("GROK_API_KEY") | |
| OPENROUTER_API_KEY = get_secret_from_vault("OPENROUTER-API-KEY") or os.getenv("OPENROUTER_API_KEY") | |
| MISTRAL_API_KEY = get_secret_from_vault("MISTRAL-API-KEY") or os.getenv("MISTRAL_API_KEY") | |
| if OPENAI_API_KEY: | |
| os.environ["OPENAI_API_KEY"] = OPENAI_API_KEY | |
| if GEMINI_API_KEY: | |
| os.environ["GEMINI_API_KEY"] = GEMINI_API_KEY | |
| if DEEPSEEK_API_KEY: | |
| os.environ["DEEPSEEK_API_KEY"] = DEEPSEEK_API_KEY | |
| if GROK_API_KEY: | |
| os.environ["GROK_API_KEY"] = GROK_API_KEY | |
| if OPENROUTER_API_KEY: | |
| os.environ["OPENROUTER_API_KEY"] = OPENROUTER_API_KEY | |
| if MISTRAL_API_KEY: | |
| os.environ["MISTRAL_API_KEY"] = MISTRAL_API_KEY | |
| REDIS_URL = os.getenv("REDIS_URL", "redis://localhost:6379") | |
| try: | |
| redis_client = redis.Redis.from_url(REDIS_URL, decode_responses=True) | |
| redis_client.ping() | |
| logger.info("✅ Redis متصل") | |
| except Exception as e: | |
| redis_client = None | |
| logger.warning(f"⚠️ Redis غير متاح: {e}") | |
| scheduler = BackgroundScheduler() | |
| def generate_daily_report_for_all_projects(): | |
| db = SessionLocal() | |
| try: | |
| projects = db.query(Project).all() | |
| for project in projects: | |
| defects = db.query(Defect).filter(Defect.project_id == project.id).all() | |
| open_defects = [d for d in defects if d.status == "Open"] | |
| high_severity = [d for d in defects if d.severity == "High"] | |
| total_defects = len(defects) | |
| health_score = 100 | |
| health_score -= len(open_defects) * 5 | |
| health_score -= len(high_severity) * 10 | |
| health_score = max(0, min(100, health_score)) | |
| summary = f"مشروع {project.name}\n" | |
| summary += f"التاريخ: {datetime.now().strftime('%Y-%m-%d')}\n" | |
| summary += f"إجمالي العيوب: {total_defects}\n" | |
| summary += f"عيوب مفتوحة: {len(open_defects)}\n" | |
| summary += f"عيوب عالية الخطورة: {len(high_severity)}\n" | |
| summary += f"مؤشر الصحة: {health_score}%\n" | |
| logger.info(f"تقرير يومي لمشروع {project.id}:\n{summary}") | |
| finally: | |
| db.close() | |
| scheduler.add_job( | |
| generate_daily_report_for_all_projects, | |
| trigger=CronTrigger(hour=18, minute=0), | |
| id="daily_report", | |
| replace_existing=True | |
| ) | |
| scheduler.start() | |
| ALGORITHM = "HS256" | |
| ACCESS_TOKEN_EXPIRE_MINUTES = 30 | |
| pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") | |
| oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") | |
| def create_access_token(data: dict): | |
| to_encode = data.copy() | |
| expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) | |
| to_encode.update({"exp": expire}) | |
| encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) | |
| return encoded_jwt | |
| async def get_current_user(token: str = Depends(oauth2_scheme)): | |
| credentials_exception = HTTPException( | |
| status_code=status.HTTP_401_UNAUTHORIZED, | |
| detail="Could not validate credentials", | |
| headers={"WWW-Authenticate": "Bearer"}, | |
| ) | |
| try: | |
| payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) | |
| username: str = payload.get("sub") | |
| if username is None: | |
| raise credentials_exception | |
| except JWTError: | |
| raise credentials_exception | |
| db = SessionLocal() | |
| user = db.query(User).filter(User.username == username).first() | |
| db.close() | |
| if user is None: | |
| raise credentials_exception | |
| return user | |
| async def get_current_active_user(current_user: User = Depends(get_current_user)): | |
| if not current_user.is_active: | |
| raise HTTPException(status_code=400, detail="Inactive user") | |
| return current_user | |
| app = FastAPI(title="Engineering OS v9.0 - Enterprise Edition") | |
| # ========== إضافة CORS ========== | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=[ | |
| "http://localhost:8501", # Streamlit | |
| "http://localhost:3000", # React (Vite) | |
| "http://localhost:8000", # نفس الباك إند | |
| ], | |
| allow_credentials=True, | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| ) | |
| UPLOAD_DIR = "uploads" | |
| os.makedirs(UPLOAD_DIR, exist_ok=True) | |
| def on_startup(): | |
| init_db() | |
| logger.info("Database Initialized with Enterprise schema.") | |
| def shutdown_event(): | |
| scheduler.shutdown() | |
| # دالة مساعدة لإرجاع استجابة موحدة | |
| def success_response(data=None, meta=None): | |
| response = {"success": True, "data": data} | |
| if meta: | |
| response["meta"] = meta | |
| return JSONResponse(content=response) | |
| def error_response(message, code="ERROR", status_code=400): | |
| return JSONResponse( | |
| status_code=status_code, | |
| content={"success": False, "error": {"code": code, "message": message}} | |
| ) | |
| def health_check(): | |
| return success_response({"status": "ok", "redis": redis_client is not None, "scheduler": "running"}) | |
| # ---------- Endpoints المستخدمين والمشاريع ---------- | |
| def register(username: str, email: str, password: str, full_name: str = "", role: str = "viewer"): | |
| if len(password.encode('utf-8')) > 72: | |
| return error_response("كلمة المرور طويلة جداً. الحد الأقصى 72 حرفاً.", code="PASSWORD_TOO_LONG") | |
| db = SessionLocal() | |
| try: | |
| if db.query(User).filter((User.username == username) | (User.email == email)).first(): | |
| return error_response("Username or email already registered", code="USER_EXISTS") | |
| hashed = get_password_hash(password) | |
| user = User( | |
| username=username, | |
| email=email, | |
| hashed_password=hashed, | |
| full_name=full_name, | |
| role=role | |
| ) | |
| db.add(user) | |
| db.commit() | |
| db.refresh(user) | |
| return success_response({"id": user.id, "username": user.username, "role": user.role}) | |
| finally: | |
| db.close() | |
| def login(form_data: OAuth2PasswordRequestForm = Depends()): | |
| if len(form_data.password.encode('utf-8')) > 72: | |
| return error_response("كلمة المرور طويلة جداً. الحد الأقصى 72 حرفاً.", code="PASSWORD_TOO_LONG") | |
| db = SessionLocal() | |
| try: | |
| user = db.query(User).filter(User.username == form_data.username).first() | |
| if not user or not verify_password(form_data.password, user.hashed_password): | |
| return error_response("Incorrect username or password", code="INVALID_CREDENTIALS", status_code=400) | |
| access_token = create_access_token(data={"sub": user.username}) | |
| return success_response({"access_token": access_token, "token_type": "bearer"}) | |
| finally: | |
| db.close() | |
| async def read_users_me(current_user: User = Depends(get_current_active_user)): | |
| return success_response({ | |
| "id": current_user.id, | |
| "username": current_user.username, | |
| "email": current_user.email, | |
| "role": current_user.role, | |
| "full_name": current_user.full_name | |
| }) | |
| def add_user_to_project( | |
| project_id: int, | |
| user_id: int, | |
| permission: str = "read", | |
| current_user: User = Depends(get_current_active_user) | |
| ): | |
| if current_user.role != "admin": | |
| return error_response("Not enough permissions", code="FORBIDDEN", status_code=403) | |
| db = SessionLocal() | |
| try: | |
| project = db.query(Project).filter(Project.id == project_id).first() | |
| user = db.query(User).filter(User.id == user_id).first() | |
| if not project or not user: | |
| return error_response("Project or User not found", code="NOT_FOUND", status_code=404) | |
| existing = db.query(ProjectUser).filter( | |
| ProjectUser.project_id == project_id, | |
| ProjectUser.user_id == user_id | |
| ).first() | |
| if existing: | |
| existing.permission = permission | |
| else: | |
| pu = ProjectUser(project_id=project_id, user_id=user_id, permission=permission) | |
| db.add(pu) | |
| db.commit() | |
| return success_response(True) | |
| finally: | |
| db.close() | |
| def create_project( | |
| name: str, | |
| location: str = "Cairo", | |
| current_user: User = Depends(get_current_active_user) | |
| ): | |
| db = SessionLocal() | |
| try: | |
| proj = Project(name=name, location=location) | |
| db.add(proj) | |
| db.flush() | |
| pu = ProjectUser(project_id=proj.id, user_id=current_user.id, permission="admin") | |
| db.add(pu) | |
| db.commit() | |
| db.refresh(proj) | |
| logger.info(f"Created Project: {proj.name} by user {current_user.username}") | |
| return success_response({"id": proj.id, "name": proj.name}) | |
| except Exception as e: | |
| logger.error(f"Error creating project: {e}") | |
| return error_response(str(e), code="CREATE_FAILED") | |
| finally: | |
| db.close() | |
| def get_projects(current_user: User = Depends(get_current_active_user)): | |
| db = SessionLocal() | |
| try: | |
| if current_user.role == "admin": | |
| projects = db.query(Project).all() | |
| else: | |
| projects = db.query(Project).join(ProjectUser).filter(ProjectUser.user_id == current_user.id).all() | |
| return success_response([{"id": p.id, "name": p.name, "location": p.location} for p in projects]) | |
| finally: | |
| db.close() | |
| def delete_project( | |
| project_id: int, | |
| current_user: User = Depends(get_current_active_user) | |
| ): | |
| db = SessionLocal() | |
| try: | |
| project_user = db.query(ProjectUser).filter( | |
| ProjectUser.project_id == project_id, | |
| ProjectUser.user_id == current_user.id | |
| ).first() | |
| if current_user.role != "admin" and (not project_user or project_user.permission not in ["admin"]): | |
| return error_response("Not enough permissions", code="FORBIDDEN", status_code=403) | |
| proj = db.query(Project).filter(Project.id == project_id).first() | |
| if proj: | |
| db.delete(proj) | |
| db.commit() | |
| logger.info(f"Deleted Project ID: {project_id}") | |
| return success_response(True) | |
| finally: | |
| db.close() | |
| # ---------- Endpoint جلب بيانات المشروع مع Health Score ---------- | |
| def get_project_data( | |
| project_id: int, | |
| current_user: User = Depends(get_current_active_user) | |
| ): | |
| cache_key = f"project_data:{project_id}:user:{current_user.id}" | |
| if redis_client: | |
| cached = redis_client.get(cache_key) | |
| if cached: | |
| logger.info(f"✅ استخدام cached data للمشروع {project_id}") | |
| return success_response(json.loads(cached)) | |
| db = SessionLocal() | |
| try: | |
| project_user = db.query(ProjectUser).filter( | |
| ProjectUser.project_id == project_id, | |
| ProjectUser.user_id == current_user.id | |
| ).first() | |
| if current_user.role != "admin" and not project_user: | |
| return error_response("Not enough permissions", code="FORBIDDEN", status_code=403) | |
| project = db.query(Project).filter(Project.id == project_id).first() | |
| if not project: | |
| return error_response("Project not found", code="NOT_FOUND", status_code=404) | |
| analyses = db.query(Analysis).filter(Analysis.project_id == project_id).order_by(Analysis.created_at.desc()).all() | |
| boqs = db.query(BOQItem).filter(BOQItem.project_id == project_id).all() | |
| defects = db.query(Defect).filter(Defect.project_id == project_id).all() | |
| files = db.query(UploadedFile).filter(UploadedFile.project_id == project_id).all() | |
| tasks = db.query(Task).filter(Task.project_id == project_id).all() # NEW | |
| total_cost = sum([b.total_price or 0 for b in boqs]) | |
| open_defects = [d for d in defects if d.status == "Open"] | |
| high_severity = [d for d in defects if d.severity == "High"] | |
| health_score = 100 | |
| health_score -= len(open_defects) * 5 | |
| health_score -= len(high_severity) * 10 | |
| health_score = max(0, min(100, health_score)) | |
| result = { | |
| "project_info": {"name": project.name, "location": project.location}, | |
| "timeline": [ | |
| { | |
| "type": "analysis", | |
| "date": a.created_at.strftime("%Y-%m-%d %H:%M"), | |
| "task": a.task_type, | |
| "status": a.safety_status | |
| } for a in analyses | |
| ], | |
| "boq": { | |
| "items": [{"id": b.id, "desc": b.description, "unit": b.unit, "qty": b.quantity, "price": b.total_price} for b in boqs], | |
| "total_cost": total_cost | |
| }, | |
| "defects": [ | |
| {"id": d.id, "desc": d.description, "severity": d.severity, "status": d.status} for d in defects | |
| ], | |
| "files_count": len(files), | |
| "health_score": health_score, | |
| "tasks": [ # NEW | |
| { | |
| "id": t.id, | |
| "description": t.description, | |
| "assignee": t.assignee, | |
| "due_date": t.due_date.strftime("%Y-%m-%d"), | |
| "priority": t.priority, | |
| "status": t.status | |
| } for t in tasks | |
| ] | |
| } | |
| if redis_client: | |
| redis_client.setex(cache_key, 300, json.dumps(result)) | |
| return success_response(result) | |
| finally: | |
| db.close() | |
| # ---------- Endpoint المعالجة الرئيسي ---------- | |
| async def process_request( | |
| message: str = Form(None), | |
| project_id: int = Form(...), | |
| file: UploadFile = File(None), | |
| history: str = Form("[]"), | |
| current_user: User = Depends(get_current_active_user) | |
| ): | |
| db_check = SessionLocal() | |
| try: | |
| project_user = db_check.query(ProjectUser).filter( | |
| ProjectUser.project_id == project_id, | |
| ProjectUser.user_id == current_user.id | |
| ).first() | |
| if current_user.role != "admin" and (not project_user or project_user.permission not in ["write", "admin"]): | |
| return error_response("Not enough permissions", code="FORBIDDEN", status_code=403) | |
| finally: | |
| db_check.close() | |
| file_bytes = await file.read() if file else None | |
| file_type = file.content_type if file else None | |
| db = SessionLocal() | |
| file_record = None | |
| try: | |
| if file_bytes and file: | |
| file_path = os.path.join(UPLOAD_DIR, f"{project_id}_{file.filename}") | |
| with open(file_path, "wb") as f: | |
| f.write(file_bytes) | |
| file_record = UploadedFile( | |
| project_id=project_id, | |
| filename=file.filename, | |
| file_type=file_type, | |
| file_path=file_path, | |
| analyzed=True | |
| ) | |
| db.add(file_record) | |
| db.commit() | |
| db.refresh(file_record) | |
| logger.info(f"File saved: {file.filename}") | |
| except Exception as e: | |
| logger.error(f"File saving error: {e}") | |
| finally: | |
| db.close() | |
| result = await route_request(message, file_bytes, file_type, project_id, json.loads(history)) | |
| db = SessionLocal() | |
| try: | |
| domain = result.get("domain") | |
| results = result.get("results", {}) | |
| if domain == "boq" and "boq_item" in results: | |
| item = results["boq_item"] | |
| new_item = BOQItem( | |
| project_id=project_id, | |
| description=item.get("description"), | |
| unit=item.get("unit"), | |
| quantity=item.get("quantity"), | |
| total_price=item.get("total_price", 0) | |
| ) | |
| db.add(new_item) | |
| elif domain == "design": | |
| new_analysis = Analysis( | |
| project_id=project_id, | |
| task_type=result.get("task"), | |
| input_text=message, | |
| result_json=result, | |
| safety_status="Unsafe" if "⚠️ تحذير" in str(results) else "Safe" | |
| ) | |
| db.add(new_analysis) | |
| elif domain == "site": | |
| if "checklist_data" in results: | |
| report = SiteReport( | |
| project_id=project_id, | |
| summary=message, | |
| issues=json.dumps(results["checklist_data"]) | |
| ) | |
| db.add(report) | |
| if "⚠️ العيوب" in results: | |
| new_defect = Defect( | |
| project_id=project_id, | |
| description=results["⚠️ العيوب"], | |
| severity="Medium", | |
| status="Open", | |
| image_id=file_record.id if file_record else None | |
| ) | |
| db.add(new_defect) | |
| elif domain == "general" and message: | |
| new_analysis = Analysis( | |
| project_id=project_id, | |
| task_type="chat", | |
| input_text=message, | |
| result_json=result, | |
| safety_status="Safe" | |
| ) | |
| db.add(new_analysis) | |
| db.commit() | |
| logger.info(f"Data saved for Project {project_id} - Domain: {domain}") | |
| except Exception as e: | |
| logger.error(f"Database Save Error: {e}") | |
| db.rollback() | |
| finally: | |
| db.close() | |
| return success_response(result) | |
| # ---------- Endpoint تصدير PDF ---------- | |
| def export_pdf( | |
| project_id: int, | |
| current_user: User = Depends(get_current_active_user) | |
| ): | |
| db_check = SessionLocal() | |
| try: | |
| project_user = db_check.query(ProjectUser).filter( | |
| ProjectUser.project_id == project_id, | |
| ProjectUser.user_id == current_user.id | |
| ).first() | |
| if current_user.role != "admin" and not project_user: | |
| return error_response("Not enough permissions", code="FORBIDDEN", status_code=403) | |
| finally: | |
| db_check.close() | |
| try: | |
| pdf_bytes = generate_project_report(project_id) | |
| if pdf_bytes: | |
| return Response( | |
| content=pdf_bytes, | |
| media_type="application/pdf", | |
| headers={"Content-Disposition": f"attachment; filename=report_{project_id}.pdf"} | |
| ) | |
| return error_response("Could not generate PDF", code="PDF_FAILED") | |
| except Exception as e: | |
| logger.error(f"PDF Export Error: {e}") | |
| return error_response(str(e), code="EXCEPTION") | |
| # ---------- Endpoint إضافة بند حصر ---------- | |
| def add_boq_item( | |
| project_id: int, | |
| desc: str, | |
| unit: str, | |
| qty: float, | |
| price: float, | |
| current_user: User = Depends(get_current_active_user) | |
| ): | |
| db_check = SessionLocal() | |
| try: | |
| project_user = db_check.query(ProjectUser).filter( | |
| ProjectUser.project_id == project_id, | |
| ProjectUser.user_id == current_user.id | |
| ).first() | |
| if current_user.role != "admin" and (not project_user or project_user.permission not in ["write", "admin"]): | |
| return error_response("Not enough permissions", code="FORBIDDEN", status_code=403) | |
| finally: | |
| db_check.close() | |
| db = SessionLocal() | |
| try: | |
| new_item = BOQItem( | |
| project_id=project_id, | |
| description=desc, | |
| unit=unit, | |
| quantity=qty, | |
| total_price=price | |
| ) | |
| db.add(new_item) | |
| db.commit() | |
| return success_response({"id": new_item.id}) | |
| except Exception as e: | |
| logger.error(f"Add BOQ error: {e}") | |
| return error_response(str(e), code="DB_ERROR") | |
| finally: | |
| db.close() | |
| # ---------- Endpoint حذف بند حصر ---------- | |
| def delete_boq_item( | |
| boq_id: int, | |
| current_user: User = Depends(get_current_active_user) | |
| ): | |
| db = SessionLocal() | |
| try: | |
| item = db.query(BOQItem).filter(BOQItem.id == boq_id).first() | |
| if not item: | |
| return error_response("BOQ item not found", code="NOT_FOUND", status_code=404) | |
| project_user = db.query(ProjectUser).filter( | |
| ProjectUser.project_id == item.project_id, | |
| ProjectUser.user_id == current_user.id | |
| ).first() | |
| if current_user.role != "admin" and (not project_user or project_user.permission not in ["write", "admin"]): | |
| return error_response("Not enough permissions", code="FORBIDDEN", status_code=403) | |
| db.delete(item) | |
| db.commit() | |
| return success_response(True) | |
| except Exception as e: | |
| logger.error(f"Delete BOQ error: {e}") | |
| return error_response(str(e), code="DB_ERROR") | |
| finally: | |
| db.close() | |
| # ---------- Endpoint تعديل عيب ---------- | |
| def update_defect( | |
| defect_id: int, | |
| description: str = None, | |
| severity: str = None, | |
| status: str = None, | |
| current_user: User = Depends(get_current_active_user) | |
| ): | |
| db = SessionLocal() | |
| try: | |
| defect = db.query(Defect).filter(Defect.id == defect_id).first() | |
| if not defect: | |
| return error_response("Defect not found", code="NOT_FOUND", status_code=404) | |
| project_user = db.query(ProjectUser).filter( | |
| ProjectUser.project_id == defect.project_id, | |
| ProjectUser.user_id == current_user.id | |
| ).first() | |
| if current_user.role != "admin" and (not project_user or project_user.permission not in ["write", "admin"]): | |
| return error_response("Not enough permissions", code="FORBIDDEN", status_code=403) | |
| if description: | |
| defect.description = description | |
| if severity: | |
| defect.severity = severity | |
| if status: | |
| defect.status = status | |
| db.commit() | |
| return success_response(True) | |
| except Exception as e: | |
| logger.error(f"Update defect error: {e}") | |
| return error_response(str(e), code="DB_ERROR") | |
| finally: | |
| db.close() | |
| # ---------- Endpoint حذف عيب ---------- | |
| def delete_defect( | |
| defect_id: int, | |
| current_user: User = Depends(get_current_active_user) | |
| ): | |
| db = SessionLocal() | |
| try: | |
| defect = db.query(Defect).filter(Defect.id == defect_id).first() | |
| if not defect: | |
| return error_response("Defect not found", code="NOT_FOUND", status_code=404) | |
| project_user = db.query(ProjectUser).filter( | |
| ProjectUser.project_id == defect.project_id, | |
| ProjectUser.user_id == current_user.id | |
| ).first() | |
| if current_user.role != "admin" and (not project_user or project_user.permission not in ["write", "admin"]): | |
| return error_response("Not enough permissions", code="FORBIDDEN", status_code=403) | |
| db.delete(defect) | |
| db.commit() | |
| return success_response(True) | |
| except Exception as e: | |
| logger.error(f"Delete defect error: {e}") | |
| return error_response(str(e), code="DB_ERROR") | |
| finally: | |
| db.close() | |
| # ---------- Endpoint إعدادات المشروع ---------- | |
| def get_project_settings( | |
| project_id: int, | |
| current_user: User = Depends(get_current_active_user) | |
| ): | |
| db_check = SessionLocal() | |
| try: | |
| project_user = db_check.query(ProjectUser).filter( | |
| ProjectUser.project_id == project_id, | |
| ProjectUser.user_id == current_user.id | |
| ).first() | |
| if current_user.role != "admin" and not project_user: | |
| return error_response("Not enough permissions", code="FORBIDDEN", status_code=403) | |
| finally: | |
| db_check.close() | |
| db = SessionLocal() | |
| try: | |
| settings = db.query(ProjectSettings).filter(ProjectSettings.project_id == project_id).first() | |
| if not settings: | |
| settings = ProjectSettings(project_id=project_id) | |
| db.add(settings) | |
| db.commit() | |
| db.refresh(settings) | |
| return success_response({ | |
| "concrete_price": settings.concrete_price, | |
| "steel_price": settings.steel_price, | |
| "preferred_ai_model": settings.preferred_ai_model | |
| }) | |
| finally: | |
| db.close() | |
| def save_project_settings( | |
| project_id: int, | |
| concrete_price: float, | |
| steel_price: float, | |
| preferred_ai_model: str, | |
| current_user: User = Depends(get_current_active_user) | |
| ): | |
| db_check = SessionLocal() | |
| try: | |
| project_user = db_check.query(ProjectUser).filter( | |
| ProjectUser.project_id == project_id, | |
| ProjectUser.user_id == current_user.id | |
| ).first() | |
| if current_user.role != "admin" and (not project_user or project_user.permission not in ["admin"]): | |
| return error_response("Not enough permissions", code="FORBIDDEN", status_code=403) | |
| finally: | |
| db_check.close() | |
| db = SessionLocal() | |
| try: | |
| settings = db.query(ProjectSettings).filter(ProjectSettings.project_id == project_id).first() | |
| if not settings: | |
| settings = ProjectSettings(project_id=project_id) | |
| db.add(settings) | |
| settings.concrete_price = concrete_price | |
| settings.steel_price = steel_price | |
| settings.preferred_ai_model = preferred_ai_model | |
| db.commit() | |
| return success_response(True) | |
| except Exception as e: | |
| logger.error(f"Save settings error: {e}") | |
| return error_response(str(e), code="DB_ERROR") | |
| finally: | |
| db.close() | |
| # ---------- Endpoint إحصائيات المشروع ---------- | |
| def project_stats( | |
| project_id: int, | |
| current_user: User = Depends(get_current_active_user) | |
| ): | |
| db_check = SessionLocal() | |
| try: | |
| project_user = db_check.query(ProjectUser).filter( | |
| ProjectUser.project_id == project_id, | |
| ProjectUser.user_id == current_user.id | |
| ).first() | |
| if current_user.role != "admin" and not project_user: | |
| return error_response("Not enough permissions", code="FORBIDDEN", status_code=403) | |
| finally: | |
| db_check.close() | |
| db = SessionLocal() | |
| try: | |
| analyses_count = db.query(Analysis).filter(Analysis.project_id == project_id).count() | |
| files_count = db.query(UploadedFile).filter(UploadedFile.project_id == project_id).count() | |
| defects_count = db.query(Defect).filter(Defect.project_id == project_id).count() | |
| total_cost = db.query(func.sum(BOQItem.total_price)).filter(BOQItem.project_id == project_id).scalar() or 0 | |
| return success_response({ | |
| "analyses_count": analyses_count, | |
| "files_count": files_count, | |
| "defects_count": defects_count, | |
| "total_cost": total_cost | |
| }) | |
| finally: | |
| db.close() | |
| # ---------- Endpoint تصدير BOQ إلى Excel ---------- | |
| def export_boq_excel( | |
| project_id: int, | |
| current_user: User = Depends(get_current_active_user) | |
| ): | |
| db_check = SessionLocal() | |
| try: | |
| project_user = db_check.query(ProjectUser).filter( | |
| ProjectUser.project_id == project_id, | |
| ProjectUser.user_id == current_user.id | |
| ).first() | |
| if current_user.role != "admin" and not project_user: | |
| return error_response("Not enough permissions", code="FORBIDDEN", status_code=403) | |
| finally: | |
| db_check.close() | |
| db = SessionLocal() | |
| try: | |
| boqs = db.query(BOQItem).filter(BOQItem.project_id == project_id).all() | |
| if not boqs: | |
| return error_response("No BOQ items", code="NOT_FOUND", status_code=404) | |
| data = [] | |
| for b in boqs: | |
| data.append({ | |
| "الوصف": b.description, | |
| "الوحدة": b.unit, | |
| "الكمية": b.quantity, | |
| "السعر الإجمالي": b.total_price | |
| }) | |
| df = pd.DataFrame(data) | |
| with tempfile.NamedTemporaryFile(delete=False, suffix=".xlsx") as tmp: | |
| df.to_excel(tmp.name, index=False) | |
| return FileResponse(tmp.name, media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", filename=f"boq_project_{project_id}.xlsx") | |
| except Exception as e: | |
| logger.error(f"Export BOQ error: {e}") | |
| return error_response(str(e), code="EXPORT_FAILED") | |
| finally: | |
| db.close() | |
| # ---------- Endpoints تقارير الموقع ---------- | |
| async def upload_site_visit( | |
| project_id: int, | |
| location_name: str = Form(None), | |
| latitude: float = Form(None), | |
| longitude: float = Form(None), | |
| notes: str = Form(None), | |
| files: List[UploadFile] = File(None), | |
| current_user: User = Depends(get_current_active_user) | |
| ): | |
| db_check = SessionLocal() | |
| try: | |
| project_user = db_check.query(ProjectUser).filter( | |
| ProjectUser.project_id == project_id, | |
| ProjectUser.user_id == current_user.id | |
| ).first() | |
| if current_user.role != "admin" and (not project_user or project_user.permission not in ["write", "admin"]): | |
| return error_response("Not enough permissions", code="FORBIDDEN", status_code=403) | |
| finally: | |
| db_check.close() | |
| db = SessionLocal() | |
| try: | |
| site_visit = SiteVisit( | |
| project_id=project_id, | |
| location_name=location_name, | |
| latitude=latitude, | |
| longitude=longitude, | |
| notes=notes, | |
| created_by=current_user.id | |
| ) | |
| db.add(site_visit) | |
| db.flush() | |
| if files: | |
| for file in files: | |
| if file.content_type.startswith("image/"): | |
| contents = await file.read() | |
| tags = exifread.process_file(io.BytesIO(contents), details=False) | |
| date_taken = None | |
| if 'EXIF DateTimeOriginal' in tags: | |
| date_str = str(tags['EXIF DateTimeOriginal']) | |
| try: | |
| date_taken = datetime.strptime(date_str, '%Y:%m:%d %H:%M:%S') | |
| except: | |
| pass | |
| file_path = os.path.join(UPLOAD_DIR, f"site_{project_id}_{file.filename}") | |
| with open(file_path, "wb") as f: | |
| f.write(contents) | |
| img_record = SiteVisitImage( | |
| site_visit_id=site_visit.id, | |
| image_path=file_path, | |
| caption=file.filename, | |
| timestamp=date_taken | |
| ) | |
| db.add(img_record) | |
| db.commit() | |
| return success_response({"site_visit_id": site_visit.id}) | |
| except Exception as e: | |
| logger.error(f"Upload site visit error: {e}") | |
| db.rollback() | |
| return error_response(str(e), code="UPLOAD_FAILED") | |
| finally: | |
| db.close() | |
| def get_site_visits( | |
| project_id: int, | |
| current_user: User = Depends(get_current_active_user) | |
| ): | |
| db_check = SessionLocal() | |
| try: | |
| project_user = db_check.query(ProjectUser).filter( | |
| ProjectUser.project_id == project_id, | |
| ProjectUser.user_id == current_user.id | |
| ).first() | |
| if current_user.role != "admin" and not project_user: | |
| return error_response("Not enough permissions", code="FORBIDDEN", status_code=403) | |
| finally: | |
| db_check.close() | |
| db = SessionLocal() | |
| try: | |
| visits = db.query(SiteVisit).filter(SiteVisit.project_id == project_id).order_by(SiteVisit.visit_date.desc()).all() | |
| result = [] | |
| for v in visits: | |
| images = db.query(SiteVisitImage).filter(SiteVisitImage.site_visit_id == v.id).all() | |
| result.append({ | |
| "id": v.id, | |
| "visit_date": v.visit_date.isoformat(), | |
| "location_name": v.location_name, | |
| "latitude": v.latitude, | |
| "longitude": v.longitude, | |
| "notes": v.notes, | |
| "images": [{"path": img.image_path, "caption": img.caption} for img in images] | |
| }) | |
| return success_response(result) | |
| finally: | |
| db.close() | |
| # ---------- Endpoints الذاكرة (Memory Bank) ---------- | |
| def add_memory_entry( | |
| project_id: int, | |
| entry_type: str, | |
| title: str, | |
| content: str, | |
| tags: str = "", | |
| current_user: User = Depends(get_current_active_user) | |
| ): | |
| db_check = SessionLocal() | |
| try: | |
| project_user = db_check.query(ProjectUser).filter( | |
| ProjectUser.project_id == project_id, | |
| ProjectUser.user_id == current_user.id | |
| ).first() | |
| if current_user.role != "admin" and (not project_user or project_user.permission not in ["write", "admin"]): | |
| return error_response("Not enough permissions", code="FORBIDDEN", status_code=403) | |
| finally: | |
| db_check.close() | |
| db = SessionLocal() | |
| try: | |
| memory = MemoryEntry( | |
| project_id=project_id, | |
| entry_type=entry_type, | |
| title=title, | |
| content=content, | |
| tags=tags | |
| ) | |
| db.add(memory) | |
| db.commit() | |
| return success_response({"id": memory.id}) | |
| except Exception as e: | |
| logger.error(f"Add memory error: {e}") | |
| return error_response(str(e), code="DB_ERROR") | |
| finally: | |
| db.close() | |
| def get_memory_entries( | |
| project_id: int, | |
| entry_type: str = None, | |
| tag: str = None, | |
| current_user: User = Depends(get_current_active_user) | |
| ): | |
| db_check = SessionLocal() | |
| try: | |
| project_user = db_check.query(ProjectUser).filter( | |
| ProjectUser.project_id == project_id, | |
| ProjectUser.user_id == current_user.id | |
| ).first() | |
| if current_user.role != "admin" and not project_user: | |
| return error_response("Not enough permissions", code="FORBIDDEN", status_code=403) | |
| finally: | |
| db_check.close() | |
| db = SessionLocal() | |
| try: | |
| query = db.query(MemoryEntry).filter(MemoryEntry.project_id == project_id) | |
| if entry_type: | |
| query = query.filter(MemoryEntry.entry_type == entry_type) | |
| if tag: | |
| query = query.filter(MemoryEntry.tags.contains(tag)) | |
| entries = query.order_by(MemoryEntry.created_at.desc()).all() | |
| return success_response([ | |
| { | |
| "id": e.id, | |
| "entry_type": e.entry_type, | |
| "title": e.title, | |
| "content": e.content, | |
| "tags": e.tags, | |
| "created_at": e.created_at.isoformat() | |
| } | |
| for e in entries | |
| ]) | |
| finally: | |
| db.close() | |
| # ---------- Endpoints سير العمل (Workflows) ---------- | |
| def create_workflow( | |
| project_id: int, | |
| name: str, | |
| trigger_event: str, | |
| action_type: str, | |
| action_params: dict, | |
| current_user: User = Depends(get_current_active_user) | |
| ): | |
| if current_user.role != "admin": | |
| return error_response("Only admins can create workflows", code="FORBIDDEN", status_code=403) | |
| db = SessionLocal() | |
| try: | |
| workflow = Workflow( | |
| project_id=project_id, | |
| name=name, | |
| trigger_event=trigger_event, | |
| action_type=action_type, | |
| action_params=action_params | |
| ) | |
| db.add(workflow) | |
| db.commit() | |
| return success_response({"id": workflow.id}) | |
| except Exception as e: | |
| logger.error(f"Create workflow error: {e}") | |
| return error_response(str(e), code="DB_ERROR") | |
| finally: | |
| db.close() | |
| def get_workflows( | |
| project_id: int, | |
| current_user: User = Depends(get_current_active_user) | |
| ): | |
| db_check = SessionLocal() | |
| try: | |
| project_user = db_check.query(ProjectUser).filter( | |
| ProjectUser.project_id == project_id, | |
| ProjectUser.user_id == current_user.id | |
| ).first() | |
| if current_user.role != "admin" and not project_user: | |
| return error_response("Not enough permissions", code="FORBIDDEN", status_code=403) | |
| finally: | |
| db_check.close() | |
| db = SessionLocal() | |
| try: | |
| workflows = db.query(Workflow).filter(Workflow.project_id == project_id).all() | |
| return success_response([ | |
| { | |
| "id": w.id, | |
| "name": w.name, | |
| "trigger_event": w.trigger_event, | |
| "action_type": w.action_type, | |
| "action_params": w.action_params, | |
| "is_active": w.is_active | |
| } | |
| for w in workflows | |
| ]) | |
| finally: | |
| db.close() | |
| def toggle_workflow( | |
| workflow_id: int, | |
| current_user: User = Depends(get_current_active_user) | |
| ): | |
| if current_user.role != "admin": | |
| return error_response("Only admins can modify workflows", code="FORBIDDEN", status_code=403) | |
| db = SessionLocal() | |
| try: | |
| workflow = db.query(Workflow).filter(Workflow.id == workflow_id).first() | |
| if not workflow: | |
| return error_response("Workflow not found", code="NOT_FOUND", status_code=404) | |
| workflow.is_active = not workflow.is_active | |
| db.commit() | |
| return success_response({"is_active": workflow.is_active}) | |
| finally: | |
| db.close() | |
| # ---------- Endpoint إضافة عيب يدوي ---------- | |
| def add_defect( | |
| project_id: int, | |
| description: str, | |
| severity: str = "Medium", | |
| status: str = "Open", | |
| current_user: User = Depends(get_current_active_user) | |
| ): | |
| db_check = SessionLocal() | |
| try: | |
| project_user = db_check.query(ProjectUser).filter( | |
| ProjectUser.project_id == project_id, | |
| ProjectUser.user_id == current_user.id | |
| ).first() | |
| if current_user.role != "admin" and (not project_user or project_user.permission not in ["write", "admin"]): | |
| return error_response("Not enough permissions", code="FORBIDDEN", status_code=403) | |
| finally: | |
| db_check.close() | |
| db = SessionLocal() | |
| try: | |
| new_defect = Defect( | |
| project_id=project_id, | |
| description=description, | |
| severity=severity, | |
| status=status | |
| ) | |
| db.add(new_defect) | |
| db.commit() | |
| return success_response({"id": new_defect.id}) | |
| except Exception as e: | |
| logger.error(f"Add defect error: {e}") | |
| return error_response(str(e), code="DB_ERROR") | |
| finally: | |
| db.close() | |
| # ========== NEW: Endpoints للمهام ========== | |
| def create_task( | |
| project_id: int, | |
| description: str, | |
| assignee: str, | |
| due_date: str, # YYYY-MM-DD | |
| priority: str, | |
| status: str = "قيد الانتظار", | |
| current_user: User = Depends(get_current_active_user) | |
| ): | |
| db_check = SessionLocal() | |
| try: | |
| project_user = db_check.query(ProjectUser).filter( | |
| ProjectUser.project_id == project_id, | |
| ProjectUser.user_id == current_user.id | |
| ).first() | |
| if current_user.role != "admin" and (not project_user or project_user.permission not in ["write", "admin"]): | |
| return error_response("Not enough permissions", code="FORBIDDEN", status_code=403) | |
| finally: | |
| db_check.close() | |
| db = SessionLocal() | |
| try: | |
| task = Task( | |
| project_id=project_id, | |
| description=description, | |
| assignee=assignee, | |
| due_date=datetime.strptime(due_date, "%Y-%m-%d").date(), | |
| priority=priority, | |
| status=status | |
| ) | |
| db.add(task) | |
| db.commit() | |
| return success_response({"id": task.id}) | |
| except Exception as e: | |
| logger.error(f"Create task error: {e}") | |
| return error_response(str(e), code="DB_ERROR") | |
| finally: | |
| db.close() | |
| def get_tasks( | |
| project_id: int, | |
| current_user: User = Depends(get_current_active_user) | |
| ): | |
| db_check = SessionLocal() | |
| try: | |
| project_user = db_check.query(ProjectUser).filter( | |
| ProjectUser.project_id == project_id, | |
| ProjectUser.user_id == current_user.id | |
| ).first() | |
| if current_user.role != "admin" and not project_user: | |
| return error_response("Not enough permissions", code="FORBIDDEN", status_code=403) | |
| finally: | |
| db_check.close() | |
| db = SessionLocal() | |
| try: | |
| tasks = db.query(Task).filter(Task.project_id == project_id).all() | |
| return success_response([ | |
| { | |
| "id": t.id, | |
| "description": t.description, | |
| "assignee": t.assignee, | |
| "due_date": t.due_date.strftime("%Y-%m-%d"), | |
| "priority": t.priority, | |
| "status": t.status | |
| } for t in tasks | |
| ]) | |
| finally: | |
| db.close() | |
| def update_task( | |
| task_id: int, | |
| description: str = None, | |
| assignee: str = None, | |
| due_date: str = None, | |
| priority: str = None, | |
| status: str = None, | |
| current_user: User = Depends(get_current_active_user) | |
| ): | |
| db = SessionLocal() | |
| try: | |
| task = db.query(Task).filter(Task.id == task_id).first() | |
| if not task: | |
| return error_response("Task not found", code="NOT_FOUND", status_code=404) | |
| # Check permission | |
| project_user = db.query(ProjectUser).filter( | |
| ProjectUser.project_id == task.project_id, | |
| ProjectUser.user_id == current_user.id | |
| ).first() | |
| if current_user.role != "admin" and (not project_user or project_user.permission not in ["write", "admin"]): | |
| return error_response("Not enough permissions", code="FORBIDDEN", status_code=403) | |
| if description is not None: | |
| task.description = description | |
| if assignee is not None: | |
| task.assignee = assignee | |
| if due_date is not None: | |
| task.due_date = datetime.strptime(due_date, "%Y-%m-%d").date() | |
| if priority is not None: | |
| task.priority = priority | |
| if status is not None: | |
| task.status = status | |
| db.commit() | |
| return success_response(True) | |
| except Exception as e: | |
| logger.error(f"Update task error: {e}") | |
| return error_response(str(e), code="DB_ERROR") | |
| finally: | |
| db.close() | |
| def delete_task( | |
| task_id: int, | |
| current_user: User = Depends(get_current_active_user) | |
| ): | |
| db = SessionLocal() | |
| try: | |
| task = db.query(Task).filter(Task.id == task_id).first() | |
| if not task: | |
| return error_response("Task not found", code="NOT_FOUND", status_code=404) | |
| project_user = db.query(ProjectUser).filter( | |
| ProjectUser.project_id == task.project_id, | |
| ProjectUser.user_id == current_user.id | |
| ).first() | |
| if current_user.role != "admin" and (not project_user or project_user.permission not in ["write", "admin"]): | |
| return error_response("Not enough permissions", code="FORBIDDEN", status_code=403) | |
| db.delete(task) | |
| db.commit() | |
| return success_response(True) | |
| except Exception as e: | |
| logger.error(f"Delete task error: {e}") | |
| return error_response(str(e), code="DB_ERROR") | |
| finally: | |
| db.close() |