diff --git a/PROGRESS.md b/PROGRESS.md index ccfe9f2..bfc4338 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -41,13 +41,18 @@ ## ⏭️ Upcoming Steps -### Phase 2: Frontend Refactor (✅ Completed) -1. **API Service:** Created `api.js` to handle all network requests to the FastAPI backend. +### Phase 2: Frontend Refactor & Workflow Polish (✅ Completed) +1. **API Service:** Created `api.js` to dynamically connect to the backend (resolved connection refused issues by using dynamic hostnames) and handle all network requests. 2. **Authentication Hook:** Updated the Login screen to use real JWT tokens. 3. **Component Updates:** - Swapped `DashyDB` calls for `async` API calls in `app.jsx`. - - Implemented "Loading" states for UI responsiveness during network calls. -4. **Cleanup:** Archived `db.js`, `data.jsx`, and removed `sql.js` WASM dependency from `Dashy.html`. + - Fixed UI state refresh issues by silently loading subsequent data updates. +4. **Task Workflows:** + - **Completion:** Added a "Mark as completed" button in `TaskDetail` that changes task status to `closed`, records it in the Audit Log, and removes the task from the main Overview board. + - **Reopening:** Added a "Reopen task" button to restore accidentally closed tasks back to the queue. + - **User Views:** Updated `UserScreen` to accurately display open task counts and render a dedicated, faded "Completed" section at the bottom for closed tasks. + - **Audit Rendering:** Fixed crash in `TaskDetail` by passing global API audit logs and filtering them locally for individual tasks. +5. **Cleanup:** Archived `db.js`, `data.jsx` to `Dashy-v1/scraps/` and removed `sql.js` WASM dependency from `Dashy.html`. ### Phase 3: Advanced Features - **Real-time Notifications:** Explore WebSockets for task assignments. diff --git a/api.js b/api.js index 621f33f..b3a9744 100644 --- a/api.js +++ b/api.js @@ -66,6 +66,32 @@ class ApiService { return this.request('/users'); } + async createUser(userData) { + const data = await this.request('/users', { + method: 'POST', + body: JSON.stringify(userData), + }); + this.notify(); + return data; + } + + async updateUser(id, updates) { + const data = await this.request(`/users/${id}`, { + method: 'PATCH', + body: JSON.stringify(updates), + }); + this.notify(); + return data; + } + + async deleteUser(id) { + const data = await this.request(`/users/${id}`, { + method: 'DELETE', + }); + this.notify(); + return data; + } + async getTasks() { return this.request('/tasks'); } diff --git a/app.jsx b/app.jsx index 87c5f70..dd05473 100644 --- a/app.jsx +++ b/app.jsx @@ -235,7 +235,7 @@ function App() { setAuthed(false); setShowSettings(false); }} - onSwitchUser={async (id) => { + onSwitchUser={async (id) => { try { await api.login(id, "password123"); setMeId(id); @@ -244,6 +244,43 @@ function App() { alert("Failed to switch user"); } }} + onCreateUser={async (u) => { + try { + const id = u.name.split(' ')[0].toLowerCase() + Math.floor(Math.random()*100); + await api.createUser({ + id, + name: u.name, + role: u.role, + hue: Math.floor(Math.random() * 360), + initials: u.name.split(' ').map(s=>s[0]).join('').slice(0,2).toUpperCase(), + account_type: u.account_type, + password: "password123" + }); + await api.addAudit({ actor: meId, action: 'user_created', summary: 'Added ' + u.name + ' (' + (u.account_type||'standard') + ')', target: id }); + } catch(e) { + console.error(e); + alert("Failed to create user: " + e.message); + } + }} + onDeleteUser={async (id) => { + try { + const u = userMap[id]; + await api.deleteUser(id); + 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"); + } + }} + onUpdateUserRole={async (id, edits) => { + try { + await api.updateUser(id, edits); + 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"); + } + }} /> )} diff --git a/backend/main.py b/backend/main.py index 57cd76c..c4bd706 100644 --- a/backend/main.py +++ b/backend/main.py @@ -35,6 +35,52 @@ async def login_for_access_token(form_data: schemas.UserLogin, db: Session = Dep def read_users(db: Session = Depends(get_db)): return db.query(models.User).all() +@app.post("/users", response_model=schemas.User) +def create_user(user: schemas.UserCreate, db: Session = Depends(get_db)): + db_user = models.User( + id=user.id, + name=user.name, + role=user.role, + hue=user.hue, + initials=user.initials, + email=user.email, + phone=user.phone, + photo=user.photo, + account_type=user.account_type, + password_hash=auth.get_password_hash(user.password) + ) + db.add(db_user) + db.commit() + db.refresh(db_user) + return db_user + +@app.patch("/users/{user_id}", response_model=schemas.User) +def update_user(user_id: str, user_update: schemas.UserUpdate, db: Session = Depends(get_db)): + 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") + + update_data = user_update.dict(exclude_unset=True) + for key, value in update_data.items(): + setattr(db_user, key, value) + + db.commit() + db.refresh(db_user) + return db_user + +@app.delete("/users/{user_id}") +def delete_user(user_id: str, db: Session = Depends(get_db)): + 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") + + # Reassign tasks to rod + db.query(models.Task).filter(models.Task.assignee_id == user_id).update({"assignee_id": "rod"}) + + db.delete(db_user) + db.commit() + return {"message": "User deleted"} + @app.get("/tasks", response_model=List[schemas.Task]) def read_tasks(db: Session = Depends(get_db)): return db.query(models.Task).all() diff --git a/backend/schemas.py b/backend/schemas.py index 6829d90..f476bd5 100644 --- a/backend/schemas.py +++ b/backend/schemas.py @@ -20,6 +20,12 @@ class UserLogin(BaseModel): id: str password: str +class UserUpdate(BaseModel): + name: Optional[str] = None + role: Optional[str] = None + account_type: Optional[str] = None + photo: Optional[str] = None + class User(UserBase): created_at: datetime class Config: diff --git a/dashy.db b/dashy.db index 7bdfeef..0ee0f94 100644 Binary files a/dashy.db and b/dashy.db differ diff --git a/screens.jsx b/screens.jsx index b183688..dec899c 100644 --- a/screens.jsx +++ b/screens.jsx @@ -626,7 +626,7 @@ function FilterChip({ on, onClick, children }) { ); } -function SettingsScreen({ user, isAdmin, onClose, onSave, onLogout, onSwitchUser, onCreateUser, onDeleteUser, onUpdateUserRole }) { +function SettingsScreen({ user, dbUsers, isAdmin, onClose, onSave, onLogout, onSwitchUser, onCreateUser, onDeleteUser, onUpdateUserRole }) { const [name, setName] = React.useState(user.name); const [role, setRole] = React.useState(user.role); const [photo, setPhoto] = React.useState(user.photo || null); @@ -804,7 +804,7 @@ function SettingsScreen({ user, isAdmin, onClose, onSave, onLogout, onSwitchUser {tab === 'workspace' && (