From fd84caef63cc1bc0eb4ddb0e5319318a86b469f5 Mon Sep 17 00:00:00 2001 From: NPS Agent Date: Wed, 13 May 2026 10:40:57 +0930 Subject: [PATCH] Deleting user now works, and forces you to allocate tasks from each user to another user before deleting them --- PROGRESS.md | 1 + api.js | 7 ++++++- app.jsx | 4 ++-- backend/main.py | 13 +++++++++++++ backend/models.py | 2 +- backend/schemas.py | 2 +- dashy.db | Bin 98304 -> 110592 bytes 7 files changed, 24 insertions(+), 5 deletions(-) diff --git a/PROGRESS.md b/PROGRESS.md index f856b48..72ac627 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -70,6 +70,7 @@ - **`Escape`**: Instantly close any open modal (Task Detail, Add Task, Settings, or Audit Logs). - **`n`**: Open the "Add Task" modal from the main dashboard (disabled while typing in inputs). 24. **Drag-and-Drop Stability:** Fixed a bug where tasks would "disappear" if dropped in an invalid area. Tasks now remain visible at their original position if a drop is cancelled. +25. **User Deletion Safety:** Implemented a backend check to prevent deleting users who have assigned tasks or notes. Upgraded the frontend `ApiService` to correctly parse and display these descriptive error messages from the backend. ### Phase 3: Advanced Features - **Real-time Notifications:** Explore WebSockets for task assignments. diff --git a/api.js b/api.js index 1f2a97b..5883705 100644 --- a/api.js +++ b/api.js @@ -32,7 +32,12 @@ class ApiService { if (response.status === 401) { this.logout(); } - throw new Error(`API Error: ${response.statusText}`); + let errorMsg = response.statusText; + try { + const data = await response.json(); + if (data && data.detail) errorMsg = data.detail; + } catch (e) {} + throw new Error(`API Error: ${errorMsg}`); } return response.json(); diff --git a/app.jsx b/app.jsx index 8d9a169..0784ad0 100644 --- a/app.jsx +++ b/app.jsx @@ -392,7 +392,7 @@ function App() { await api.addAudit({ actor: meId, action: 'user_deleted', summary: 'Removed ' + (u?u.name:id), target: null }); } catch(e) { console.error(e); - alert("Failed to delete user"); + alert("Failed to delete user: " + e.message); } }} onUpdateUserRole={async (id, edits) => { @@ -401,7 +401,7 @@ function App() { await api.addAudit({ actor: meId, action: 'user_updated', summary: 'Updated ' + (userMap[id]?userMap[id].name:id) + ' permissions', target: null }); } catch(e) { console.error(e); - alert("Failed to update user"); + alert("Failed to update user: " + e.message); } }} onChangePassword={async (oldPwd, newPwd) => { diff --git a/backend/main.py b/backend/main.py index fac61fd..3d8e754 100644 --- a/backend/main.py +++ b/backend/main.py @@ -96,6 +96,19 @@ def delete_user(user_id: str, db: Session = Depends(get_db), current_user: model db_user = db.query(models.User).filter(models.User.id == user_id).first() if not db_user: raise HTTPException(status_code=404, detail="User not found") + + # Check for OPEN assigned tasks (that are not in the trash) + open_tasks = db.query(models.Task).filter( + models.Task.assignee_id == user_id, + models.Task.status == "open", + models.Task.deleted_at == None + ).first() + if open_tasks: + raise HTTPException(status_code=400, detail="Cannot delete user: They still have OPEN tasks assigned to them. Reassign them first.") + + # Nullify references in closed tasks and notes so we don't lose history + db.query(models.Task).filter(models.Task.assignee_id == user_id).update({"assignee_id": None}) + db.query(models.TaskNote).filter(models.TaskNote.author_id == user_id).update({"author_id": None}) db.delete(db_user) db.commit() diff --git a/backend/models.py b/backend/models.py index a6e1854..99bf66a 100644 --- a/backend/models.py +++ b/backend/models.py @@ -59,7 +59,7 @@ class TaskNote(Base): id = Column(Integer, primary_key=True, index=True) task_id = Column(String, ForeignKey("tasks.id", ondelete="CASCADE"), nullable=False) - author_id = Column(String, ForeignKey("users.id"), nullable=False) + author_id = Column(String, ForeignKey("users.id"), nullable=True) body = Column(String, nullable=False) created_at = Column(DateTime(timezone=True), server_default=func.now()) diff --git a/backend/schemas.py b/backend/schemas.py index c28f98a..0ccd8c6 100644 --- a/backend/schemas.py +++ b/backend/schemas.py @@ -62,7 +62,7 @@ class Tag(TagBase): class TaskBase(BaseModel): title: str description: Optional[str] = None - assignee_id: str + assignee_id: Optional[str] = None added_by: str priority: str source: str diff --git a/dashy.db b/dashy.db index f7842e7d93f712288d441cd60c5565476c46c51d..e2a6cabbc9a249c634ff197980d29faa3f94dfcb 100644 GIT binary patch delta 5196 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 DNrNeO delta 2648 zcmaJ?d2kcg8GmmN?V;U$GC~LwV9!xw595oU>ZB;M*$j8#nP8}2&IHq2(j`KBJAJKP zK1)~Y(p5f-#wzR=s_;xKQ@AXc&61oB55!WLSTbQ*-`eS2(b`#OVVS}W!dO_v@^ny= zWT`haRCqx!6)MPwFjZDZR?;k^y3b_OCH^L|-o!FeR_+aEENyDEu9d5OB{$=wx9}>v zfDWe9vA%>X7p}8ag(>pF@OnXKuD8N?EFsB*vB6#qSZ^Ykk<+~~>D~d|LBDcMo3GpQ zO+8D;no>q(L(9ULRJ*f>r_5F>{Nawfbtp^k`4m*2#`{ZBQYlzfY}3>nie-Y?Of*^W zu$4q8DV1}72DKK>vWCK!w1+HsXWM@P@t7p6<9U z)U!d#Hf~OLBpmD7p4gHaaz$6TRwrXC`W&6h8oT1nnS}lS!UH7836M7$RfJ8}!nS!W z-_0~RmxW!)4feIs!A3dT9$nd)jW#$VsjZ2neOsb~u~1Jco9d2-mwOw#{j0maAM6Rn zf|7c@9#8;Q9X0}81Lzw12u)84mNO{t=jJlF83u<2k_ke4E7c#TaTC<4gH3+ksJ9ph zbW-Ia2Hjt%ecV;%5&gURt?c`33x9zd z7T(vznQQcMav5=I(rUm6I>+Neh4*_V#WN1q5LyuBxpC;GNTXnNEH^9E81#}Q6w3KA zI8TXLb)#MLXdJ=DF*g&ot6Dr!bn<|ZFQ)@{leHeC^ z1H640)^VQ9&XOYS1M~CwYC{?c)>@y*eAnk?1v(iGP^lWq#W6UmM!i0+CF;*Edb-Ny zIVT}h3}w|g)`zq>SjMRc6m%)mhoG}sGfxktGjcpGOX|EN<3a5|CKT)Mk0tsP&tW)+ zZJ5f{l0ro?YgW<+Vd_q@A0C8_1bqZ-$me@zas2?5z?Mn6La@%OH7U2o;G%CJRKBgi z8b0TsP(?=$nJP`z8fD}2Fzzj*h{pP&_h4ib>1;TxUh7D<|EmikHkVQqmxyA6SX%Z! zbQ=V#sot!_6j(>zs|Hu9hD}pNMdh7+@TYWcCMy`*lV;`V{cz)*OjhoP)dqFqzUET@ znqoWvzpUI2j9*Jjd+p}cP_<^|y}hvJj{O&V)!x224qwDaz`A_CgUK~=$k?9Kn-tqV zxVhZY1N-1(1j>RZ^7-{j<6d}xP@O%K&+n>Z(QPmTwE}b!y@HOSPtnKdZ|FL@f-a#y zqBE!$`B5uMpdIJ{+C6Etn2?g{hlH~07_6-pa4XF82E*Zy(|FV2)0dwjt4th&%{mo@x{r){j-(7TwTaWA>i~UOo*VR6bX}dDvuHkv zL#Weflq5Au5E{jCjba#%Gt4W~L;$ack1LcPZp{DW?X;kvT&`1omnV6 ztUrg1ViUL@KaEf5Ms-Cti~;m}y2z!NBKtaj5=+seY!{O!0Y1f4(tr}N)8q&?!apk< zAXD5hvlDsv1*CE@08PV~h*@yJ{e$%p08S&Du$!L14D4*QNq33YVbjbp%p>d&e#I}v z0{V6AO)|i|gR@Ejy?53uB0tCV6Nr0PQvx9Dzhf-86c7}Q?e zT?n^g!Cps`XtS%W2JhlUAm;ORiXj4ZbKH_EVzbF^a2daJznL--hL$#b`hITNnX{!i z+-!G6zIampz?{x|=_J;!iTH zW9jYm(*XS&-B1iEIEz4!qS=a=f>l+5U6?Nz1)Tqs|B!!&e}n%8|1jRxg`A(7vTJyM=7SD*v9@-{WhFsf6e}!9cQ0qcd&8#Rdy4* zhW!rPz*e(}`7blg++f~j&M=eAi%fxeib*qJri<}0O-v0li_y`asb7*GQ>A8|*gXg{ G)c*i+gUKoY