Phase 3.1: Enhanced Chore Logging and Reporting System
This commit is contained in:
316
IMPLEMENTATION_GUIDE_PART1.txt
Normal file
316
IMPLEMENTATION_GUIDE_PART1.txt
Normal file
@@ -0,0 +1,316 @@
|
||||
========================================
|
||||
🚀 COMPREHENSIVE IMPLEMENTATION GUIDE
|
||||
========================================
|
||||
Major Features Update - All Code Changes
|
||||
========================================
|
||||
|
||||
This document contains ALL code changes needed for:
|
||||
1. Admin avatar upload for other users
|
||||
2. Chore assignment types (any_one vs all_assigned)
|
||||
3. Kiosk available chores section
|
||||
4. Kiosk completion confirmation modal
|
||||
|
||||
========================================
|
||||
STEP 1: RUN MIGRATION
|
||||
========================================
|
||||
|
||||
Already created: backend/migrations/004_add_assignment_type.py
|
||||
|
||||
Run it:
|
||||
```
|
||||
python backend/migrations/004_add_assignment_type.py
|
||||
```
|
||||
|
||||
========================================
|
||||
STEP 2: BACKEND - Admin Avatar Upload
|
||||
========================================
|
||||
|
||||
FILE: backend/app/api/v1/uploads.py
|
||||
ADD these endpoints at the end of the file:
|
||||
|
||||
```python
|
||||
@router.post("/admin/users/{user_id}/avatar", status_code=status.HTTP_200_OK)
|
||||
async def admin_upload_user_avatar(
|
||||
user_id: int,
|
||||
file: UploadFile = File(...),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Upload avatar for any user (admin only).
|
||||
"""
|
||||
# Check if current user is admin
|
||||
if not current_user.is_admin:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Only administrators can upload avatars for other users"
|
||||
)
|
||||
|
||||
# Get target user
|
||||
target_user = db.query(User).filter(User.id == user_id).first()
|
||||
if not target_user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="User not found"
|
||||
)
|
||||
|
||||
if not validate_image(file.filename):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Invalid file type. Allowed: {', '.join(ALLOWED_EXTENSIONS)}"
|
||||
)
|
||||
|
||||
# Generate unique filename
|
||||
file_ext = Path(file.filename).suffix.lower()
|
||||
filename = f"user_{user_id}_{uuid.uuid4().hex[:8]}{file_ext}"
|
||||
file_path = USER_UPLOAD_DIR / filename
|
||||
|
||||
# Delete old avatar if exists
|
||||
if target_user.avatar_url:
|
||||
old_file = USER_UPLOAD_DIR / Path(target_user.avatar_url).name
|
||||
if old_file.exists():
|
||||
old_file.unlink()
|
||||
|
||||
# Save new avatar
|
||||
save_upload_file(file, file_path)
|
||||
|
||||
# Update user record
|
||||
avatar_url = f"/static/uploads/users/{filename}"
|
||||
target_user.avatar_url = avatar_url
|
||||
db.commit()
|
||||
db.refresh(target_user)
|
||||
|
||||
return {
|
||||
"message": "Avatar uploaded successfully",
|
||||
"avatar_url": avatar_url
|
||||
}
|
||||
|
||||
|
||||
@router.delete("/admin/users/{user_id}/avatar", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def admin_delete_user_avatar(
|
||||
user_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""Delete avatar for any user (admin only)"""
|
||||
# Check if current user is admin
|
||||
if not current_user.is_admin:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Only administrators can delete avatars for other users"
|
||||
)
|
||||
|
||||
# Get target user
|
||||
target_user = db.query(User).filter(User.id == user_id).first()
|
||||
if not target_user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="User not found"
|
||||
)
|
||||
|
||||
if not target_user.avatar_url:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="No avatar to delete"
|
||||
)
|
||||
|
||||
# Delete file
|
||||
old_file = USER_UPLOAD_DIR / Path(target_user.avatar_url).name
|
||||
if old_file.exists():
|
||||
old_file.unlink()
|
||||
|
||||
# Update database
|
||||
target_user.avatar_url = None
|
||||
db.commit()
|
||||
|
||||
return None
|
||||
```
|
||||
|
||||
========================================
|
||||
STEP 3: BACKEND - Update Public API
|
||||
========================================
|
||||
|
||||
FILE: backend/app/api/v1/public.py
|
||||
UPDATE the get_public_chores endpoint to include assignment_type:
|
||||
|
||||
In the chore_dict, add:
|
||||
```python
|
||||
"assignment_type": chore.assignment_type,
|
||||
```
|
||||
|
||||
ADD new endpoint for claiming chores:
|
||||
```python
|
||||
@router.post("/chores/{chore_id}/claim")
|
||||
async def claim_public_chore(
|
||||
chore_id: int,
|
||||
user_id: int,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Claim an unassigned chore (add assignment).
|
||||
"""
|
||||
# Get chore
|
||||
chore = db.query(Chore).filter(Chore.id == chore_id).first()
|
||||
if not chore:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Chore not found"
|
||||
)
|
||||
|
||||
# Get user
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="User not found"
|
||||
)
|
||||
|
||||
# Check if already assigned
|
||||
existing = db.query(ChoreAssignment).filter(
|
||||
ChoreAssignment.chore_id == chore_id,
|
||||
ChoreAssignment.user_id == user_id
|
||||
).first()
|
||||
|
||||
if existing:
|
||||
return {"message": "Already assigned to this chore"}
|
||||
|
||||
# Create assignment
|
||||
assignment = ChoreAssignment(
|
||||
chore_id=chore_id,
|
||||
user_id=user_id
|
||||
)
|
||||
db.add(assignment)
|
||||
db.commit()
|
||||
|
||||
return {
|
||||
"message": "Chore claimed successfully",
|
||||
"chore_id": chore_id,
|
||||
"user_id": user_id
|
||||
}
|
||||
```
|
||||
|
||||
========================================
|
||||
STEP 4: FRONTEND - Update Upload Service
|
||||
========================================
|
||||
|
||||
FILE: frontend/src/api/uploads.ts
|
||||
ADD these methods to uploadService:
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* Upload avatar for another user (admin only)
|
||||
*/
|
||||
async uploadAvatarForUser(userId: number, file: File): Promise<{ message: string; avatar_url: string }> {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
const response = await api.post(`/api/v1/uploads/admin/users/${userId}/avatar`, formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
});
|
||||
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete avatar for another user (admin only)
|
||||
*/
|
||||
async deleteAvatarForUser(userId: number): Promise<void> {
|
||||
await api.delete(`/api/v1/uploads/admin/users/${userId}/avatar`);
|
||||
},
|
||||
```
|
||||
|
||||
========================================
|
||||
STEP 5: FRONTEND - Update Avatar Component
|
||||
========================================
|
||||
|
||||
FILE: frontend/src/components/AvatarUpload.tsx
|
||||
UPDATE the interface and component:
|
||||
|
||||
```typescript
|
||||
interface AvatarUploadProps {
|
||||
currentAvatarUrl?: string;
|
||||
onUploadSuccess: (avatarUrl: string) => void;
|
||||
onDeleteSuccess?: () => void;
|
||||
userId?: number; // NEW: For admin uploading for other users
|
||||
}
|
||||
```
|
||||
|
||||
In handleFileSelect, UPDATE the upload call:
|
||||
```typescript
|
||||
try {
|
||||
const result = userId
|
||||
? await uploadService.uploadAvatarForUser(userId, file)
|
||||
: await uploadService.uploadAvatar(file);
|
||||
onUploadSuccess(result.avatar_url);
|
||||
```
|
||||
|
||||
In handleDelete, UPDATE the delete call:
|
||||
```typescript
|
||||
try {
|
||||
if (userId) {
|
||||
await uploadService.deleteAvatarForUser(userId);
|
||||
} else {
|
||||
await uploadService.deleteAvatar();
|
||||
}
|
||||
setPreviewUrl(null);
|
||||
```
|
||||
|
||||
========================================
|
||||
STEP 6: FRONTEND - Update Settings Page
|
||||
========================================
|
||||
|
||||
FILE: frontend/src/pages/Settings.tsx
|
||||
In the admin edit modal, ADD userId prop:
|
||||
|
||||
Find the AvatarUpload component in the admin modal and update:
|
||||
```typescript
|
||||
<AvatarUpload
|
||||
userId={editingUser.id} // ADD THIS
|
||||
currentAvatarUrl={editingUser.avatar_url}
|
||||
onUploadSuccess={(url) => {
|
||||
setEditingUser({ ...editingUser, avatar_url: url });
|
||||
}}
|
||||
onDeleteSuccess={() => {
|
||||
setEditingUser({ ...editingUser, avatar_url: undefined });
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
========================================
|
||||
STEP 7: FRONTEND - Update Chore Interface
|
||||
========================================
|
||||
|
||||
FILE: frontend/src/api/chores.ts
|
||||
ADD to Chore interface:
|
||||
|
||||
```typescript
|
||||
export interface Chore {
|
||||
id: number;
|
||||
title: string;
|
||||
description?: string;
|
||||
room: string;
|
||||
frequency: 'daily' | 'weekly' | 'fortnightly' | 'monthly' | 'on_trigger';
|
||||
points: number;
|
||||
image_url?: string;
|
||||
assignment_type: 'any_one' | 'all_assigned'; // ADD THIS
|
||||
status: 'pending' | 'in_progress' | 'completed' | 'skipped';
|
||||
// ... rest of fields
|
||||
}
|
||||
```
|
||||
|
||||
========================================
|
||||
|
||||
Due to size, I'll create separate implementation files for:
|
||||
- Kiosk View updates (available chores + completion modal)
|
||||
- Chore form updates (assignment type selector)
|
||||
|
||||
These are complex and need their own dedicated implementation files.
|
||||
|
||||
Would you like me to:
|
||||
1. Create the kiosk updates file next?
|
||||
2. Create the chore forms update file?
|
||||
3. Apply what we have so far and test?
|
||||
|
||||
========================================
|
||||
Reference in New Issue
Block a user