From aa6b0c807b261f52d16ddd95288a7d114ee64f34 Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Sun, 1 Feb 2026 23:16:56 +0530 Subject: [PATCH] Refactor backend to SQLModel; reset schema; add Company OS endpoints --- .../alembic/__pycache__/env.cpython-312.pyc | Bin 2468 -> 2468 bytes backend/alembic/env.py | 10 +- ...57587037601_initial_company_os_sqlmodel.py | 32 ++ ...nitial_company_os_sqlmodel.cpython-312.pyc | Bin 0 -> 1026 bytes ...9c1e3c2a_company_os_schema.cpython-312.pyc | Bin 0 -> 11517 bytes .../versions/ce4f1502f674_create_tasks.py | 43 --- backend/app/__pycache__/main.cpython-312.pyc | Bin 1209 -> 1857 bytes .../__pycache__/activities.cpython-312.pyc | Bin 0 -> 1725 bytes .../app/api/__pycache__/hr.cpython-312.pyc | Bin 0 -> 4428 bytes .../app/api/__pycache__/org.cpython-312.pyc | Bin 0 -> 5298 bytes .../api/__pycache__/projects.cpython-312.pyc | Bin 0 -> 2971 bytes .../app/api/__pycache__/tasks.cpython-312.pyc | Bin 3319 -> 0 bytes .../app/api/__pycache__/utils.cpython-312.pyc | Bin 0 -> 968 bytes .../app/api/__pycache__/work.cpython-312.pyc | Bin 0 -> 5336 bytes backend/app/api/activities.py | 30 ++ backend/app/api/hr.py | 61 ++++ backend/app/api/org.py | 79 +++++ backend/app/api/projects.py | 45 +++ backend/app/api/tasks.py | 64 ---- backend/app/api/utils.py | 28 ++ backend/app/api/work.py | 80 +++++ .../db/__pycache__/session.cpython-312.pyc | Bin 548 -> 944 bytes backend/app/db/base.py | 5 - backend/app/db/session.py | 15 +- backend/app/main.py | 23 +- backend/app/models/__init__.py | 19 +- .../__pycache__/__init__.cpython-312.pyc | Bin 210 -> 594 bytes .../__pycache__/activity.cpython-312.pyc | Bin 0 -> 1035 bytes .../app/models/__pycache__/hr.cpython-312.pyc | Bin 0 -> 1836 bytes .../models/__pycache__/org.cpython-312.pyc | Bin 0 -> 1446 bytes .../__pycache__/projects.cpython-312.pyc | Bin 0 -> 1262 bytes .../models/__pycache__/task.cpython-312.pyc | Bin 1602 -> 0 bytes .../models/__pycache__/work.cpython-312.pyc | Bin 0 -> 1929 bytes backend/app/models/activity.py | 19 ++ backend/app/models/hr.py | 35 +++ backend/app/models/org.py | 27 ++ backend/app/models/projects.py | 20 ++ backend/app/models/task.py | 28 -- backend/app/models/work.py | 34 ++ .../schemas/__pycache__/hr.cpython-312.pyc | Bin 0 -> 1279 bytes .../schemas/__pycache__/org.cpython-312.pyc | Bin 0 -> 1580 bytes .../__pycache__/projects.cpython-312.pyc | Bin 0 -> 780 bytes .../schemas/__pycache__/task.cpython-312.pyc | Bin 1599 -> 0 bytes .../schemas/__pycache__/work.cpython-312.pyc | Bin 0 -> 1409 bytes backend/app/schemas/hr.py | 24 ++ backend/app/schemas/org.py | 31 ++ backend/app/schemas/projects.py | 13 + backend/app/schemas/task.py | 35 --- backend/app/schemas/work.py | 27 ++ backend/requirements.txt | 33 +- frontend/src/app/_components/Shell.module.css | 24 ++ frontend/src/app/_components/Shell.tsx | 43 +++ frontend/src/app/globals.css | 53 +--- frontend/src/app/layout.tsx | 20 +- frontend/src/app/page.tsx | 293 +++++++----------- frontend/src/lib/api.ts | 24 ++ 56 files changed, 867 insertions(+), 450 deletions(-) create mode 100644 backend/alembic/versions/157587037601_initial_company_os_sqlmodel.py create mode 100644 backend/alembic/versions/__pycache__/157587037601_initial_company_os_sqlmodel.cpython-312.pyc create mode 100644 backend/alembic/versions/__pycache__/5c0c9c1e3c2a_company_os_schema.cpython-312.pyc delete mode 100644 backend/alembic/versions/ce4f1502f674_create_tasks.py create mode 100644 backend/app/api/__pycache__/activities.cpython-312.pyc create mode 100644 backend/app/api/__pycache__/hr.cpython-312.pyc create mode 100644 backend/app/api/__pycache__/org.cpython-312.pyc create mode 100644 backend/app/api/__pycache__/projects.cpython-312.pyc delete mode 100644 backend/app/api/__pycache__/tasks.cpython-312.pyc create mode 100644 backend/app/api/__pycache__/utils.cpython-312.pyc create mode 100644 backend/app/api/__pycache__/work.cpython-312.pyc create mode 100644 backend/app/api/activities.py create mode 100644 backend/app/api/hr.py create mode 100644 backend/app/api/org.py create mode 100644 backend/app/api/projects.py delete mode 100644 backend/app/api/tasks.py create mode 100644 backend/app/api/utils.py create mode 100644 backend/app/api/work.py delete mode 100644 backend/app/db/base.py create mode 100644 backend/app/models/__pycache__/activity.cpython-312.pyc create mode 100644 backend/app/models/__pycache__/hr.cpython-312.pyc create mode 100644 backend/app/models/__pycache__/org.cpython-312.pyc create mode 100644 backend/app/models/__pycache__/projects.cpython-312.pyc delete mode 100644 backend/app/models/__pycache__/task.cpython-312.pyc create mode 100644 backend/app/models/__pycache__/work.cpython-312.pyc create mode 100644 backend/app/models/activity.py create mode 100644 backend/app/models/hr.py create mode 100644 backend/app/models/org.py create mode 100644 backend/app/models/projects.py delete mode 100644 backend/app/models/task.py create mode 100644 backend/app/models/work.py create mode 100644 backend/app/schemas/__pycache__/hr.cpython-312.pyc create mode 100644 backend/app/schemas/__pycache__/org.cpython-312.pyc create mode 100644 backend/app/schemas/__pycache__/projects.cpython-312.pyc delete mode 100644 backend/app/schemas/__pycache__/task.cpython-312.pyc create mode 100644 backend/app/schemas/__pycache__/work.cpython-312.pyc create mode 100644 backend/app/schemas/hr.py create mode 100644 backend/app/schemas/org.py create mode 100644 backend/app/schemas/projects.py delete mode 100644 backend/app/schemas/task.py create mode 100644 backend/app/schemas/work.py create mode 100644 frontend/src/app/_components/Shell.module.css create mode 100644 frontend/src/app/_components/Shell.tsx create mode 100644 frontend/src/lib/api.ts diff --git a/backend/alembic/__pycache__/env.cpython-312.pyc b/backend/alembic/__pycache__/env.cpython-312.pyc index 5563bc578a57388e1bdd1564713288dd6e19c338..ddf714ab5f1aac98ccd03a0e91c87da23141344e 100644 GIT binary patch delta 140 zcmZ1?yhNDyG%qg~0}xbCtj{di$UB`;m?JpQ$2UJEHAj>27F#ZeQ9OAg<3>jD&B;vr z85yNFE3+svGiq#hWsd+VE-uUgt103H8gh#{v7lh`E{;5fav<*mGb1D8T?VD64608V kr0+B6KV?w4%OLxeje${ovKyy5yYzJ-<%>eflZ!Zw05a|;)@LScnkk%^txh%bi$IpqG-QmjqN@#0fOv78gh)H$Npcr+D% str: diff --git a/backend/alembic/versions/157587037601_initial_company_os_sqlmodel.py b/backend/alembic/versions/157587037601_initial_company_os_sqlmodel.py new file mode 100644 index 00000000..bdf86a77 --- /dev/null +++ b/backend/alembic/versions/157587037601_initial_company_os_sqlmodel.py @@ -0,0 +1,32 @@ +"""initial company os (sqlmodel) + +Revision ID: 157587037601 +Revises: +Create Date: 2026-02-01 23:16:42.890750 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '157587037601' +down_revision: Union[str, Sequence[str], None] = None +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### diff --git a/backend/alembic/versions/__pycache__/157587037601_initial_company_os_sqlmodel.cpython-312.pyc b/backend/alembic/versions/__pycache__/157587037601_initial_company_os_sqlmodel.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2f3445e9e4329ff0e8d49fc72166f744c193144b GIT binary patch literal 1026 zcmah|zi-n(6h7Pe;kfzHRuqIlV5v(3N}QymDN)5j8Ca@{s*9J?#dkCY|48qg7FR+< zh@oRU6}FBb@t?4?>Vl3DF|b7xgw%;UHx&h`aFXA>Pv5(H@B3_jC=?7J*2~=&?hh4! z&k~%i)F?TAFOog53NWw%Rq!Rq5&;ekzEzuT&DH9s3}J24xK9a+h`A_2+icVt zbCp`7QmdPd*>-)deWy{KzgufHYliVvr~ze_>nmivPXdSV^lBh3FDqOPdrLfjz5&H0P!jcNuv8xBSqO3HK*%#u+;hbt=_Hj- z^6W@1O-p15Ey$B@#$08H%Hg@jBFak;q96z(6iH7Yq`JH!CxiT#61~YBe~o#fFNm+N z?~!}-ny6F6!(tJJnxZJ*)Qpn)Sp>@T7jXF#xH8ni#La=eJ$(dje5^MIh3)>%%Ilf^ z**BH9mky>Xho$NfX#Rt#HU`G_gPrM}XZzaj^L>0UdFwEL`v^3SGn$h7UKAm*=NGG0 B35@^% literal 0 HcmV?d00001 diff --git a/backend/alembic/versions/__pycache__/5c0c9c1e3c2a_company_os_schema.cpython-312.pyc b/backend/alembic/versions/__pycache__/5c0c9c1e3c2a_company_os_schema.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5dd817ae90ec71e209ede49897b70688472f3090 GIT binary patch literal 11517 zcmdTKTWk~A)po|t*s%$ikZfKsyfzrf%ORu*39rOJAUJFuuxrA4$DT>zV0*&M7;w7V zf>f#7kE+?P5`hE}NCYJkK}8~HQAyR6O4Ul8$b&%B{cHPy)Q?r6T4}#}?%3npu!G}7 zf!$j2^?jUs-sjwx|Kf1i4Df0F_0L1WGK1kCsG)s`RORIqRDNsdH82LBfiW_surXrt znIbv993yJygo%jRXIAS(*y77&%s!H}vNp!j3^SQrUmlwSZ4+xPMs1=$2mQi0sg4sq z`y0lssd2{}#%=1jgUR~^;akNx7(4vt#|hV}k-i`o8K$FgiW4XyILJn5o2`pI9}+@b zlxlBlqUwV+!Ny=MTNiZG$q`m)qJr$M{@VH)cmM8&UA9)9rA3x%gQAIY*SL3A*0?Kc zYHhX`z`em`Qpj%hT#StdS;gEN1;j3+V&aCqiXBI;kbH`+V#m=d4u-oB^`{3N0iKQq z2mN6>z=j3I#<0U|lo9+~G>FJ)A173D;QR9TMaU?kQ5zUCX|?%Z#weP#){sT3&Hpkc zY@D3$*J_ig3X?NMOd_iA!Di@TG-KncYb9rCC1aV&olNn(1gs>6H4~p!T|Vs^_4!}M zq;Y{^UNt|i)flq|H<^;wxR}mu7)Ct}{lDlu50gXBlJ;d;(t%;kU`Y&WwPuU?UuKnN z6$V*6m8h^edV0^t#+Ofd^)e{0!6@r#5|0Ue57v{%xd_&^xP6#{sdYMf$`~1w)}+Q4 z_4q=J6FpAfy~h{fvg_sqJwD&MMln6DOs;tk!>A|G%4@zDqqzJiDoIBXT3ND--_jMK zJRiT=Q!c|OFE4&G#u%Ssqzp$_CW4Gwm&E}iU^G1@2dk{B0VRkveY=xKiCbNdgMVRvA%X7Ef5N^e)V zocP^|;na)Y1-q!McM_Et)|H8hAB*>1R$&@=H{M%~QCtZh%Kl_ggHhJ=q1t8b zM%@_J4DzTapzO}(br{B#<8zm&T@)FH-ur&G94gW2EB}5U##@GnnW|6c-t1SDA4}|H zd!E{j>EhkQP6I|UgRfywdinP!Y1?GKb8EzKE}!yz=FdKAn=r~5JZ=fMR6iC!(>o!o z|9-?VdoT^?`B|1{Jv;+Vrfwk@tPjhf-Hh>;!7ttl?Uo;q_TCkuP3hQsAG3!kWQqsi z3V*;jnYtNXU@5&>`(c)iX1D7feRP#FNm^k9H>AliIr}Z*NBjJR>^Tiy`pC=zwZB)vHFnwNa@L&PBQtag!7gIB7l5 z?MaWIS-^o;8*cZ4@FK@>Aa4)quL#h=vte$aBJ&i61BTLbLQ!PB0iG3vMdwA4YLlcG zh?#}(o{nx$inTO-kGCydcdg~ox&%+9Y5;=gpgd?2XvNaXg=3MZlG`2?S;$t3rCa2I zN2TBdABxa?{1_W=h2Rr;IusR^)$k$`e9pZQvycysWq6fl6A<@%r55%@Yps-O$ zCPar(?t(1E2_h2rTv#z5?e6p*gg|8&h$fW>XNtT2Bu4*PK$**4WP>r0RqS+FgxE-e z-Z8ucaSY#ust36UTTKh}APw)l)hFUXs6G**zf>PGc4Z+gxA4WI0YNRONM_EmS1*W z?tD(J`$Wl5Rr^FtChN7{_pfcb?izDV)X62)Gh1bHe`-K-Zn+h> zJv2UaGd$BHlLw*u8M$GO+;D?PkXzMO=^R;l>p`+-b$y~-y+IfZHElxe8+mGcH$ zej<}}YNF)aG1Ya~cgHu`_sIRY;QG+m(ADsC?~^*2?8GNDj5pkDoEBxWS?hh>Qn-o`?Y!~4=^mMC`m6hI4S#Ns zx_yabr=|Y0G8qO8xaSnvK{;jFDrZsqNKV(3`)<>nrpe|<&d22s%V&RRBNCfEJ)9Y;yI#ND%m+PFg-HcF}oK?NZlzC+FqFeDR0z(JP7bYsnHWam zsEzY7aUnSd3+3qHD}4}AB@au!ER#<3J|#}3r+s50t@i)sz!JdwV1Zrm2Q9RHQU9Rz z6sT@Lc-J(hPs z;Ubdz|9pn$hGBo3-p}WyI@JBQO>L-W1O6j46-6O`8VbdV{#BXW|Dv$`0=~k(p}1sF zQ%2z&Q79GFOjpd7Jte$YSJiax%%@L@j?7)IiH4b-Pl>~5FYUeT9c_@xcF9CNN+7AG zTPEt>)V$Im6D~9rrPpei*n|L4eWy%pNY!C2kckTY#%7t=qD|Q@6XjZchfJ*3>XlCk zN>v9OEG;}M#`vh$wOYyb`x!3i_bV2#7@V4Tl%jYPrAlt#5*l72aZ8z%1 zDklo0b(M4ZRSCns=e70vBlXX#+?Q;hdqy{pUc8a>#b-Ad$+>;bz9V69zqG`SM$4;< LIZoq7RYU&+f1?H{ literal 0 HcmV?d00001 diff --git a/backend/alembic/versions/ce4f1502f674_create_tasks.py b/backend/alembic/versions/ce4f1502f674_create_tasks.py deleted file mode 100644 index e505b1c3..00000000 --- a/backend/alembic/versions/ce4f1502f674_create_tasks.py +++ /dev/null @@ -1,43 +0,0 @@ -"""create tasks - -Revision ID: ce4f1502f674 -Revises: -Create Date: 2026-02-01 22:23:55.940851 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision: str = 'ce4f1502f674' -down_revision: Union[str, Sequence[str], None] = None -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - """Upgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - op.create_table('tasks', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('title', sa.String(length=200), nullable=False), - sa.Column('description', sa.Text(), nullable=True), - sa.Column('status', sa.String(length=32), nullable=False), - sa.Column('assignee', sa.String(length=120), nullable=True), - sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), - sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), - sa.PrimaryKeyConstraint('id') - ) - op.create_index(op.f('ix_tasks_id'), 'tasks', ['id'], unique=False) - # ### end Alembic commands ### - - -def downgrade() -> None: - """Downgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - op.drop_index(op.f('ix_tasks_id'), table_name='tasks') - op.drop_table('tasks') - # ### end Alembic commands ### diff --git a/backend/app/__pycache__/main.cpython-312.pyc b/backend/app/__pycache__/main.cpython-312.pyc index acfc3e257458c19bc45a53459b2917e5cbb033ae..49a249ce96b508686bea9a84c38a31a918dc9db8 100644 GIT binary patch literal 1857 zcmah}%}*Ow5TCcZ{=i>=F&GGOXdy+8s0~F80@74zqDqkxf|{HxRo3G7uou?5*4s4* z+NeZIRVt+lIi>$XL=HXn*c_@JdVyjU`Xo!K+8lbLp_g*%?AmLnNZKd8(QjscZ{Ezj z+5IsZ4FRrC9(|?_DFDAYinjs{Kn@oqfX6_A5*^AUM~H(ZT`J2t*~PN1lzlm$ixu5p z4&;JO+=e*R#9_pdCXOPGHE}!Q4j23M&T>2#cd=jZDkpLY0unHp259hU+ZN{GtXwyX zFOD|wNx7IksWmVSZIK4L1`nYi6>)z&1aKEVg~jpH{H>bmaoAffc(kRd>ZrerCqfBzT)MR8$kZdX+Dd1d^h5& z7_Eq}Wpw;+j8+$48zcCy81J3Vl(uf6o%f+t*SXbrK_YG+S0J7E!#S8HBCHyQX{)wo z8WyU74^_*)GIt$e?CPzLZr{`>)!B;58Cv|@tlEqt3R=vzHKS-f!6TZX*?Bs@)^)4G zjH|l3GI*uPjKb<5##!@?WhOFXX-W8O&DNO+EHiH5Wk+Q6jvyD(iioPZZm#4_t`#-I z5}ggTz!_zRt*N@@G0My?nY5*rm`X902w1kt?P{fPvK7t*%K+rC7r|q&o3jb6eh^s2 z@@IJv{ZHV~IeYX$wq%xBR<+cU%2gvfw^~3@p5x{sE7(@n#QF+Y-`lENUv^$CEHQ&- z)k-B>Ry8A2SrsAE$Tt(>Nep@hx8;1{>+ri0{`@-cM!XitV)nXSdUejuuLA!Agi0E@ z4QY8+NaoTVo@p(zcRx@0-Q``#H0#+z`sF zX|VL52<7t&RkW~to@1v#1Qzh=)r!XP{{$l0)H3DP1Y`;(x483Ni8z*=L2gDZ*xIsY zYs}()$I*Fg$Ty}%ggkDEi?$N?%wf-L@*;MP7d=OZ=cqLMF4{fhQ6e5?#pFvO>`2em z?zk{fhQe6TirneFh&fg|pRt(b3?FZEj4~QU#3`yZd_Jq9Yqs!NHrFa5;LT)Ar8Ixs z2oc0bX3IF9?h}ichGS8s%*#P8GTVI-{8ET%IkPwBKG#w3QrV6QP5l(Qu^#wE4(%&}J*8(y>DdqV?Dw4A z?@#R~`!|^@4#MC|{sn%Y{~!*M?~8$Z0~^{d>3t~?5<8SZ>N{}IK@f*SP-00gSU4LTiUgwsCi~fE&4&j@` R9ph)i-Pbb8v*|d>e*h;P!khpA delta 534 zcmX@ex093aG%qg~0}v>;)@SB3P2`hcDq@_dVV=sG!2OlCCFl5P2VpL=V5>k_G z7`Zw5Q~8S|Qn|AfCkHT!O7o|1rEsS+g76xi)vORR7#V=_%9Aaaj_oO2S(6&%x3^O7qS90NS7SPk?H zC;wxLoy^Z-%C?fBNQi-9@_c4*O}32G#GI0hB2l1`AfFfW0Eq^M2Rs53R61-fu!t84 z0|opvMJ7*Tu`}UKEGW=REXdR=Ni5DTE@A^p-QoeW} diff --git a/backend/app/api/__pycache__/activities.cpython-312.pyc b/backend/app/api/__pycache__/activities.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5bdda4efbb77531f9c9ce9322aa7b5d4f45fd0bc GIT binary patch literal 1725 zcmZuxZ)hAv6rcTbw|jegxi-<5i}upiCiH~nAgK^j6>3^RJxgo?%9XHeZYSxUyL;#C zq~_#;Q46*pl=>wG3Q;H+iC+Xk^oxRD3i=`80yzeXl_HXFslhM(D^RIK>oBm5+$vk#>v? zsckt)m>0tb_MGgb9P?^yqvKpm*W_H+F1Kt)?KX6A$czZXGVvu*O;@MsUJ3M39+)&vvj$g8YJtUzTR z^N}G^b+Bsc5eG17T5yg;4+~s}l8aIu=*}4qR_hVokrok`C85u*s;I6^p`3bQm#1;}4v3t_WY=Mbp|>i;!hY4O)G)SyO5%V*4_dTV z#VpY=fLE_zGRw58UGplo!;BKaHpPx*(_9Zz@ggn}-Z&%yg6S3FU}DYINH;fC2ZtpV z^IYGhO!4hS5N^N}+w)l5uJS8Fctyz*T<~fUUIu1%qFLn-E{@ugF&;i7ydb$4;kjm7 z=MJ%$OR*m^1uh?x?2x}JNBMYI$X~AbIB$pcWt-SRezH~q;7^iD8LncOufP^cp8ftQ zdmcb7KWmrXg-zvQZ}7SQyN<)v8cTa_NUi8vk{kr;f-n3P#4@^P_S`63FWl`)~~Cmv(j)H=u-f8HscD5Hl=mZhXQKOy zQ|tBhU_0^Ba(pk2Wc}0rkNU5@@=3ON?Aaf+Uy_Zn!ahn%>3s#shaSj)TEYGU_mPFf literal 0 HcmV?d00001 diff --git a/backend/app/api/__pycache__/hr.cpython-312.pyc b/backend/app/api/__pycache__/hr.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7d8b6a85b70974eccf882e11ac6740ea2b598b63 GIT binary patch literal 4428 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 literal 0 HcmV?d00001 diff --git a/backend/app/api/__pycache__/tasks.cpython-312.pyc b/backend/app/api/__pycache__/tasks.cpython-312.pyc deleted file mode 100644 index d868344a2da804409b34d18346a11047623ed4da..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3319 zcmb7GU2Gf25#GIDlE+_(Rvi5#jOa^bhvX8lorQ#_JfNTS!MbHj#zX7&DnO zIg3y8mXH=KF)g|}-jpmkExWQ{`YeCi@5-VXu#~jo%90thLg|ndPKT{XI)V|03|^OY z-xOZ@7LPd6XhZV)NFM~t{0Zi~?cF;ZQ9uu!7Xl;LFb3DN~&LvYzi znEr*ntf2e;eQ!KbdA`o^VrMNJeckuK7r5?Q@VyoIvdVeFS9Tn{uPP(>ZQ%fVCpLmV z=s3#%fgiz?l{ridy8tCNGFDzUOl>oN@Dj@};W&IdcA;nyvb6Mpgd1&qYLHU`7m4Wxd`){)8Tvl?4VYi!oLA6fw#hTFSg%BZ!$%<^U44mq2Rqhp<0 z=3CaQ7%CyRKC*S!g=Mi-%~_!=?A1nN=*Jxh{RmJuE6z&JCNj!=Su9IS&A-6t&A7nU zy$deNeCu0)9Bwycv>}Jfv4-3+wng9pInudLZ$hRXhQ}eB3D|BDllkI|ewU+)2c3)t zq(mj%aMY|h4s;bVu4raaH!?+_EGOxwJaD52Dwcd2a9J#)7p(#nvyNfeR0c=YaR~W3 zIFJE09fBz-Q@L40!uZt17x@NAKozQ^QhbTxpV%zz05vw6a4Wj|ln1x(fRzQaufpW9 z#qh%L&4ah)H6>9Q+fX9+!##`q%8{{q;gc)AlMj@Rb)|1r>02CIQTo=DXDeg1X#7^- z7lC(!pCc^xE(ldzefGXDaznVG-j4m%*Hh;~U6&pmo!c0Qu>sT;H{JsQ5Q!fdkzASk z{}H*38}MFugyWi^i&^ZVfEmtd^4S^3vj?}^1@8~OyItEu1DVWZ(J2xmlOe1vsXVDdHx#mj6_YTI@bVG1 zUollvn>MV{V4hfnt%Zi+^f0JT4BFmF%sZS!n9}VGdyrw?+aIG$YqR&&rrP}jMh%3m zl}f}t@VVy>w-{7lJEglvQlXFsk?}1R3#y|{6V?pmIGb3;?;#LU6aPP)5L%%z6i@ZFo>pMhEBSg)M=UwoDJjGMK1X*- zZ?s-HmAd7h;>a%HRVxU>#7f*Sh_}&~H-Z#R=?RTvj3LxF?8H5ClOh~hsTW#~O-3Dq zinfYBG9XtGf)mJ~KUp0o36qRD0%RnUGC5N+^|+mg`a=5J1pTv_dN$rEAP#&cXXETi zlt2nyX}hcPre!A~GpQeWVM?j#I=o7Q1e-RKG@`sbb}+6SQMSixVj85fzjLMHgKnouQFv8d`4a-28rP6*ovaS!X*8XhDw4al%Fn+ajr;IVX_d`m*h~cJdTcRyz1jcE^=m zV>B7CfGc93ZiQD&^Vt9+)HIp6cQ%EJYj_oIu+J3X=<##U4;|_i&G1HJD$@O9JUI)6UBRRbYA{uL@LyY9p@;)PTlvE-LeB&??tWL`4sO0OZ~1ga7~l literal 0 HcmV?d00001 diff --git a/backend/app/api/__pycache__/work.cpython-312.pyc b/backend/app/api/__pycache__/work.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2eba431805c393643c0d8b022ab290a6202f6606 GIT binary patch literal 5336 zcmb7IU2GKB6}~e&`(uyy$3NJMe-axUU}M9^galM*0>b8}q{azGMC&BWurs!Iz2n{7 znFS2%)UJ?Xl&Uc*5>!HJP$R_zKjfj6^4Pv1^<`IUx-cRlRTEWtb81zUP#=2Eof+@g zEH1s$-h1x7=bkxp&-u z#)4`n7GiT>x?XLFHK>iTMztx{#1Wn-f?PAkE!N)UOpRijlufCfwwjeu7L^tyxEU4} z>+XWi*knJPcPoDT>?kic%YiHIyXH2Dg+bP<*2FP@tYAgTIvJeYA_pBZT0zEM$6T!?I2@+H5s<%PwB8(Zg8A`;Td<*2zL@%dnZ*SH8~``_>3EAR6-lvNUOZb zA)|B3A8GpmUnjyDqBJ>~(Iq{ZnbctFlO+^IjgI1r994clTOtwKfcF>Fn%$ zawIyQQI)8qN#hcgCZj`B2_V!VQ^)d9IpoD|4HI)v!%hM%i_XUli_I zSa}Z3`BNm~8AMD&Z15%+>g5P;`kGr$>S^VU+o*@AlA&=$&89O`ih`agLi~U*6W6D* zicw=t&|}Xl^qk?%N>k~KB;O$szv=I_EI7pFoSLP0^@b;>Cnhr&jM^LvFLGSc!O>;e z@FXBQC3V9~l~D*S;}m_uXts0%#5nb!EwIV7r7r;mDCxn44(zrKsZ&~Os_Z}_w9xa?6DU3~&$0mzolZD(z%Y?kkzsLU@ zhcW&~bAUz4pT|WCmWD-ZIL=S=|02Zw5$|}A7@RplyFlGbAv63&FQehW9@7aYGJL?o zQ1m535%|h#+=U&hRia;>*pPcW2tlA0nLFZ^DH!0uY4N-$yA*aO?kYWvwtmslQJq*d$uUt%|bFvc8K?2s9 z%Nt$`QFI&V6bTqEr~?=XDcX)=k!ZM+x}s{BO+cT=R3U+xbQL_-TKHZiajd$zlK1{nz!IXG-Er zGyM<6;G;m>f~&Bne+k#8mR+#?-5TtE`@GowdbBV!Tpl`896D3@V65$oygx<6RZy1gC@*tER!fQ zJqo(hUg#n|%FK-^rCQD~8l;>)zP55?ws+QP2A5b{r8PRLG}{sA+wh2E@Y6=2gKEv! zSoHPGkCmHu6q|RHnqP+5%pn9Y)@R(6K?jzt^6b!(&}?GCDHlN7B^0{5s=Oh(@+X&(!JFV@)FG4e5qlt|WZfS{eMU|{Q zrYL)Jd_BWM*I3GnSDSSLN)^=7Tbn9E9r@G+7C26jvWSahHz$H=2Gm^S<&#s zcK}q$rIk162f&BG%hEtw>&bOuD}(+rA;&9+RDq$&n&C^0%6X zo6R?UOEq0Ho@Ec=eIK{Zw$2^ None: + session.add( + Activity( + actor_employee_id=actor_employee_id, + entity_type=entity_type, + entity_id=entity_id, + verb=verb, + payload_json=json.dumps(payload) if payload is not None else None, + ) + ) diff --git a/backend/app/api/work.py b/backend/app/api/work.py new file mode 100644 index 00000000..ae17b2df --- /dev/null +++ b/backend/app/api/work.py @@ -0,0 +1,80 @@ +from __future__ import annotations + +from datetime import datetime + +from fastapi import APIRouter, Depends, HTTPException +from sqlmodel import Session, select + +from app.api.utils import log_activity +from app.db.session import get_session +from app.models.work import Task, TaskComment +from app.schemas.work import TaskCommentCreate, TaskCreate, TaskUpdate + +router = APIRouter(tags=["work"]) + + +@router.get("/tasks", response_model=list[Task]) +def list_tasks(project_id: int | None = None, session: Session = Depends(get_session)): + stmt = select(Task).order_by(Task.id.asc()) + if project_id is not None: + stmt = stmt.where(Task.project_id == project_id) + return session.exec(stmt).all() + + +@router.post("/tasks", response_model=Task) +def create_task(payload: TaskCreate, session: Session = Depends(get_session)): + task = Task(**payload.model_dump()) + 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}) + session.commit() + return task + + +@router.patch("/tasks/{task_id}", response_model=Task) +def update_task(task_id: int, payload: TaskUpdate, session: Session = Depends(get_session)): + task = session.get(Task, task_id) + if not task: + raise HTTPException(status_code=404, detail="Task not found") + + data = payload.model_dump(exclude_unset=True) + for k, v in data.items(): + setattr(task, k, v) + task.updated_at = datetime.utcnow() + + session.add(task) + session.commit() + session.refresh(task) + log_activity(session, actor_employee_id=None, entity_type="task", entity_id=task.id, verb="updated", payload=data) + session.commit() + return task + + +@router.delete("/tasks/{task_id}") +def delete_task(task_id: int, session: Session = Depends(get_session)): + task = session.get(Task, task_id) + if not task: + raise HTTPException(status_code=404, detail="Task not found") + session.delete(task) + session.commit() + log_activity(session, actor_employee_id=None, entity_type="task", entity_id=task_id, verb="deleted") + session.commit() + return {"ok": True} + + +@router.get("/task-comments", response_model=list[TaskComment]) +def list_task_comments(task_id: int, session: Session = Depends(get_session)): + return session.exec(select(TaskComment).where(TaskComment.task_id == task_id).order_by(TaskComment.id.asc())).all() + + +@router.post("/task-comments", response_model=TaskComment) +def create_task_comment(payload: TaskCommentCreate, session: Session = Depends(get_session)): + c = TaskComment(**payload.model_dump()) + session.add(c) + session.commit() + session.refresh(c) + log_activity(session, actor_employee_id=c.author_employee_id, entity_type="task", entity_id=c.task_id, verb="commented") + session.commit() + return c diff --git a/backend/app/db/__pycache__/session.cpython-312.pyc b/backend/app/db/__pycache__/session.cpython-312.pyc index 450a171a98a21430c99b7cb6ddcdcb7ad008dca1..7d7d28362e8b937a41e70d7459061dea9b747208 100644 GIT binary patch literal 944 zcmY*X&1(}u6o0dyN!qj(YW;n zj3&%wQMDE^ypj(5^7r`y0f|3kr0VF4qjHs630o;e@N@vi0 zoPbdmr3kZd2?S{=1XeMVjBKi_&?0Zu7U8lqOW|^ycwS*{NrRC+81==!F;8KTS+O&L63Wk7VGNG=GG=^=PU@p6W%llP!^# zx@EpLOV}YLH8@L}JW92t7^h`C&AIMc6C=!Zxf~4~pL;lt>?oHarIRTAf){g! zCR(dixhbRLve6YPnA_w*pI5&R{mQv4y)Sv6+HvLzCT<4oDnEw@lCzjcbEpzRzQBb8 mD0iX!9WH*&7IrrGXG-0f(!279?By+^XAolbG72g5)BOczL(pXa literal 548 zcmYL`ziSjh6vt<7Z+CAucV0j=g`J9W5O1-UfFMCoB%swc%iPYpyG`cDW@Z-MN-gZ| zY;7g^15(;s2?22-g@}cnkS>*PatV3Go6q+V_d8bb@#;-+MKU{DV%I9I8K zedVibtwm8AC~CB}Cqb98#_3W^S-CFpJQs~!cKunU_>0L}mlnmmaSfg*jPIvQ?Y>~5 zT!L&(-y7GijHx!#_(^qRR@c)~8^EOI)xt#H9dhQ_j9DleBf4nSb2ogs>iD@XnfO9Z zv67rU&6Zk1#w=Sh!&J7@mI!jk=v^qC&2$Z_6zuf_wm@jJ87ucd@eJS1_$<2^!nAJB z29KoXjesq4124vZCH5hve*vZRgxvl^?w*kQn13ZVKfd{~vH5*t^XSQO^7vi!bA0P_ Ra}<8vJ{~`NKQ!0z)PJB)o{Ing diff --git a/backend/app/db/base.py b/backend/app/db/base.py deleted file mode 100644 index fa2b68a5..00000000 --- a/backend/app/db/base.py +++ /dev/null @@ -1,5 +0,0 @@ -from sqlalchemy.orm import DeclarativeBase - - -class Base(DeclarativeBase): - pass diff --git a/backend/app/db/session.py b/backend/app/db/session.py index 27cdbb59..ca3dc247 100644 --- a/backend/app/db/session.py +++ b/backend/app/db/session.py @@ -1,9 +1,16 @@ from __future__ import annotations -from sqlalchemy import create_engine -from sqlalchemy.orm import sessionmaker +from sqlmodel import Session, SQLModel, create_engine from app.core.config import settings -engine = create_engine(settings.database_url, pool_pre_ping=True) -SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) +engine = create_engine(settings.database_url, echo=False) + + +def init_db() -> None: + SQLModel.metadata.create_all(engine) + + +def get_session(): + with Session(engine) as session: + yield session diff --git a/backend/app/main.py b/backend/app/main.py index a69bee4e..abae2369 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -3,10 +3,15 @@ from __future__ import annotations from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware -from app.api.tasks import router as tasks_router +from app.api.activities import router as activities_router +from app.api.hr import router as hr_router +from app.api.org import router as org_router +from app.api.projects import router as projects_router +from app.api.work import router as work_router from app.core.config import settings +from app.db.session import init_db -app = FastAPI(title="OpenClaw Agency API", version="0.1.0") +app = FastAPI(title="OpenClaw Agency API", version="0.3.0") origins = [o.strip() for o in settings.cors_origins.split(",") if o.strip()] if origins: @@ -14,11 +19,21 @@ if origins: CORSMiddleware, allow_origins=origins, allow_credentials=True, - allow_methods=["*"] , + allow_methods=["*"], allow_headers=["*"], ) -app.include_router(tasks_router) + +@app.on_event("startup") +def on_startup() -> None: + init_db() + + +app.include_router(org_router) +app.include_router(projects_router) +app.include_router(work_router) +app.include_router(hr_router) +app.include_router(activities_router) @app.get("/health") diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py index ff3fc7fb..0266e58c 100644 --- a/backend/app/models/__init__.py +++ b/backend/app/models/__init__.py @@ -1,2 +1,17 @@ -# Import models here so Alembic can discover them -from .task import Task # noqa: F401 +from app.models.org import Department, Employee +from app.models.projects import Project, ProjectMember +from app.models.work import Task, TaskComment +from app.models.hr import HeadcountRequest, EmploymentAction +from app.models.activity import Activity + +__all__ = [ + "Department", + "Employee", + "Project", + "ProjectMember", + "Task", + "TaskComment", + "HeadcountRequest", + "EmploymentAction", + "Activity", +] diff --git a/backend/app/models/__pycache__/__init__.cpython-312.pyc b/backend/app/models/__pycache__/__init__.cpython-312.pyc index f78d778feb101327edeb9d982de669c9dab3e883..50b46221c93aedcd5a53aac0fc5983fd3d524b2e 100644 GIT binary patch literal 594 zcmY*Xu};G<6tvT{NfSaVAtru+46QdNgoFTzr3fJgaUc70U1u9SzySmUkTjkl-mIL^{W3A3 zrMzRB6IpVamC8NgITID7y6&7yaZTe=1xwPSyicWe>`PLNlyC4zaC1=?+ozNyu^4CN z1-%*5qEzigGvgn|Wh$}|D%Y^vw5-CWtm%Ad8>ky-7;p@@2AT%UlBBQUENrPwlILA6 z63U9Mkb~8IwYBEti>ZrMQ`K6t?}Qw!QdF>J4dp8IUrVx_c|M79qKGgSMK55g2LrPn z8VXJkDaepWlA-e|)`TyYRv?knEN0}cLk5~us88Y%%@U-O(c(2Iiqb4Cqo|u#Gk0GX p$BZ7xZQWh}4N&Z8_^v?+Cjd>r-UM_eVE-NL{@9*xJveep`@iJhq~HJm delta 103 zcmcb_a*2`eG%qg~0}v>;)@Oz@OyrX=6aez3Go&!2Fy=7iGDa~nGE_2YGQVU53Tran uVhKqs&i2z}y2VlgWE3$2MJFF&OqAjPGC)d-L5vT~jEs!W7{rR$fE)mL&lHFN diff --git a/backend/app/models/__pycache__/activity.cpython-312.pyc b/backend/app/models/__pycache__/activity.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d412768e818e919d5dddee55f30cbf509a912c02 GIT binary patch literal 1035 zcmYjQO-LI-6rSDeCYw#7O*N%dtwD21q3)$8k%As977Z5dWho3}W}>V66K5yd4TT2U zg2&wKvDacK_S9?dy%dszE)*=KNDqaAr3X))*-gZOeSF`0-@Nzsz1=UloDTRoZ@;vD zO91>3WN?x(kS-QEI0P7A$p(&ON|L~`EjzL)M>t_89K}=|)l?-QgGT@>uK`wxij$qx zkrL-IlYv%F{}QAug&E|!9z)FXT%S`7BSx6zkg^=Aix#nQsI5I&Uhy!otMUEV_LE5$ zA2>e*1eg*Aru>vQXC`Do6fEOJM>&#XxvBCtk~lfXX+N6kXjjox6itgL*10YdA=%U- zfA-3s>-*Dqh^vML6WNHqeJieW-F9vq@llFnW^tbDZwh@Nmq$WvzRIjUiv^*MVq_7& zvd&*Rg-H!HY!+tf)N&9F;0_6zy5+Lsi(!Y91xl zrYmlQd7js!aG--04ii43VTQPj*M%&olQ7*UJgDpuy3y3RWKbttM}h4j47YvH<*OO) zcLr-w>VS$BgnE?{zA_9EE9XKDLKis%!cd3M@o>WyI1Ax!1KIsZ4nl@DY!Xwd@+>qf zQVm0R?PGwD4vA1+m=R&3ddUW>y52r5aL;>i!7aF4F}6I17|2Ikh$7cm4XPYCtJK>j zRpuLBow!vS?cYb690bM&s_qaM8>n74c$38Tjs6ybnR-AqQA`-|_|Ld`15QSZt(EVE zJMSJGulEYGt%U=t5Rq;yUN;GezdEMo{Se;O9z}6PxRcYrL)QD z)^ht^SD8K?D<0qeH2HC|H#XmT+}3)^Je?SnTjxVoqtR5#DKWrWgEc50AWaBK^!l2= zYey}|a}&R2-0!$RQL4KfU21dj^dHhGPQ_>F3;&;lBuU@E)K4&R4oZKM8EHW}c>b3C J0|G_&{{x!=8chHI literal 0 HcmV?d00001 diff --git a/backend/app/models/__pycache__/hr.cpython-312.pyc b/backend/app/models/__pycache__/hr.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bfa75cbf04f0b98abefc59db58205c7e5c894015 GIT binary patch literal 1836 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 K@oxlE$ny`pi=aaQ diff --git a/backend/app/models/__pycache__/work.cpython-312.pyc b/backend/app/models/__pycache__/work.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..532721b862a1a817b737c9587c6062dc4e78318a GIT binary patch literal 1929 zcmb_dO-vhC5Z+y{?Pb@-V2nd>LnwieMGfu&MOB0%L8L^X4N8R@MSJLW@q56+{y1;f zrAAe2DXqkz2X5uarBx9e+*5nZu~IKc_5oJv0aaDhTP4ySICWjR<7_jKrp48?td=r4Ad1CSycKmMCB)LQBvjl7wA%Co2V6J)NwsFt)eD3M9SX*J0(b zV&(!?3EBP&*hI)?+bm74zR^DTKfn8z_TcQev=kYDJ}>|0_?|F+5aTO?nhV5&;jINx zAJCgG>Q#YC42|5=0;+kXv!ONrH&yGpJq3ch}hSg${ zy6zi`7Hw`>6c5#nk4@?ND+P(8$hK=e8nsarRK@ zel~YFG*W$cYp^bk9H%esbk#DA^sVZ>&AWB+*75jTwZiky8sm=-#DVRJy7>6$-yU_c z=KnO*P@f!#gWGd;@yT&+xHj;^)$gx1ax+j!s*5v6=UfeOhN z!ay9iaLm*M8$tUT!YINRLT8TIb=>9=-bM%$$I%)_xPcIUnhCUSwgIQxTL8e}y7rpk zI_ewP(Q1C9Z@PMabD=IyAB|1ark;P;7%Loz*=?A=!l9ge_Ta)%z*6(7gwk9p`(*~w zWDzVMDoOI5TH)X~PlEpgo0A`Qc(&_PX}T%yJ8TN#@Kbq{0DnYyp8tg#`Hj2sf*XAq e>)~hl&3Cq^{^Zblc|8uebz^7rFAlBnE&L4zT;&A- literal 0 HcmV?d00001 diff --git a/backend/app/models/activity.py b/backend/app/models/activity.py new file mode 100644 index 00000000..51c3a8fc --- /dev/null +++ b/backend/app/models/activity.py @@ -0,0 +1,19 @@ +from __future__ import annotations + +from datetime import datetime + +from sqlmodel import Field, SQLModel + + +class Activity(SQLModel, table=True): + __tablename__ = "activities" + + id: int | None = Field(default=None, primary_key=True) + actor_employee_id: int | None = Field(default=None, foreign_key="employees.id") + + entity_type: str + entity_id: int | None = None + verb: str + + payload_json: str | None = None + created_at: datetime = Field(default_factory=datetime.utcnow) diff --git a/backend/app/models/hr.py b/backend/app/models/hr.py new file mode 100644 index 00000000..2b3f59a1 --- /dev/null +++ b/backend/app/models/hr.py @@ -0,0 +1,35 @@ +from __future__ import annotations + +from datetime import datetime + +from sqlmodel import Field, SQLModel + + +class HeadcountRequest(SQLModel, table=True): + __tablename__ = "headcount_requests" + + id: int | None = Field(default=None, primary_key=True) + department_id: int = Field(foreign_key="departments.id") + requested_by_manager_id: int = Field(foreign_key="employees.id") + + role_title: str + employee_type: str # human | agent + quantity: int = Field(default=1) + + justification: str | None = None + status: str = Field(default="submitted") + + created_at: datetime = Field(default_factory=datetime.utcnow) + + +class EmploymentAction(SQLModel, table=True): + __tablename__ = "employment_actions" + + id: int | None = Field(default=None, primary_key=True) + employee_id: int = Field(foreign_key="employees.id") + issued_by_employee_id: int = Field(foreign_key="employees.id") + + action_type: str # praise|warning|pip|termination + notes: str | None = None + + created_at: datetime = Field(default_factory=datetime.utcnow) diff --git a/backend/app/models/org.py b/backend/app/models/org.py new file mode 100644 index 00000000..9beb459a --- /dev/null +++ b/backend/app/models/org.py @@ -0,0 +1,27 @@ +from __future__ import annotations + +from typing import Optional + +from sqlmodel import Field, SQLModel + + +class Department(SQLModel, table=True): + __tablename__ = "departments" + + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True, unique=True) + head_employee_id: int | None = Field(default=None, foreign_key="employees.id") + + +class Employee(SQLModel, table=True): + __tablename__ = "employees" + + id: int | None = Field(default=None, primary_key=True) + name: str + employee_type: str # human | agent + + department_id: int | None = Field(default=None, foreign_key="departments.id") + manager_id: int | None = Field(default=None, foreign_key="employees.id") + + title: str | None = None + status: str = Field(default="active") diff --git a/backend/app/models/projects.py b/backend/app/models/projects.py new file mode 100644 index 00000000..acfeb15e --- /dev/null +++ b/backend/app/models/projects.py @@ -0,0 +1,20 @@ +from __future__ import annotations + +from sqlmodel import Field, SQLModel + + +class Project(SQLModel, table=True): + __tablename__ = "projects" + + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True, unique=True) + status: str = Field(default="active") + + +class ProjectMember(SQLModel, table=True): + __tablename__ = "project_members" + + id: int | None = Field(default=None, primary_key=True) + project_id: int = Field(foreign_key="projects.id") + employee_id: int = Field(foreign_key="employees.id") + role: str | None = None diff --git a/backend/app/models/task.py b/backend/app/models/task.py deleted file mode 100644 index 2f298f57..00000000 --- a/backend/app/models/task.py +++ /dev/null @@ -1,28 +0,0 @@ -from __future__ import annotations - -from datetime import datetime - -from sqlalchemy import DateTime, Integer, String, Text -from sqlalchemy.sql import func -from sqlalchemy.orm import Mapped, mapped_column - -from app.db.base import Base - - -class Task(Base): - __tablename__ = "tasks" - - id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True) - title: Mapped[str] = mapped_column(String(200), nullable=False) - description: Mapped[str | None] = mapped_column(Text, nullable=True) - - # kanban columns: todo | doing | done - status: Mapped[str] = mapped_column(String(32), nullable=False, default="todo") - - # simple attribution (no auth) - assignee: Mapped[str | None] = mapped_column(String(120), nullable=True) - - created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) - updated_at: Mapped[datetime] = mapped_column( - DateTime(timezone=True), server_default=func.now(), onupdate=func.now() - ) diff --git a/backend/app/models/work.py b/backend/app/models/work.py new file mode 100644 index 00000000..545d5657 --- /dev/null +++ b/backend/app/models/work.py @@ -0,0 +1,34 @@ +from __future__ import annotations + +from datetime import datetime + +from sqlmodel import Field, SQLModel + + +class Task(SQLModel, table=True): + __tablename__ = "tasks" + + id: int | None = Field(default=None, primary_key=True) + + project_id: int = Field(foreign_key="projects.id", index=True) + title: str + description: str | None = None + + status: str = Field(default="backlog", index=True) + + assignee_employee_id: int | None = Field(default=None, foreign_key="employees.id") + reviewer_employee_id: int | None = Field(default=None, foreign_key="employees.id") + created_by_employee_id: int | None = Field(default=None, foreign_key="employees.id") + + created_at: datetime = Field(default_factory=datetime.utcnow) + updated_at: datetime = Field(default_factory=datetime.utcnow) + + +class TaskComment(SQLModel, table=True): + __tablename__ = "task_comments" + + id: int | None = Field(default=None, primary_key=True) + task_id: int = Field(foreign_key="tasks.id", index=True) + author_employee_id: int | None = Field(default=None, foreign_key="employees.id") + body: str + created_at: datetime = Field(default_factory=datetime.utcnow) diff --git a/backend/app/schemas/__pycache__/hr.cpython-312.pyc b/backend/app/schemas/__pycache__/hr.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c8957d2952706d224562a5220fc1cb8d3a0ba1fb GIT binary patch literal 1279 zcmaJ=&1(}u6rag%Hfh@0uOzkFl%Vwxq3*pDL8My5##$+q92}-QQC7&&AQ(`g zM?JOwg7~L+jEAt`Nj&r>G+ui0&1_oIT3p!Qy!Yn4H#6`3-na4b0#e`Scdc9op`ThA zKsH_R$x9Fp5km~S$ioecm7H-ip3yKc%Am)H86OZcx$)IVV;ZKmX2EI=S+m-j18aWB zYQ-b9!m$dh;bfHheh|{I75Ea={EO#L*8|2~CrzB<;1qna3Hl-8sDT-3WVYaj++dj% zuxiQB65NYr&m~JsxrV7d@>&~dWLW{cMwJG)8B5A*oH8ef{BV=+M_h&v1*aiTjFulJ z%tRxvD1mfMn&IBPt zFgz!!7^h)&n+X5ev~O4_7?`u0ZbfZ%P2V2?YYw1YPH9hbgYf;ae1Z0}I# zHTRiK+ihDqP3}=?H^senEG(5;f%@eVz(>@ZDR<3suTt%r)!yXH`)30~vD8^RaJpux zS6%F^e_H--E{gJ5{MDVk3)>ev+knn;lchby&@rW%x?{MO{kLOVZ3Z2yiwVe-SQ0oD zNnMMYC8pHj(+Xx3+)z+dplT&b3iSPEl{9w-Dw9-pcgk%Pp^=sCB5d8L+=QOd_5vrw}R6Q*M| z1qap1!pL!1%U-UFE=d)FE|+9p*%T-bxc~qqnJRY8V(;cc*IXFL*XKK{2hi>Lw3;fQ zUdt&0??z!HfWShHa*~((E>N1g!UQ#?mae)!=VCif^D8O)?~B{utY%2s0KW~4@eg$C g7pn9v3*R}~?ju<{9 literal 0 HcmV?d00001 diff --git a/backend/app/schemas/__pycache__/org.cpython-312.pyc b/backend/app/schemas/__pycache__/org.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..40fca14ffcaa5e05441d1a8c7e99ec282204b9e8 GIT binary patch literal 1580 zcmZvc&ui2`6vt;av)MH5YOT_0f6=N~Bj}#IXz8W?K+&RzAi0EbXS7@MW1VExfZ&1( zJ+3GJ2Jzq@;zei=OTd$O@FsM<^yGV!wB2pD&~M&*Gn1M3$#>^_wQ3Xm`}D5ytwhK# zofJ~epL8dI;4=|KP@e?Ur4-|mUkVJ@prk}@6JdNHf=T16k*{%?u9l%{_EpQeT7jz7 zS2cBY1gdslwUUlHV}I~<4$Ve+7)A+C8c`TSYCU>*?|vktU(dJ8wb+~PT!8#Dk;J8f zxTVK%ZfS_pGE{}(GCZd!W0o3@nT_9&O|FtahRLduJdv3ZCn{qh59Hde-3XI~w+m}g zD6{c($%Q9_rXQtJdJW-}Gt2XEqUU9{=LL~y`52FR-pdyEi;?4=*LQr+Q;7Tv*#RZH zcWUcVAZtA4>s;}$_8_f85Ij)PhO8%XEo#cJ?(^4|`Ev-;+EZSCAwyB)&1NmGugieL z9QFK4GgT(MPYVX|1jt9ywX5&%bk8mQV5d48+iXdV?@bzx?h*_SqH_VAWWvz+s9Oen ztiVSH@jY$|zz58LU%^aSxZMZ}Htm>d6yq_JDvBOIh7pdd<0$%wConp>C%7t}0)dXE zX4*G4FKw}zBW>aI;cdAvGT7F>BhVXpa}2yuhRoEhGH4aEN|Bl9SuvtB+pTC%jbfHE zXDmB*v#>?(Ayt6^-CS1PH%yW?<(h_(RmHyPz(<)K@DRSQf>@n`m03AyB)-f{UQZgY z;2DGWTd`vu#;3`h#=XvvSoHrZV`-@f0uw#CkO=N8*{ zJN6b^?4F)&-`s@SYRQ@$Rxxu#XBvJ=9s2-x;WfLQNmqW!Dt~nN7BhOk%OTjgw@IuS1_@xgQi}V$PH2;z7--Z6B`Y?kZ zd)HddFQOH-2rDT3$Jasr8kEwXV&_edz@umP$0nWWJn0d5^d?L?-+A&6 I;(2|<|C6p)6aWAK literal 0 HcmV?d00001 diff --git a/backend/app/schemas/__pycache__/projects.cpython-312.pyc b/backend/app/schemas/__pycache__/projects.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d9fc27855c443530d415a68f970aa09aec26be7c GIT binary patch literal 780 zcmZuuOKTKC5bmCr!z5#TfMS*nK|$Dn&L0R~BD#tW5y@pPO*`GXYvz^i8D&H;phAxE zl;0r!6p!Yx40sX|Z^EvZoUEGJg#;X!ufD44uB!g(tM7XRzfbSRzrg%xojKT@rPCph zBO-{P2}x;0DRLt*QZq6sG01%)%nw9Z()?m}cOt8e+7NY4MeX|B!2OL!2DDk?S(cZ) z9Osz^*M0ir(c@gmB<@OdIIl;iH^Dz5l0;OH$k>5dq$!L|hzc{ZTF*kSwg*n*Z>#*J zjLR)0c_|xHm#VQcp327IaXEekRzv4XhXcED8N(D~jmKD;iz-3B%-CMVlX+%^u~XAB zrckxdWC}=5!eErAGT@qzxZ+u`UB^Ju`3yS93z@|Ue|?L;08$6fc)Tk!5%8i2bUczN z*Fn(@tZx^!a-e7nNc2U3k7VNc?;lJqt?u7D*gCdW)rG~G|EskD>9cA@pq8|R*(ydO zyY)K7D2T97Y^M;QSop>R$=2Vlhj}KIi@or{14k_(Uq1+HetiSrk4Y*0POkqX{h4FX-r;aYz|1^@t{o2l0$*zf_y;;9v`YX0 literal 0 HcmV?d00001 diff --git a/backend/app/schemas/__pycache__/task.cpython-312.pyc b/backend/app/schemas/__pycache__/task.cpython-312.pyc deleted file mode 100644 index ba37677e7f54a1e73996eb42aad3ac4ac76f89b2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1599 zcmZux&1>976rYhsE3Nj!-X?baQD+_3DJ;ZUN_{G!hm<;vo4SD{rHFu-?2K(C`q+$S zX*U$QB?%&$MD3nrs3)sE*)HkDDXB|4ApMLMn%+t*8y{F$Q z6&Jzy@X1fXMu6&@pI^TG@Gk{Vu7dmZo=$Googy5KF1c}v}LqjI3OHE>pjY{U;qUrt51&31~U~nG%3IGRoqagM}9&gEQ;Pi(PH&ZDxO9nFJ8^psvqGA`9h#=WR?PYHV&?dMOe605nd|#e!aAYiW#50$ zppqWJleXKd!a;e=FWffe3deV;po}fQ!)4S(i znftZX-WNNc^vUYr+*0qWr*nO>G?=aT)^^tWq&}Fh_rBb@w@>Qg+$hGW+@93)cQE~b zxn<@{a#EX6xsbtv&LD>DD$5>PHgmu#4lPH|xjMVJQOZ+ryRB++LsZmJssK=XHshPL z6G}0moRbR9C{R*sWgpg5VdU8xruePcVn!`nP@wNuSM2-}>|UB#ExG6ibWQ1O9NdUSPAsy* zx%l(-JcTOEMx(4+akC>k0$Nch(esNHWVa1}DWU2%bJ|@-V;QtWK`qcNs@2f_qRT*^ z>~wt`JD2kw|4dwhZ5W>P3cwo^WBeK|{f!o0qq%?3g*OFMo!Xna_7vJ?D^;H59{9|m22o&W#< diff --git a/backend/app/schemas/__pycache__/work.cpython-312.pyc b/backend/app/schemas/__pycache__/work.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ddbdc63ac79346bfa7d41afdf340f7f2acf84ff2 GIT binary patch literal 1409 zcmZux&uiR96rNd)R$8ssaU9om;o_YwOP(oVjrrVSx7ac0bJ5%r0`mveO z)(8SIIDybpa*F?jkUymt!{)FEdMZ8iRG4lsyd;7M8k2;El%m~;8;KQKlr+dUL|9LVVA6VRRquq%cx~`D zr@XfDI^cDuyv?HJdw=7LJ}uikO|zWmgDllR-FrXY`92de?p28^tWTlGec&&NBq0?f zY&?J)N=r0uf>&5zF=N>w!&?Z)FgLVKtX0RF!h=}5W{zQVHFFK?AS-NtzVz;My?aYZ zp3Bl2qJaUHo3MN>jYk(z@V)jYxi!bOBdfLG)gkeqG>{Vb6I*SyaaPlLNf z4~*n)Rlg3hp-g)*-`nInU=#tCDN_;f;V{s>zD&3d_A<45eORa_CP%iW3)a zIz!#1jvnC*PQK8MJxVE<1R%oEJIsN{J&S%u}hg*4u0l1+MLx>=*-S&L76#aMCJNtvS0ba%hp<7tBgY#05KtkFL6D=faByex zv46yt54|z-Ctl~tw^hBkv+FxJ7(b8lk%GZc_-9hO`e6*CB4fpXRvl`jHV4cedg|V+ zrs_-e5p1UL=m6j!i&FXrS^tZyoirVK{rSoXf#qb$rk|k=%gKU8=MUX~2`uJD{{fwi BOM3tS literal 0 HcmV?d00001 diff --git a/backend/app/schemas/hr.py b/backend/app/schemas/hr.py new file mode 100644 index 00000000..78dceb4a --- /dev/null +++ b/backend/app/schemas/hr.py @@ -0,0 +1,24 @@ +from __future__ import annotations + +from sqlmodel import SQLModel + + +class HeadcountRequestCreate(SQLModel): + department_id: int + requested_by_manager_id: int + role_title: str + employee_type: str + quantity: int = 1 + justification: str | None = None + + +class HeadcountRequestUpdate(SQLModel): + status: str | None = None + justification: str | None = None + + +class EmploymentActionCreate(SQLModel): + employee_id: int + issued_by_employee_id: int + action_type: str + notes: str | None = None diff --git a/backend/app/schemas/org.py b/backend/app/schemas/org.py new file mode 100644 index 00000000..0079b282 --- /dev/null +++ b/backend/app/schemas/org.py @@ -0,0 +1,31 @@ +from __future__ import annotations + +from sqlmodel import SQLModel + + +class DepartmentCreate(SQLModel): + name: str + head_employee_id: int | None = None + + +class DepartmentUpdate(SQLModel): + name: str | None = None + head_employee_id: int | None = None + + +class EmployeeCreate(SQLModel): + name: str + employee_type: str + department_id: int | None = None + manager_id: int | None = None + title: str | None = None + status: str = "active" + + +class EmployeeUpdate(SQLModel): + name: str | None = None + employee_type: str | None = None + department_id: int | None = None + manager_id: int | None = None + title: str | None = None + status: str | None = None diff --git a/backend/app/schemas/projects.py b/backend/app/schemas/projects.py new file mode 100644 index 00000000..98c33970 --- /dev/null +++ b/backend/app/schemas/projects.py @@ -0,0 +1,13 @@ +from __future__ import annotations + +from sqlmodel import SQLModel + + +class ProjectCreate(SQLModel): + name: str + status: str = "active" + + +class ProjectUpdate(SQLModel): + name: str | None = None + status: str | None = None diff --git a/backend/app/schemas/task.py b/backend/app/schemas/task.py deleted file mode 100644 index 4b43d013..00000000 --- a/backend/app/schemas/task.py +++ /dev/null @@ -1,35 +0,0 @@ -from __future__ import annotations - -from datetime import datetime -from typing import Literal - -from pydantic import BaseModel, Field - - -TaskStatus = Literal["todo", "doing", "done"] - - -class TaskCreate(BaseModel): - title: str = Field(min_length=1, max_length=200) - description: str | None = None - status: TaskStatus = "todo" - assignee: str | None = None - - -class TaskUpdate(BaseModel): - title: str | None = Field(default=None, min_length=1, max_length=200) - description: str | None = None - status: TaskStatus | None = None - assignee: str | None = None - - -class TaskOut(BaseModel): - id: int - title: str - description: str | None - status: TaskStatus - assignee: str | None - created_at: datetime - updated_at: datetime | None - - model_config = {"from_attributes": True} diff --git a/backend/app/schemas/work.py b/backend/app/schemas/work.py new file mode 100644 index 00000000..b483e5b6 --- /dev/null +++ b/backend/app/schemas/work.py @@ -0,0 +1,27 @@ +from __future__ import annotations + +from sqlmodel import SQLModel + + +class TaskCreate(SQLModel): + project_id: int + title: str + description: str | None = None + status: str = "backlog" + assignee_employee_id: int | None = None + reviewer_employee_id: int | None = None + created_by_employee_id: int | None = None + + +class TaskUpdate(SQLModel): + title: str | None = None + description: str | None = None + status: str | None = None + assignee_employee_id: int | None = None + reviewer_employee_id: int | None = None + + +class TaskCommentCreate(SQLModel): + task_id: int + author_employee_id: int | None = None + body: str diff --git a/backend/requirements.txt b/backend/requirements.txt index 297a4067..86c261cb 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,26 +1,7 @@ -alembic==1.18.3 -annotated-doc==0.0.4 -annotated-types==0.7.0 -anyio==4.12.1 -click==8.3.1 -fastapi==0.128.0 -greenlet==3.3.1 -h11==0.16.0 -httptools==0.7.1 -idna==3.11 -Mako==1.3.10 -MarkupSafe==3.0.3 -psycopg2-binary==2.9.11 -pydantic==2.12.5 -pydantic-settings==2.12.0 -pydantic_core==2.41.5 -python-dotenv==1.2.1 -PyYAML==6.0.3 -SQLAlchemy==2.0.46 -starlette==0.50.0 -typing-inspection==0.4.2 -typing_extensions==4.15.0 -uvicorn==0.40.0 -uvloop==0.22.1 -watchfiles==1.1.1 -websockets==16.0 +fastapi +uvicorn[standard] +sqlmodel +alembic +psycopg2-binary +python-dotenv +pydantic-settings diff --git a/frontend/src/app/_components/Shell.module.css b/frontend/src/app/_components/Shell.module.css new file mode 100644 index 00000000..4192c023 --- /dev/null +++ b/frontend/src/app/_components/Shell.module.css @@ -0,0 +1,24 @@ +.shell{min-height:100vh;display:grid;grid-template-columns:260px 1fr;background:var(--mc-bg)} +.sidebar{border-right:1px solid var(--mc-border);padding:20px 16px;position:sticky;top:0;height:100vh;display:flex;flex-direction:column;gap:16px;background:linear-gradient(180deg,var(--mc-surface) 0%, color-mix(in oklab,var(--mc-surface), var(--mc-bg) 40%) 100%)} +.brand{display:flex;flex-direction:column;gap:6px} +.brandTitle{font-family:var(--mc-font-serif);font-size:18px;letter-spacing:-0.2px} +.brandSub{font-size:12px;color:var(--mc-muted)} +.nav{display:flex;flex-direction:column;gap:6px} +.nav a{display:flex;align-items:center;gap:10px;padding:10px 12px;border-radius:12px;color:var(--mc-text);text-decoration:none;border:1px solid transparent} +.nav a:hover{background:color-mix(in oklab,var(--mc-accent), transparent 92%);border-color:color-mix(in oklab,var(--mc-accent), transparent 80%)} +.active{background:color-mix(in oklab,var(--mc-accent), transparent 88%);border-color:color-mix(in oklab,var(--mc-accent), transparent 70%)} +.main{padding:28px 28px 48px} +.topbar{display:flex;justify-content:space-between;align-items:flex-start;gap:18px;margin-bottom:18px} +.h1{font-family:var(--mc-font-serif);font-size:30px;line-height:1.1;letter-spacing:-0.6px;margin:0} +.p{margin:8px 0 0;color:var(--mc-muted);max-width:72ch} +.btn{border:1px solid var(--mc-border);background:var(--mc-surface);padding:10px 12px;border-radius:12px;cursor:pointer} +.btnPrimary{border-color:color-mix(in oklab,var(--mc-accent), black 10%);background:var(--mc-accent);color:white} +.grid2{display:grid;grid-template-columns:1.4fr 1fr;gap:16px} +.card{background:var(--mc-surface);border:1px solid var(--mc-border);border-radius:16px;padding:14px} +.cardTitle{margin:0 0 10px;font-size:13px;color:var(--mc-muted);letter-spacing:0.06em;text-transform:uppercase} +.list{display:flex;flex-direction:column;gap:10px} +.item{border:1px solid var(--mc-border);border-radius:14px;padding:12px;background:color-mix(in oklab,var(--mc-surface), white 20%)} +.mono{font-family:var(--mc-font-mono);font-size:12px;color:var(--mc-muted)} +.badge{display:inline-flex;align-items:center;padding:4px 8px;border-radius:999px;font-size:12px;border:1px solid var(--mc-border);background:color-mix(in oklab,var(--mc-bg), var(--mc-surface) 40%)} +.kbd{font-family:var(--mc-font-mono);font-size:12px;background:color-mix(in oklab,var(--mc-bg), var(--mc-surface) 40%);border:1px solid var(--mc-border);border-bottom-width:2px;padding:2px 6px;border-radius:8px} +@media (max-width: 980px){.shell{grid-template-columns:1fr}.sidebar{position:relative;height:auto}.grid2{grid-template-columns:1fr}.main{padding:18px}} diff --git a/frontend/src/app/_components/Shell.tsx b/frontend/src/app/_components/Shell.tsx new file mode 100644 index 00000000..48503dce --- /dev/null +++ b/frontend/src/app/_components/Shell.tsx @@ -0,0 +1,43 @@ +"use client"; + +import Link from "next/link"; +import { usePathname } from "next/navigation"; +import styles from "./Shell.module.css"; + +const NAV = [ + { href: "/", label: "Mission Control" }, + { href: "/projects", label: "Projects" }, + { href: "/departments", label: "Departments" }, + { href: "/people", label: "People" }, + { href: "/hr", label: "HR" }, +]; + +export function Shell({ children }: { children: React.ReactNode }) { + const path = usePathname(); + return ( +
+ +
{children}
+
+ ); +} diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index e3734be1..fffb10f8 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -1,42 +1,13 @@ -:root { - --background: #ffffff; - --foreground: #171717; -} - -@media (prefers-color-scheme: dark) { - :root { - --background: #0a0a0a; - --foreground: #ededed; - } -} - -html, -body { - max-width: 100vw; - overflow-x: hidden; -} - -body { - color: var(--foreground); - background: var(--background); - font-family: Arial, Helvetica, sans-serif; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -* { - box-sizing: border-box; - padding: 0; - margin: 0; -} - -a { - color: inherit; - text-decoration: none; -} - -@media (prefers-color-scheme: dark) { - html { - color-scheme: dark; - } +:root{ + --mc-bg:#f6f4ef; + --mc-surface:#ffffff; + --mc-border:rgba(15,23,42,0.10); + --mc-text:#0f172a; + --mc-muted:rgba(15,23,42,0.62); + --mc-accent:#2563eb; + --mc-font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif; + --mc-font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; } +html,body{height:100%} +body{margin:0;color:var(--mc-text);background:var(--mc-bg);font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji","Segoe UI Emoji";} +*{box-sizing:border-box} diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx index 42fc323e..1e83ed8b 100644 --- a/frontend/src/app/layout.tsx +++ b/frontend/src/app/layout.tsx @@ -1,20 +1,10 @@ import type { Metadata } from "next"; -import { Geist, Geist_Mono } from "next/font/google"; import "./globals.css"; - -const geistSans = Geist({ - variable: "--font-geist-sans", - subsets: ["latin"], -}); - -const geistMono = Geist_Mono({ - variable: "--font-geist-mono", - subsets: ["latin"], -}); +import { Shell } from "./_components/Shell"; export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", + title: "OpenClaw Agency โ€” Mission Control", + description: "Company OS for projects, departments, people, and HR.", }; export default function RootLayout({ @@ -24,8 +14,8 @@ export default function RootLayout({ }>) { return ( - - {children} + + {children} ); diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index ff35352c..a3dddb93 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -1,212 +1,147 @@ "use client"; -import {useEffect, useMemo, useState} from "react"; +import { useEffect, useState } from "react"; +import styles from "./_components/Shell.module.css"; +import { apiGet } from "../lib/api"; -type TaskStatus = "todo" | "doing" | "done"; +type Activity = { + id: number; + actor_employee_id: number | null; + entity_type: string; + entity_id: number | null; + verb: string; + payload: any; + created_at: string; +}; + +type Project = { id: number; name: string; status: string }; + +type Department = { id: number; name: string; head_employee_id: number | null }; + +type Employee = { + id: number; + name: string; + employee_type: string; + department_id: number | null; + manager_id: number | null; + title: string | null; + status: string; +}; type Task = { id: number; + project_id: number; title: string; - description: string | null; - status: TaskStatus; - assignee: string | null; + status: string; + assignee_employee_id: number | null; + reviewer_employee_id: number | null; created_at: string; - updated_at: string | null; + updated_at: string; }; -const STATUSES: Array<{key: TaskStatus; label: string}> = [ - {key: "todo", label: "To do"}, - {key: "doing", label: "Doing"}, - {key: "done", label: "Done"}, -]; - -function apiUrl(path: string) { - const base = process.env.NEXT_PUBLIC_API_URL; - if (!base) throw new Error("NEXT_PUBLIC_API_URL is not set"); - return `${base}${path}`; -} - -export default function Home() { +export default function MissionControlHome() { + const [activities, setActivities] = useState([]); + const [projects, setProjects] = useState([]); + const [departments, setDepartments] = useState([]); + const [employees, setEmployees] = useState([]); const [tasks, setTasks] = useState([]); - const [loading, setLoading] = useState(false); const [error, setError] = useState(null); - const [title, setTitle] = useState(""); - const [assignee, setAssignee] = useState(""); - const [description, setDescription] = useState(""); - - const byStatus = useMemo(() => { - const map: Record = {todo: [], doing: [], done: []}; - for (const t of tasks) map[t.status].push(t); - return map; - }, [tasks]); - - async function refresh() { - setLoading(true); + async function load() { setError(null); try { - const res = await fetch(apiUrl("/tasks"), {cache: "no-store"}); - if (!res.ok) throw new Error(`Failed to load tasks (${res.status})`); - setTasks(await res.json()); - } catch (e: unknown) { - const msg = e instanceof Error ? e.message : "Unknown error"; - setError(msg); - } finally { - setLoading(false); + const [a, p, d, e, t] = await Promise.all([ + apiGet("/activities?limit=20"), + apiGet("/projects"), + apiGet("/departments"), + apiGet("/employees"), + apiGet("/tasks"), + ]); + setActivities(a); + setProjects(p); + setDepartments(d); + setEmployees(e); + setTasks(t); + } catch (err: unknown) { + setError(err instanceof Error ? err.message : "Unknown error"); } } useEffect(() => { - refresh(); + load(); }, []); - async function createTask() { - if (!title.trim()) return; - setError(null); - const payload = { - title, - description: description.trim() ? description : null, - assignee: assignee.trim() ? assignee : null, - status: "todo" as const, - }; - - const res = await fetch(apiUrl("/tasks"), { - method: "POST", - headers: {"Content-Type": "application/json"}, - body: JSON.stringify(payload), - }); - - if (!res.ok) { - setError(`Failed to create task (${res.status})`); - return; - } - - setTitle(""); - setAssignee(""); - setDescription(""); - await refresh(); - } - - async function move(task: Task, status: TaskStatus) { - const res = await fetch(apiUrl(`/tasks/${task.id}`), { - method: "PATCH", - headers: {"Content-Type": "application/json"}, - body: JSON.stringify({status}), - }); - if (!res.ok) { - setError(`Failed to update task (${res.status})`); - return; - } - await refresh(); - } - - async function remove(task: Task) { - const res = await fetch(apiUrl(`/tasks/${task.id}`), {method: "DELETE"}); - if (!res.ok) { - setError(`Failed to delete task (${res.status})`); - return; - } - await refresh(); - } + const activeProjects = projects.filter((x) => x.status === "active").length; + const activeEmployees = employees.filter((x) => x.status === "active").length; + const blockedTasks = tasks.filter((t) => t.status === "blocked").length; + const reviewQueue = tasks.filter((t) => t.status === "review").length; return ( -
-
+
+
-

OpenClaw Agency Board

-

- Simple Kanban (no auth). Everyone can see who owns what. +

Mission Control

+

+ Company dashboard: departments, employees/agents, projects, and work โ€” designed to run like a real org.

- -
+ + -
-

Create task

-
- setTitle(e.target.value)} - placeholder="Task title" - style={input()} - /> - setAssignee(e.target.value)} - placeholder="Assignee (e.g. Head: Design)" - style={input()} - /> + {error ? ( +
+
Error
+
{error}
-