From f0e065abcd3b2aa247f5c57cc90b9ffc64cfb729 Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Sun, 1 Feb 2026 23:46:14 +0530 Subject: [PATCH] Add project staffing endpoints and project detail Kanban UI --- .../api/__pycache__/projects.cpython-312.pyc | Bin 2971 -> 5177 bytes .../app/api/__pycache__/work.cpython-312.pyc | Bin 5336 -> 5636 bytes backend/app/api/projects.py | 46 +- backend/app/api/work.py | 15 +- frontend/src/api/generated/model/index.ts | 1 + .../src/api/generated/model/projectMember.ts | 13 + .../src/api/generated/projects/projects.ts | 520 ++++++++++++++++++ frontend/src/app/departments/page.tsx | 143 +++++ frontend/src/app/hr/page.tsx | 206 +++++++ frontend/src/app/people/page.tsx | 156 ++++++ frontend/src/app/projects/[id]/page.tsx | 276 ++++++++++ frontend/src/app/projects/page.tsx | 93 ++++ frontend/src/components/ui/badge.tsx | 32 ++ frontend/src/components/ui/input.tsx | 24 + frontend/src/components/ui/label.tsx | 20 + frontend/src/components/ui/select.tsx | 23 + frontend/src/components/ui/textarea.tsx | 23 + 17 files changed, 1589 insertions(+), 2 deletions(-) create mode 100644 frontend/src/api/generated/model/projectMember.ts create mode 100644 frontend/src/app/departments/page.tsx create mode 100644 frontend/src/app/hr/page.tsx create mode 100644 frontend/src/app/people/page.tsx create mode 100644 frontend/src/app/projects/[id]/page.tsx create mode 100644 frontend/src/app/projects/page.tsx create mode 100644 frontend/src/components/ui/badge.tsx create mode 100644 frontend/src/components/ui/input.tsx create mode 100644 frontend/src/components/ui/label.tsx create mode 100644 frontend/src/components/ui/select.tsx create mode 100644 frontend/src/components/ui/textarea.tsx diff --git a/backend/app/api/__pycache__/projects.cpython-312.pyc b/backend/app/api/__pycache__/projects.cpython-312.pyc index 6b231b81015ba567c4d1c89aaefca572609ff5be..00467cf493ebc31c9355d3dc6fd3f16f5431456b 100644 GIT binary patch delta 2457 zcmZ`)T}&HC5Z=8z-yPUK+knB?7ziK|r)kI!N!yelRiXK5f+-?I5vZ~ZcO*78M(ZP0 zh>@{cDUDPmpsPgHL{%RuH4%A;8mUU(BK5ISW!FeHC!+MBRn#}Pq%V2u?42DqEM{K_feUn{^!?5$RvyJa9^Y zCA;B4d_Z(=791p5VB6c!x{bu-2C@f7@>Jr`Ly<# zgxWrpn46LBY5T=Mx}EM9=4NHsFOv><3s`mblA;nT@Pwy?mPxlnR3)C&g`}c}Rh5vv zRPz=k`zWcQ}BNUOh?oIZCQs>~Du#Ts7d?hJ>pcP`Es~m@UkC$=uYtO}> z-fIQBd3VpXEw2kbaU*HTNd|P4Mehn}!J8#uR!|CS5oW@fLeqN?T7?|Y6}4)&8GpUa ze5*{-4o9p^0FP<%k=X2uuwb6;fsFE913Zs$SXe2*6ofl)9EMw!J@im#r&yJ5r+BZTglVUWR z(86N`5-UjBhV7M|DF%iBlVYP%MY8pjSvZ`n9KI!o{^Ra2_bscmwy>yQdO#W&b_50Z zAhZT8g>F^iv$4cgMFG(197)6!U8(@aOtx+b%d#R@3UmT?nRpkC!V*I;K&5YlHfPp$ zH%fz*(Tse#kTgGAIIe?}UMV#>4HoafKY10%BKp(r{Vw|GQlWo1*FT)S7%E(pa~I_` zyRvw)$XTzS&7580*SIFi2Q$Gnu7PqxnW3eVYn*Q*%{hyrqv&ldHgpy1JS&0aK+)b% zYzsWGt>Ph5c!XEbgH6)}nF z^8ta-qL>IUR1|4CFDo%cRm}NDqYs#+*K5)*Q)B>uoC0NXd-dJsQLh3Z2$pB4^)(>S z>h^|P<2T0d`0v)_?S~dmu3KG87Ygp~oV)w(-rwE58g)(zUa$xO$G#l&<7O3gic|I&iKC2N>oXCv$fdB zjB4tkbJNuZ<$f62X&W)5hFdx}Uvq2f2dV0n{cU<}C^R*v&JiUPB6P;+!c;h^hG!#$ z(;T%jLu}e?tv794mrfI%Cy6;Imr+Zdqm!2Sshpcl082mXx@9)3PF^Pcz#CK7Xh?F7 zzS7DN_(&ilGsdUrh1eHQ$a68JpNxZo&dB5epd}V#`~-D8Lq~EFI`R~C{e`-ppzf#W zsK#1vrb~>3ouv-Mh}XRtZ`RiNVBnEI*L!Nwo5!cuj}JT^&mSM&L@c&#vWPi@OAN3j z8@2akd|7+fgHINHd3<7hWGp*Al^?mZi6mS{Z3n;>Skt#Zl>Y&wzA~Nw delta 693 zcmY*V&ubGw7@gTocDsq$X7i&pNhwR!Hmjx(Bpyoes$ObO9s*L9%-C*Cvdgyo)Bz-bJ&?#f4G5*~w7MZ;uucF*c+O%0LvHXHwdEj{EWLN8Ds5JK=JmOt3 z`KX(3tzZ*RhYR=v)n29|OvE};*L9()B)bdT7kpniSPL4- zTjLpBo7^&A&`R)}67tZQ_i+cIaPNl00rQe&%Nwn$yg7^*<{7xS^RiMu-~zJ9!BC){ zL#6}4`>`}*E_@*X>dNsHTvT8wxP;)DGBclJKVbjnhRbNm6!!5Yy(j7Y*hIm?JE#TK zO_02Ujc0Fx+T~MdI)U3`K*SJWoe3t|DaDUCv;4Mo$DFS>`YgB&&$s1c`c|7WS)581HC uufcWH@Tp1s3gNp>DLo|{-yG8VKw6*4`LAU2RDD~?dhu!Ihi23Kw9H@if}ckK diff --git a/backend/app/api/__pycache__/work.cpython-312.pyc b/backend/app/api/__pycache__/work.cpython-312.pyc index 2eba431805c393643c0d8b022ab290a6202f6606..e81425b06ff802d1d5a7d216dc88c6c4bef8934b 100644 GIT binary patch delta 2234 zcmZ`)Uu;uV7{BNC_TIL4?d{sSb=}y?-&#iaCkTu=GTc-Ir)EPFG&Hs6!rFE1a_;Q} zDd{jIgco5Rl8Iu}h*7ecV0cjzN?|iK$Pj3kP?DzWw7)vviY?psJ5G6g+iRsFyK-JI5p%(p#+$LOpaL^__s%vea zob%6$&KeDfg6`J+v&5Mn5os&cPP=Dou7}zn*7n+@;RCU>o*j9=sC7JM_Ypnl zqz|{#mWI$Pj|Y++{IK}b&ORCTV@b4F&@DI9)AYC{Gkr3vzilaRr88ss;!zOt#mtya zEj3$6O|arorW@DRxJrb?`G-=`y$(Wzc))!udKt<)Bbc=!3j+0Ig|d)y@n$Xd+-33} zIVqOKob>+^n79plOcLfdz4aL-RGf3wZ}DAXs{Aik2fym}1j}MdshgEau9;5xw^QS;gSB4gTsTv z3~RyD%1f0MAh0;XW`LxNJ&p2egpB}(3m_hkv$gQGR3~Q&H^BNqgT*ku0GJZ~RJD0E zaXzsaS~DM7b0f44W>W)IMLl!k)QN@I`gvvj%|QECuIt+d76OCUJ%cw}I%kSJt=VEO8d~AC)603$OFN;*5 z-m)v#QP(Vgsjn=}sk5>ZN6ou{5KTjOF4%+^fkMX%MKgY+SSnD~4Q?YzV)^xBnS6=r zsZznvO@^(6B@xym;JsN&X@Vj>*cO;2HOqAr43QxA)w7)__3&H1w#sf4dl9hNus(!6 z2-r&*@*}BO3SN0COCO>Kw1Tvmw#b-8CJp2iPz6VgF3uhtx(vZ8wqIirU`qHy4V~M4 zcJ~F7fA5d-X*IG^(avw1S6V+9_%wQPXkqK#`HsC05{6$^r$~a|)!KMp)9;mMAr2cr zz_Qd%4b<1+)K?_76_rmRY(v0qZHdLPk)+I?N5%bG;JZR6=j3>3cm*$_$>UyuZQrCj z=NbXa0sf_blq9YN1D<x1RK}l^6lp!u$wb2zFHDfL&IGt%9urfH!UW4^l;AbE>h3q}sdr7`@;zqQ4G0MNz_P|!|YHP`u%>-}F6S(jy zc(5G+NsqHDn+A`#b}N)FnWIIP(#I$A#pAk;SC3n4Y!DX7EVl=KvboLu23Q>AA2k;% z3AB~$#`2CYTQKA0_z?VQI3>j=35#~8!6(+BmF+B+3tXJ_;DhUuQ>Zr|*4nWG_qDQ} zVii9Zx|-=hEnc}LVKo{4|5(yQ5kzcZmU|*?W=0wElMN%_joMXacpkf649{be`ktp3 z8f+=A_pwpX;bS*;@sGn><=f%m>q3zK8s4upli=SjLENw;g0%D`Pe$77x*c`haO9MP GQU3$`huaVU delta 1792 zcmZ`(U1%It6rOu$X8&gYca#0eCf#6TU6W{&NMlLVNJC?+jaDN_ZM!U)*<}C2-PzLW zwwpkq^u;#bLSKSUKG-H`r1;i1K?{OSiI6x2?W=FmB0_zr=ge%9ZNY*4?%8v{bME=O z_fp?h%KMYY;}YOIz0}CvP?o$=GPrbLNz&T#{d_DOk1dv zyuwdQ34YZk6A$wFH#V7{k~)J#&;xt4I?V^o-LgF%Al~!z) zmf$g_^8|?W10iLbG9*1yEg1G0re$L#ce_Oh@PVV;4(}9v=&e@dKQ%oG6rRHnWPu!-+~}tq+615 zd1`U$7b(1@_!f?_Zk~1al3V<9XH}|8c_)-LMHF6ebt)c+#QgGJEVjQcrX4#fC})U2 zadk-`w3)3qp+xyTmpiQE;Fw>Xs)A~(i}^UVAY`vgtB#g3Hw{ZrW)aHlociNB48>#b zG*lv4uIR~ZrB+sNQbVcg8NF6b&p??Co2u!VTyZ)@43BnhrdU(8bgf*~bhEaGy*966 zkJ$4NLdt81v#^6*Vs?an{!ocEUPAF@go6kJ2!jZCQyI1|WjE~Dk#tUdrhA42{xb$C z802gfeL^SkYj)E*-shLZx8soNTUrsI9UgJa3bJbFzmAB6|a?9G=`(g5&&>R#!$us ztN5zz)F)WXRdw?m=w_*;mGvrn2lkI3@jU>mOruvumu<_RZbW-Gqx@(88xV>&&_fDl zO`J_jXz~PDus(p4!-}dO6$cTXZmJQ?)bzOuOKYY1V&%N1VPnV8u;q3dWM=Pf@-Koh z`2;ZD;&+0TMmKUL^HzW2%9ZtGJvmh=YgS3|R=}ot&3z!ZXl>>xh6pCszW?cc$=qk} zKdUu;d3Uuo^N4P~7Fs)l-8LjFBukp&d<8^&It+O}qtDDS96UCOfXy@u%rF(RR18xw zr}eR86ke`cQG1=80Uf@`)gk_Uc#!`aK6cna{EtL|_H9#;w%Y~jxSUu_tb3Dd;f2Ho fIrQt%<97=iM^8Qyd=_(fn}W2xpEueTB|PLmNcf9+ diff --git a/backend/app/api/projects.py b/backend/app/api/projects.py index 16ad2c8..48b7249 100644 --- a/backend/app/api/projects.py +++ b/backend/app/api/projects.py @@ -5,7 +5,7 @@ from sqlmodel import Session, select from app.api.utils import log_activity from app.db.session import get_session -from app.models.projects import Project +from app.models.projects import Project, ProjectMember from app.schemas.projects import ProjectCreate, ProjectUpdate router = APIRouter(prefix="/projects", tags=["projects"]) @@ -43,3 +43,47 @@ def update_project(project_id: int, payload: ProjectUpdate, session: Session = D log_activity(session, actor_employee_id=None, entity_type="project", entity_id=proj.id, verb="updated", payload=data) session.commit() return proj + + +@router.get("/{project_id}/members", response_model=list[ProjectMember]) +def list_project_members(project_id: int, session: Session = Depends(get_session)): + return session.exec( + select(ProjectMember).where(ProjectMember.project_id == project_id).order_by(ProjectMember.id.asc()) + ).all() + + +@router.post("/{project_id}/members", response_model=ProjectMember) +def add_project_member(project_id: int, payload: ProjectMember, session: Session = Depends(get_session)): + member = ProjectMember(project_id=project_id, employee_id=payload.employee_id, role=payload.role) + session.add(member) + session.commit() + session.refresh(member) + log_activity( + session, + actor_employee_id=None, + entity_type="project_member", + entity_id=member.id, + verb="added", + payload={"project_id": project_id, "employee_id": member.employee_id}, + ) + session.commit() + return member + + +@router.delete("/{project_id}/members/{member_id}") +def remove_project_member(project_id: int, member_id: int, session: Session = Depends(get_session)): + member = session.get(ProjectMember, member_id) + if not member or member.project_id != project_id: + raise HTTPException(status_code=404, detail="Project member not found") + session.delete(member) + session.commit() + log_activity( + session, + actor_employee_id=None, + entity_type="project_member", + entity_id=member_id, + verb="removed", + payload={"project_id": project_id}, + ) + session.commit() + return {"ok": True} diff --git a/backend/app/api/work.py b/backend/app/api/work.py index ae17b2d..02790d4 100644 --- a/backend/app/api/work.py +++ b/backend/app/api/work.py @@ -12,6 +12,8 @@ from app.schemas.work import TaskCommentCreate, TaskCreate, TaskUpdate router = APIRouter(tags=["work"]) +ALLOWED_STATUSES = {"backlog", "ready", "in_progress", "review", "done", "blocked"} + @router.get("/tasks", response_model=list[Task]) def list_tasks(project_id: int | None = None, session: Session = Depends(get_session)): @@ -24,11 +26,20 @@ def list_tasks(project_id: int | None = None, session: Session = Depends(get_ses @router.post("/tasks", response_model=Task) def create_task(payload: TaskCreate, session: Session = Depends(get_session)): task = Task(**payload.model_dump()) + if task.status not in ALLOWED_STATUSES: + raise HTTPException(status_code=400, detail="Invalid status") task.updated_at = datetime.utcnow() session.add(task) session.commit() session.refresh(task) - log_activity(session, actor_employee_id=task.created_by_employee_id, entity_type="task", entity_id=task.id, verb="created", payload={"project_id": task.project_id, "title": task.title}) + log_activity( + session, + actor_employee_id=task.created_by_employee_id, + entity_type="task", + entity_id=task.id, + verb="created", + payload={"project_id": task.project_id, "title": task.title}, + ) session.commit() return task @@ -40,6 +51,8 @@ def update_task(task_id: int, payload: TaskUpdate, session: Session = Depends(ge raise HTTPException(status_code=404, detail="Task not found") data = payload.model_dump(exclude_unset=True) + if "status" in data and data["status"] not in ALLOWED_STATUSES: + raise HTTPException(status_code=400, detail="Invalid status") for k, v in data.items(): setattr(task, k, v) task.updated_at = datetime.utcnow() diff --git a/frontend/src/api/generated/model/index.ts b/frontend/src/api/generated/model/index.ts index 7a5e58b..6e2da78 100644 --- a/frontend/src/api/generated/model/index.ts +++ b/frontend/src/api/generated/model/index.ts @@ -22,6 +22,7 @@ export * from "./listTaskCommentsTaskCommentsGetParams"; export * from "./listTasksTasksGetParams"; export * from "./project"; export * from "./projectCreate"; +export * from "./projectMember"; export * from "./projectUpdate"; export * from "./task"; export * from "./taskComment"; diff --git a/frontend/src/api/generated/model/projectMember.ts b/frontend/src/api/generated/model/projectMember.ts new file mode 100644 index 0000000..2557248 --- /dev/null +++ b/frontend/src/api/generated/model/projectMember.ts @@ -0,0 +1,13 @@ +/** + * Generated by orval v8.2.0 🍺 + * Do not edit manually. + * OpenClaw Agency API + * OpenAPI spec version: 0.3.0 + */ + +export interface ProjectMember { + id?: number | null; + project_id: number; + employee_id: number; + role?: string | null; +} diff --git a/frontend/src/api/generated/projects/projects.ts b/frontend/src/api/generated/projects/projects.ts index 489b332..440a31f 100644 --- a/frontend/src/api/generated/projects/projects.ts +++ b/frontend/src/api/generated/projects/projects.ts @@ -24,6 +24,7 @@ import type { HTTPValidationError, Project, ProjectCreate, + ProjectMember, ProjectUpdate, } from ".././model"; @@ -440,3 +441,522 @@ export const useUpdateProjectProjectsProjectIdPatch = < queryClient, ); }; +/** + * @summary List Project Members + */ +export type listProjectMembersProjectsProjectIdMembersGetResponse200 = { + data: ProjectMember[]; + status: 200; +}; + +export type listProjectMembersProjectsProjectIdMembersGetResponse422 = { + data: HTTPValidationError; + status: 422; +}; + +export type listProjectMembersProjectsProjectIdMembersGetResponseSuccess = + listProjectMembersProjectsProjectIdMembersGetResponse200 & { + headers: Headers; + }; +export type listProjectMembersProjectsProjectIdMembersGetResponseError = + listProjectMembersProjectsProjectIdMembersGetResponse422 & { + headers: Headers; + }; + +export type listProjectMembersProjectsProjectIdMembersGetResponse = + | listProjectMembersProjectsProjectIdMembersGetResponseSuccess + | listProjectMembersProjectsProjectIdMembersGetResponseError; + +export const getListProjectMembersProjectsProjectIdMembersGetUrl = ( + projectId: number, +) => { + return `/projects/${projectId}/members`; +}; + +export const listProjectMembersProjectsProjectIdMembersGet = async ( + projectId: number, + options?: RequestInit, +): Promise => { + return customFetch( + getListProjectMembersProjectsProjectIdMembersGetUrl(projectId), + { + ...options, + method: "GET", + }, + ); +}; + +export const getListProjectMembersProjectsProjectIdMembersGetQueryKey = ( + projectId: number, +) => { + return [`/projects/${projectId}/members`] as const; +}; + +export const getListProjectMembersProjectsProjectIdMembersGetQueryOptions = < + TData = Awaited< + ReturnType + >, + TError = HTTPValidationError, +>( + projectId: number, + options?: { + query?: Partial< + UseQueryOptions< + Awaited< + ReturnType + >, + TError, + TData + > + >; + request?: SecondParameter; + }, +) => { + const { query: queryOptions, request: requestOptions } = options ?? {}; + + const queryKey = + queryOptions?.queryKey ?? + getListProjectMembersProjectsProjectIdMembersGetQueryKey(projectId); + + const queryFn: QueryFunction< + Awaited> + > = ({ signal }) => + listProjectMembersProjectsProjectIdMembersGet(projectId, { + signal, + ...requestOptions, + }); + + return { + queryKey, + queryFn, + enabled: !!projectId, + ...queryOptions, + } as UseQueryOptions< + Awaited>, + TError, + TData + > & { queryKey: DataTag }; +}; + +export type ListProjectMembersProjectsProjectIdMembersGetQueryResult = + NonNullable< + Awaited> + >; +export type ListProjectMembersProjectsProjectIdMembersGetQueryError = + HTTPValidationError; + +export function useListProjectMembersProjectsProjectIdMembersGet< + TData = Awaited< + ReturnType + >, + TError = HTTPValidationError, +>( + projectId: number, + options: { + query: Partial< + UseQueryOptions< + Awaited< + ReturnType + >, + TError, + TData + > + > & + Pick< + DefinedInitialDataOptions< + Awaited< + ReturnType + >, + TError, + Awaited< + ReturnType + > + >, + "initialData" + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): DefinedUseQueryResult & { + queryKey: DataTag; +}; +export function useListProjectMembersProjectsProjectIdMembersGet< + TData = Awaited< + ReturnType + >, + TError = HTTPValidationError, +>( + projectId: number, + options?: { + query?: Partial< + UseQueryOptions< + Awaited< + ReturnType + >, + TError, + TData + > + > & + Pick< + UndefinedInitialDataOptions< + Awaited< + ReturnType + >, + TError, + Awaited< + ReturnType + > + >, + "initialData" + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseQueryResult & { + queryKey: DataTag; +}; +export function useListProjectMembersProjectsProjectIdMembersGet< + TData = Awaited< + ReturnType + >, + TError = HTTPValidationError, +>( + projectId: number, + options?: { + query?: Partial< + UseQueryOptions< + Awaited< + ReturnType + >, + TError, + TData + > + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseQueryResult & { + queryKey: DataTag; +}; +/** + * @summary List Project Members + */ + +export function useListProjectMembersProjectsProjectIdMembersGet< + TData = Awaited< + ReturnType + >, + TError = HTTPValidationError, +>( + projectId: number, + options?: { + query?: Partial< + UseQueryOptions< + Awaited< + ReturnType + >, + TError, + TData + > + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseQueryResult & { + queryKey: DataTag; +} { + const queryOptions = + getListProjectMembersProjectsProjectIdMembersGetQueryOptions( + projectId, + options, + ); + + const query = useQuery(queryOptions, queryClient) as UseQueryResult< + TData, + TError + > & { queryKey: DataTag }; + + return { ...query, queryKey: queryOptions.queryKey }; +} + +/** + * @summary Add Project Member + */ +export type addProjectMemberProjectsProjectIdMembersPostResponse200 = { + data: ProjectMember; + status: 200; +}; + +export type addProjectMemberProjectsProjectIdMembersPostResponse422 = { + data: HTTPValidationError; + status: 422; +}; + +export type addProjectMemberProjectsProjectIdMembersPostResponseSuccess = + addProjectMemberProjectsProjectIdMembersPostResponse200 & { + headers: Headers; + }; +export type addProjectMemberProjectsProjectIdMembersPostResponseError = + addProjectMemberProjectsProjectIdMembersPostResponse422 & { + headers: Headers; + }; + +export type addProjectMemberProjectsProjectIdMembersPostResponse = + | addProjectMemberProjectsProjectIdMembersPostResponseSuccess + | addProjectMemberProjectsProjectIdMembersPostResponseError; + +export const getAddProjectMemberProjectsProjectIdMembersPostUrl = ( + projectId: number, +) => { + return `/projects/${projectId}/members`; +}; + +export const addProjectMemberProjectsProjectIdMembersPost = async ( + projectId: number, + projectMember: ProjectMember, + options?: RequestInit, +): Promise => { + return customFetch( + getAddProjectMemberProjectsProjectIdMembersPostUrl(projectId), + { + ...options, + method: "POST", + headers: { "Content-Type": "application/json", ...options?.headers }, + body: JSON.stringify(projectMember), + }, + ); +}; + +export const getAddProjectMemberProjectsProjectIdMembersPostMutationOptions = < + TError = HTTPValidationError, + TContext = unknown, +>(options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { projectId: number; data: ProjectMember }, + TContext + >; + request?: SecondParameter; +}): UseMutationOptions< + Awaited>, + TError, + { projectId: number; data: ProjectMember }, + TContext +> => { + const mutationKey = ["addProjectMemberProjectsProjectIdMembersPost"]; + const { mutation: mutationOptions, request: requestOptions } = options + ? options.mutation && + "mutationKey" in options.mutation && + options.mutation.mutationKey + ? options + : { ...options, mutation: { ...options.mutation, mutationKey } } + : { mutation: { mutationKey }, request: undefined }; + + const mutationFn: MutationFunction< + Awaited>, + { projectId: number; data: ProjectMember } + > = (props) => { + const { projectId, data } = props ?? {}; + + return addProjectMemberProjectsProjectIdMembersPost( + projectId, + data, + requestOptions, + ); + }; + + return { mutationFn, ...mutationOptions }; +}; + +export type AddProjectMemberProjectsProjectIdMembersPostMutationResult = + NonNullable< + Awaited> + >; +export type AddProjectMemberProjectsProjectIdMembersPostMutationBody = + ProjectMember; +export type AddProjectMemberProjectsProjectIdMembersPostMutationError = + HTTPValidationError; + +/** + * @summary Add Project Member + */ +export const useAddProjectMemberProjectsProjectIdMembersPost = < + TError = HTTPValidationError, + TContext = unknown, +>( + options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { projectId: number; data: ProjectMember }, + TContext + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseMutationResult< + Awaited>, + TError, + { projectId: number; data: ProjectMember }, + TContext +> => { + return useMutation( + getAddProjectMemberProjectsProjectIdMembersPostMutationOptions(options), + queryClient, + ); +}; +/** + * @summary Remove Project Member + */ +export type removeProjectMemberProjectsProjectIdMembersMemberIdDeleteResponse200 = + { + data: unknown; + status: 200; + }; + +export type removeProjectMemberProjectsProjectIdMembersMemberIdDeleteResponse422 = + { + data: HTTPValidationError; + status: 422; + }; + +export type removeProjectMemberProjectsProjectIdMembersMemberIdDeleteResponseSuccess = + removeProjectMemberProjectsProjectIdMembersMemberIdDeleteResponse200 & { + headers: Headers; + }; +export type removeProjectMemberProjectsProjectIdMembersMemberIdDeleteResponseError = + removeProjectMemberProjectsProjectIdMembersMemberIdDeleteResponse422 & { + headers: Headers; + }; + +export type removeProjectMemberProjectsProjectIdMembersMemberIdDeleteResponse = + | removeProjectMemberProjectsProjectIdMembersMemberIdDeleteResponseSuccess + | removeProjectMemberProjectsProjectIdMembersMemberIdDeleteResponseError; + +export const getRemoveProjectMemberProjectsProjectIdMembersMemberIdDeleteUrl = ( + projectId: number, + memberId: number, +) => { + return `/projects/${projectId}/members/${memberId}`; +}; + +export const removeProjectMemberProjectsProjectIdMembersMemberIdDelete = async ( + projectId: number, + memberId: number, + options?: RequestInit, +): Promise => { + return customFetch( + getRemoveProjectMemberProjectsProjectIdMembersMemberIdDeleteUrl( + projectId, + memberId, + ), + { + ...options, + method: "DELETE", + }, + ); +}; + +export const getRemoveProjectMemberProjectsProjectIdMembersMemberIdDeleteMutationOptions = + (options?: { + mutation?: UseMutationOptions< + Awaited< + ReturnType< + typeof removeProjectMemberProjectsProjectIdMembersMemberIdDelete + > + >, + TError, + { projectId: number; memberId: number }, + TContext + >; + request?: SecondParameter; + }): UseMutationOptions< + Awaited< + ReturnType< + typeof removeProjectMemberProjectsProjectIdMembersMemberIdDelete + > + >, + TError, + { projectId: number; memberId: number }, + TContext + > => { + const mutationKey = [ + "removeProjectMemberProjectsProjectIdMembersMemberIdDelete", + ]; + const { mutation: mutationOptions, request: requestOptions } = options + ? options.mutation && + "mutationKey" in options.mutation && + options.mutation.mutationKey + ? options + : { ...options, mutation: { ...options.mutation, mutationKey } } + : { mutation: { mutationKey }, request: undefined }; + + const mutationFn: MutationFunction< + Awaited< + ReturnType< + typeof removeProjectMemberProjectsProjectIdMembersMemberIdDelete + > + >, + { projectId: number; memberId: number } + > = (props) => { + const { projectId, memberId } = props ?? {}; + + return removeProjectMemberProjectsProjectIdMembersMemberIdDelete( + projectId, + memberId, + requestOptions, + ); + }; + + return { mutationFn, ...mutationOptions }; + }; + +export type RemoveProjectMemberProjectsProjectIdMembersMemberIdDeleteMutationResult = + NonNullable< + Awaited< + ReturnType< + typeof removeProjectMemberProjectsProjectIdMembersMemberIdDelete + > + > + >; + +export type RemoveProjectMemberProjectsProjectIdMembersMemberIdDeleteMutationError = + HTTPValidationError; + +/** + * @summary Remove Project Member + */ +export const useRemoveProjectMemberProjectsProjectIdMembersMemberIdDelete = < + TError = HTTPValidationError, + TContext = unknown, +>( + options?: { + mutation?: UseMutationOptions< + Awaited< + ReturnType< + typeof removeProjectMemberProjectsProjectIdMembersMemberIdDelete + > + >, + TError, + { projectId: number; memberId: number }, + TContext + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseMutationResult< + Awaited< + ReturnType + >, + TError, + { projectId: number; memberId: number }, + TContext +> => { + return useMutation( + getRemoveProjectMemberProjectsProjectIdMembersMemberIdDeleteMutationOptions( + options, + ), + queryClient, + ); +}; diff --git a/frontend/src/app/departments/page.tsx b/frontend/src/app/departments/page.tsx new file mode 100644 index 0000000..3ba5a6d --- /dev/null +++ b/frontend/src/app/departments/page.tsx @@ -0,0 +1,143 @@ +"use client"; + +import { useMemo, useState } from "react"; + +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Select } from "@/components/ui/select"; + +import { + useCreateDepartmentDepartmentsPost, + useListDepartmentsDepartmentsGet, + useUpdateDepartmentDepartmentsDepartmentIdPatch, +} from "@/api/generated/org/org"; +import { useListEmployeesEmployeesGet } from "@/api/generated/org/org"; + +export default function DepartmentsPage() { + const [name, setName] = useState(""); + const [headId, setHeadId] = useState(""); + + const departments = useListDepartmentsDepartmentsGet(); + const employees = useListEmployeesEmployeesGet(); + + const createDepartment = useCreateDepartmentDepartmentsPost({ + mutation: { + onSuccess: () => { + setName(""); + setHeadId(""); + departments.refetch(); + }, + }, + }); + + const updateDepartment = useUpdateDepartmentDepartmentsDepartmentIdPatch({ + mutation: { + onSuccess: () => departments.refetch(), + }, + }); + + const sortedEmployees = useMemo(() => { + return (employees.data ?? []).slice().sort((a, b) => (a.name ?? "").localeCompare(b.name ?? "")); + }, [employees.data]); + + return ( +
+
+
+

Departments

+

Create departments and assign department heads.

+
+ +
+ +
+ + + Create department + Optional head + + + setName(e.target.value)} /> + + + {createDepartment.error ? ( +
{(createDepartment.error as Error).message}
+ ) : null} +
+
+ + + + All departments + {(departments.data ?? []).length} total + + + {departments.isLoading ?
Loading…
: null} + {departments.error ? ( +
{(departments.error as Error).message}
+ ) : null} + {!departments.isLoading && !departments.error ? ( +
    + {(departments.data ?? []).map((d) => ( +
  • +
    +
    {d.name}
    +
    id: {d.id}
    +
    +
    + Head: + +
    +
  • + ))} + {(departments.data ?? []).length === 0 ? ( +
  • No departments yet.
  • + ) : null} +
+ ) : null} + {updateDepartment.error ? ( +
{(updateDepartment.error as Error).message}
+ ) : null} +
+
+
+
+ ); +} diff --git a/frontend/src/app/hr/page.tsx b/frontend/src/app/hr/page.tsx new file mode 100644 index 0000000..d5f35ce --- /dev/null +++ b/frontend/src/app/hr/page.tsx @@ -0,0 +1,206 @@ +"use client"; + +import { useState } from "react"; + +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Select } from "@/components/ui/select"; +import { Textarea } from "@/components/ui/textarea"; + +import { + useCreateHeadcountRequestHrHeadcountPost, + useCreateEmploymentActionHrActionsPost, + useListHeadcountRequestsHrHeadcountGet, + useListEmploymentActionsHrActionsGet, +} from "@/api/generated/hr/hr"; +import { useListDepartmentsDepartmentsGet, useListEmployeesEmployeesGet } from "@/api/generated/org/org"; + +export default function HRPage() { + const departments = useListDepartmentsDepartmentsGet(); + const employees = useListEmployeesEmployeesGet(); + + const headcount = useListHeadcountRequestsHrHeadcountGet(); + const actions = useListEmploymentActionsHrActionsGet(); + + const [hcDeptId, setHcDeptId] = useState(""); + const [hcManagerId, setHcManagerId] = useState(""); + const [hcRole, setHcRole] = useState(""); + const [hcType, setHcType] = useState<"human" | "agent">("human"); + const [hcQty, setHcQty] = useState("1"); + const [hcJust, setHcJust] = useState(""); + + const [actEmployeeId, setActEmployeeId] = useState(""); + const [actIssuerId, setActIssuerId] = useState(""); + const [actType, setActType] = useState("praise"); + const [actNotes, setActNotes] = useState(""); + + const createHeadcount = useCreateHeadcountRequestHrHeadcountPost({ + mutation: { + onSuccess: () => { + setHcRole(""); + setHcJust(""); + setHcQty("1"); + headcount.refetch(); + }, + }, + }); + + const createAction = useCreateEmploymentActionHrActionsPost({ + mutation: { + onSuccess: () => { + setActNotes(""); + actions.refetch(); + }, + }, + }); + + return ( +
+
+
+

HR

+

Headcount requests and employment actions.

+
+ +
+ +
+ + + Headcount request + Managers request; HR fulfills later. + + + + + setHcRole(e.target.value)} /> +
+ + setHcQty(e.target.value)} /> +
+