Revert 253c922: restore the PM/RBAC work to the integration
Reverting the production revert so the pm branch can merge cleanly on top of the restored RBAC base. Conflicts against newer integration work resolved by combining: - App.tsx: v7's bot-draft wizard bypass + login returnUrl kept alongside the supervisor gating (no-project block screen, panel scoping) - LoginPage: returnUrl navigation kept, supervisor routing added - migration.sql: bot_drafts table kept, unified-projects ALTERs restored
This commit is contained in:
@@ -3,7 +3,7 @@ from fastapi import APIRouter, Depends, Query, Request
|
||||
from sqlalchemy import func, or_, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.auth import UserContext, get_optional_user
|
||||
from app.auth import UserContext, get_optional_user, require_role
|
||||
from app.db.engine import get_db
|
||||
from app.models.procurement import Project, Quote, User
|
||||
from app.schemas.procurement import ProjectCreate, ProjectUpdate
|
||||
@@ -13,6 +13,7 @@ from app.utils.pagination import paginate, sort_query
|
||||
from app.utils.serialization import to_date, to_iso
|
||||
|
||||
router = APIRouter(prefix="/projects", tags=["projects"])
|
||||
_procurement_writer = require_role("admin", "procurement_manager")
|
||||
|
||||
|
||||
async def _optional_ctx(request: Request, db: AsyncSession = Depends(get_db)) -> UserContext | None:
|
||||
@@ -33,8 +34,12 @@ async def project_to_dict(db: AsyncSession, project: Project) -> dict:
|
||||
d = {
|
||||
"id": str(project.id),
|
||||
"name": project.name,
|
||||
"description": project.description,
|
||||
"location": project.location or "",
|
||||
"project_manager": project.project_manager or "",
|
||||
"description": project.description or "",
|
||||
"start_date": to_date(project.start_date),
|
||||
"status": project.status or "ACTIVE",
|
||||
"progress": float(project.progress) if project.progress is not None else 0.0,
|
||||
"pm_user_id": str(project.pm_user_id) if project.pm_user_id else None,
|
||||
"created_at": to_iso(project.created_at),
|
||||
}
|
||||
@@ -42,6 +47,8 @@ async def project_to_dict(db: AsyncSession, project: Project) -> dict:
|
||||
pm = await db.get(User, project.pm_user_id)
|
||||
if pm:
|
||||
d["pm_name"] = f"{pm.first_name} {pm.last_name}".strip() or pm.casdoor_id
|
||||
elif project.project_manager:
|
||||
d["pm_name"] = project.project_manager
|
||||
return d
|
||||
|
||||
|
||||
@@ -55,7 +62,14 @@ async def list_projects(
|
||||
):
|
||||
stmt = select(Project)
|
||||
if q:
|
||||
stmt = stmt.where(or_(Project.name.ilike(f"%{q}%"), Project.description.ilike(f"%{q}%")))
|
||||
stmt = stmt.where(
|
||||
or_(
|
||||
Project.name.ilike(f"%{q}%"),
|
||||
Project.description.ilike(f"%{q}%"),
|
||||
Project.location.ilike(f"%{q}%"),
|
||||
Project.project_manager.ilike(f"%{q}%"),
|
||||
)
|
||||
)
|
||||
stmt = sort_query(stmt, Project, sort)
|
||||
items, meta = await paginate(stmt, page, page_size, db)
|
||||
pm_map = await _pm_map(db, items)
|
||||
@@ -64,24 +78,38 @@ async def list_projects(
|
||||
d = {
|
||||
"id": str(p.id),
|
||||
"name": p.name,
|
||||
"description": p.description,
|
||||
"location": p.location or "",
|
||||
"project_manager": p.project_manager or "",
|
||||
"description": p.description or "",
|
||||
"start_date": to_date(p.start_date),
|
||||
"status": p.status or "ACTIVE",
|
||||
"progress": float(p.progress) if p.progress is not None else 0.0,
|
||||
"pm_user_id": str(p.pm_user_id) if p.pm_user_id else None,
|
||||
"created_at": to_iso(p.created_at),
|
||||
}
|
||||
pm = pm_map.get(p.pm_user_id)
|
||||
if pm:
|
||||
d["pm_name"] = f"{pm.first_name} {pm.last_name}".strip() or pm.casdoor_id
|
||||
elif p.project_manager:
|
||||
d["pm_name"] = p.project_manager
|
||||
data.append(d)
|
||||
return {"data": data, "meta": meta}
|
||||
|
||||
|
||||
@router.post("", status_code=201)
|
||||
async def create_project(payload: ProjectCreate, db: AsyncSession = Depends(get_db)):
|
||||
async def create_project(
|
||||
payload: ProjectCreate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_auth=Depends(_procurement_writer),
|
||||
):
|
||||
project = Project(
|
||||
name=payload.name,
|
||||
description=payload.description,
|
||||
name=payload.name.strip(),
|
||||
location=payload.location.strip() if payload.location else "",
|
||||
project_manager=payload.project_manager.strip() if payload.project_manager else "",
|
||||
description=payload.description.strip() if payload.description else "",
|
||||
start_date=payload.start_date,
|
||||
planned_start=payload.start_date,
|
||||
status="ACTIVE",
|
||||
)
|
||||
db.add(project)
|
||||
await db.flush()
|
||||
@@ -103,37 +131,51 @@ async def update_project(
|
||||
request: Request,
|
||||
ctx: UserContext | None = Depends(_optional_ctx),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_auth=Depends(_procurement_writer),
|
||||
):
|
||||
project = await db.get(Project, project_id)
|
||||
if project is None:
|
||||
raise NotFoundError("پروژه یافت نشد")
|
||||
if payload.name is not None:
|
||||
project.name = payload.name
|
||||
project.name = payload.name.strip()
|
||||
if payload.location is not None:
|
||||
project.location = payload.location.strip()
|
||||
if payload.project_manager is not None:
|
||||
project.project_manager = payload.project_manager.strip()
|
||||
if payload.description is not None:
|
||||
project.description = payload.description
|
||||
project.description = payload.description.strip()
|
||||
if payload.start_date is not None:
|
||||
project.start_date = payload.start_date
|
||||
project.planned_start = payload.start_date
|
||||
if payload.status is not None:
|
||||
project.status = payload.status
|
||||
# PM assignment (1:N): one PM per project, a PM may own many projects.
|
||||
# Admin / procurement manager only (same rule as the Organization panel).
|
||||
if "pm_user_id" in payload.model_fields_set:
|
||||
role = ctx.role if ctx else (request.headers.get("x-user-role") or "").strip().lower()
|
||||
if role not in ("admin", "procurement_manager"):
|
||||
raise ForbiddenError("تعیین مدیر پروژه فقط برای ادمین / مدیر تدارکات مجاز است")
|
||||
if role not in ("admin", "procurement_manager", "pm"):
|
||||
raise ForbiddenError("تعیین سرپرست پروژه فقط برای مدیر تدارکات، مدیر پروژه و ادمین مجاز است")
|
||||
if payload.pm_user_id is None:
|
||||
project.pm_user_id = None
|
||||
else:
|
||||
pm_user = await db.get(User, payload.pm_user_id)
|
||||
if pm_user is None:
|
||||
raise NotFoundError("کاربر یافت نشد", field="pm_user_id")
|
||||
if pm_user.role != "pm":
|
||||
raise ValidationError("ابتدا نقش این کاربر را «مدیر پروژه» کنید", field="pm_user_id")
|
||||
if pm_user.role not in ("supervisor", "pm"):
|
||||
raise ValidationError("ابتدا نقش این کاربر را «سرپرست پروژه» کنید", field="pm_user_id")
|
||||
project.pm_user_id = pm_user.id
|
||||
if not project.project_manager:
|
||||
project.project_manager = f"{pm_user.first_name} {pm_user.last_name}".strip()
|
||||
await db.flush()
|
||||
return {"data": await project_to_dict(db, project)}
|
||||
|
||||
|
||||
@router.delete("/{project_id}")
|
||||
async def delete_project(project_id, db: AsyncSession = Depends(get_db)):
|
||||
async def delete_project(
|
||||
project_id,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_auth=Depends(_procurement_writer),
|
||||
):
|
||||
project = await db.get(Project, project_id)
|
||||
if project is None:
|
||||
raise NotFoundError("پروژه یافت نشد")
|
||||
|
||||
Reference in New Issue
Block a user