317 lines
8.6 KiB
Plaintext
317 lines
8.6 KiB
Plaintext
========================================
|
|
🚀 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?
|
|
|
|
========================================
|