Backend:
- New UserRole.PROJECT_MANAGER with pm_client_ids[] on User model
- New models: Client (slug-based), Team (member_user_ids[]), Project (client-scoped)
- Job model gains project_id field
- New GET/POST/PATCH/DELETE /clients, /clients/{id}/teams, /clients/{id}/projects,
/clients/{id}/pm routes (admin-only client CRUD; PM or admin for teams/projects)
- get_accessible_project_ids() helper: staff→all, PM→their clients' projects,
CLIENT→projects from teams they belong to (with legacy owner fallback)
- list_jobs, get_job, bulk_download, get_vtt_content, delete_job all use new isolation
Frontend:
- UserRole type gains 'project_manager'
- Job, JobCreateRequest gain project_id field
- Client, Team, Project, PMUser types added
- ApiClient: full client/team/project/PM CRUD methods
- useClients hook with all query/mutation hooks
- Admin pages: ClientList + ClientDetail (teams, members, projects, PM assignment)
- NewJob form: client + project picker (shown when clients exist)
- Sidebar: Clients nav item for admin and project_manager roles
- Routes: /admin/clients and /admin/clients/:clientId behind RoleGate
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
80 lines
1.7 KiB
Python
80 lines
1.7 KiB
Python
from datetime import datetime
|
|
from typing import Optional, Annotated
|
|
|
|
from bson import ObjectId
|
|
from pydantic import BaseModel, Field, BeforeValidator
|
|
|
|
|
|
def validate_object_id(v) -> str:
|
|
if isinstance(v, ObjectId):
|
|
return str(v)
|
|
if isinstance(v, str):
|
|
return v
|
|
raise ValueError("Invalid ObjectId")
|
|
|
|
|
|
PyObjectId = Annotated[str, BeforeValidator(validate_object_id)]
|
|
|
|
|
|
class Client(BaseModel):
|
|
id: Optional[PyObjectId] = Field(None, alias="_id")
|
|
name: str
|
|
slug: str # lowercase, URL-safe identifier, unique
|
|
is_active: bool = True
|
|
created_at: Optional[datetime] = None
|
|
updated_at: Optional[datetime] = None
|
|
|
|
class Config:
|
|
populate_by_name = True
|
|
|
|
|
|
class ClientCreate(BaseModel):
|
|
name: str
|
|
slug: str
|
|
|
|
|
|
class ClientUpdate(BaseModel):
|
|
name: Optional[str] = None
|
|
slug: Optional[str] = None
|
|
is_active: Optional[bool] = None
|
|
|
|
|
|
class Team(BaseModel):
|
|
id: Optional[PyObjectId] = Field(None, alias="_id")
|
|
name: str
|
|
client_id: str
|
|
member_user_ids: list[str] = []
|
|
created_at: Optional[datetime] = None
|
|
updated_at: Optional[datetime] = None
|
|
|
|
class Config:
|
|
populate_by_name = True
|
|
|
|
|
|
class TeamCreate(BaseModel):
|
|
name: str
|
|
|
|
|
|
class TeamUpdate(BaseModel):
|
|
name: Optional[str] = None
|
|
|
|
|
|
class Project(BaseModel):
|
|
id: Optional[PyObjectId] = Field(None, alias="_id")
|
|
name: str
|
|
client_id: str
|
|
is_active: bool = True
|
|
created_at: Optional[datetime] = None
|
|
updated_at: Optional[datetime] = None
|
|
|
|
class Config:
|
|
populate_by_name = True
|
|
|
|
|
|
class ProjectCreate(BaseModel):
|
|
name: str
|
|
|
|
|
|
class ProjectUpdate(BaseModel):
|
|
name: Optional[str] = None
|
|
is_active: Optional[bool] = None
|