Phase 3.1: Add remaining local files
This commit is contained in:
75
.gitignore
vendored
75
.gitignore
vendored
@@ -4,6 +4,7 @@ __pycache__/
|
|||||||
*$py.class
|
*$py.class
|
||||||
*.so
|
*.so
|
||||||
.Python
|
.Python
|
||||||
|
<<<<<<< HEAD
|
||||||
venv/
|
venv/
|
||||||
env/
|
env/
|
||||||
ENV/
|
ENV/
|
||||||
@@ -17,12 +18,52 @@ build/
|
|||||||
*.sqlite
|
*.sqlite
|
||||||
*.sqlite3
|
*.sqlite3
|
||||||
data/
|
data/
|
||||||
|
=======
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# FastAPI / Uvicorn
|
||||||
|
*.db
|
||||||
|
*.sqlite
|
||||||
|
*.sqlite3
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# IDEs
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
.DS_Store
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
|
|
||||||
# Node
|
# Node
|
||||||
node_modules/
|
node_modules/
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
|
<<<<<<< HEAD
|
||||||
.pnpm-debug.log*
|
.pnpm-debug.log*
|
||||||
|
|
||||||
# Frontend Build
|
# Frontend Build
|
||||||
@@ -46,11 +87,29 @@ logs/
|
|||||||
.env
|
.env
|
||||||
.env.local
|
.env.local
|
||||||
.env.*.local
|
.env.*.local
|
||||||
|
=======
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
dist/
|
||||||
|
dist-ssr/
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
.coverage
|
||||||
|
htmlcov/
|
||||||
|
.pytest_cache/
|
||||||
|
coverage/
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs/
|
||||||
|
*.log
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
|
|
||||||
# OS
|
# OS
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
# Uploads
|
# Uploads
|
||||||
uploads/
|
uploads/
|
||||||
avatars/
|
avatars/
|
||||||
@@ -65,3 +124,19 @@ htmlcov/
|
|||||||
*.tmp
|
*.tmp
|
||||||
*.temp
|
*.temp
|
||||||
~*
|
~*
|
||||||
|
=======
|
||||||
|
# Database
|
||||||
|
*.db
|
||||||
|
*.sqlite
|
||||||
|
family_hub.db
|
||||||
|
|
||||||
|
# Docker
|
||||||
|
.dockerignore
|
||||||
|
|
||||||
|
# Temporary files
|
||||||
|
*.tmp
|
||||||
|
*.temp
|
||||||
|
*.bak
|
||||||
|
*.swp
|
||||||
|
*~
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
|
|||||||
250
MAJOR_UPDATE_SUMMARY.md
Normal file
250
MAJOR_UPDATE_SUMMARY.md
Normal file
@@ -0,0 +1,250 @@
|
|||||||
|
========================================
|
||||||
|
✅ MAJOR UPDATE - 4 NEW FEATURES
|
||||||
|
========================================
|
||||||
|
Date: 2026-02-02
|
||||||
|
Commit Series: ALL-IN UPDATE
|
||||||
|
|
||||||
|
## 🎉 FEATURES IMPLEMENTED
|
||||||
|
|
||||||
|
This commit series adds 4 major features to Family Hub:
|
||||||
|
|
||||||
|
1. ✅ **Admin Avatar Upload** - Admins can upload/delete avatars for any user
|
||||||
|
2. ✅ **Assignment Types** - Chores support "any one" or "all assigned" completion
|
||||||
|
3. ✅ **Available Chores (Kiosk)** - Users can claim and complete unassigned chores
|
||||||
|
4. ✅ **Completion Modal (Kiosk)** - Confirm with optional helper selection
|
||||||
|
|
||||||
|
========================================
|
||||||
|
## FILES MODIFIED/CREATED
|
||||||
|
========================================
|
||||||
|
|
||||||
|
### Backend (6 files)
|
||||||
|
✅ migrations/004_add_assignment_type.py (NEW)
|
||||||
|
✅ app/models/chore.py (MODIFIED)
|
||||||
|
⏳ app/schemas/chore.py (MODIFIED - see local)
|
||||||
|
⏳ app/api/v1/uploads.py (MODIFIED - see local)
|
||||||
|
⏳ app/api/v1/public.py (MODIFIED - see local)
|
||||||
|
|
||||||
|
### Frontend (8 files)
|
||||||
|
⏳ src/api/uploads.ts (MODIFIED - see local)
|
||||||
|
⏳ src/api/chores.ts (MODIFIED - see local)
|
||||||
|
⏳ src/components/AvatarUpload.tsx (MODIFIED - see local)
|
||||||
|
⏳ src/components/CreateChoreModal.tsx (MODIFIED - see local)
|
||||||
|
⏳ src/components/EditChoreModal.tsx (MODIFIED - see local)
|
||||||
|
⏳ src/pages/Settings.tsx (MODIFIED - see local)
|
||||||
|
⏳ src/pages/KioskView.tsx (COMPLETE REWRITE - see local)
|
||||||
|
|
||||||
|
========================================
|
||||||
|
## TO DEPLOY
|
||||||
|
========================================
|
||||||
|
|
||||||
|
1. **Run Migration:**
|
||||||
|
```bash
|
||||||
|
python backend/migrations/004_add_assignment_type.py
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Restart Backend:**
|
||||||
|
```bash
|
||||||
|
restart_backend.bat
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Frontend:** Auto-reloads with Vite
|
||||||
|
|
||||||
|
========================================
|
||||||
|
## DETAILED CHANGES
|
||||||
|
========================================
|
||||||
|
|
||||||
|
### 1. Admin Avatar Upload
|
||||||
|
|
||||||
|
**Backend:**
|
||||||
|
- Two new endpoints in `app/api/v1/uploads.py`:
|
||||||
|
- POST `/api/v1/uploads/admin/users/{user_id}/avatar`
|
||||||
|
- DELETE `/api/v1/uploads/admin/users/{user_id}/avatar`
|
||||||
|
- Admin-only permission checks
|
||||||
|
- Automatic old file cleanup
|
||||||
|
- Same validation as user uploads
|
||||||
|
|
||||||
|
**Frontend:**
|
||||||
|
- `AvatarUpload` component: Added `userId` prop
|
||||||
|
- `Settings` page: Passes `userId` in admin edit modal
|
||||||
|
- New API methods: `uploadAvatarForUser()`, `deleteAvatarForUser()`
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
Admin → Settings → User Management → Edit → Upload Avatar
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Assignment Types
|
||||||
|
|
||||||
|
**Database:**
|
||||||
|
- New column: `assignment_type` VARCHAR(20) DEFAULT 'any_one'
|
||||||
|
- Values: 'any_one' | 'all_assigned'
|
||||||
|
|
||||||
|
**Backend:**
|
||||||
|
- New enum: `ChoreAssignmentType` in models
|
||||||
|
- Completion logic respects type:
|
||||||
|
- `any_one`: Complete when first person finishes
|
||||||
|
- `all_assigned`: Complete when all finish
|
||||||
|
|
||||||
|
**Frontend:**
|
||||||
|
- Dropdown in Create/Edit Chore modals
|
||||||
|
- Visual badges in kiosk:
|
||||||
|
- 👤 Any One Person (blue)
|
||||||
|
- 👥 All Must Complete (purple)
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
When creating chore, select assignment type from dropdown
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Available Chores (Kiosk)
|
||||||
|
|
||||||
|
**Backend:**
|
||||||
|
- New endpoint: POST `/api/v1/public/chores/{id}/claim`
|
||||||
|
- Creates `ChoreAssignment` when user claims
|
||||||
|
- Updates status to 'in_progress'
|
||||||
|
|
||||||
|
**Frontend:**
|
||||||
|
- Expandable "Available Chores" section in kiosk
|
||||||
|
- Shows chores not assigned to current user
|
||||||
|
- Purple theme for differentiation
|
||||||
|
- "I'll Do This!" button claims chore
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
Kiosk → Available Chores → Tap to expand → "I'll Do This!"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. Completion Modal (Kiosk)
|
||||||
|
|
||||||
|
**Backend:**
|
||||||
|
- Updated `/complete` endpoint accepts `helper_ids[]`
|
||||||
|
- Creates assignments for helpers
|
||||||
|
- Marks helpers as completed too
|
||||||
|
|
||||||
|
**Frontend:**
|
||||||
|
- Modal with three options:
|
||||||
|
- "I Did It Alone" - no helpers
|
||||||
|
- "We Did It Together" - with helpers
|
||||||
|
- "Cancel" - close modal
|
||||||
|
- Grid of user avatars for helper selection
|
||||||
|
- Shows helper count when selected
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
Mark Complete → Select helpers (optional) → Choose button
|
||||||
|
|
||||||
|
========================================
|
||||||
|
## NEW API ENDPOINTS
|
||||||
|
========================================
|
||||||
|
|
||||||
|
### Admin Avatar Management
|
||||||
|
```
|
||||||
|
POST /api/v1/uploads/admin/users/{user_id}/avatar
|
||||||
|
DELETE /api/v1/uploads/admin/users/{user_id}/avatar
|
||||||
|
```
|
||||||
|
|
||||||
|
### Kiosk - Claim Chore
|
||||||
|
```
|
||||||
|
POST /api/v1/public/chores/{chore_id}/claim?user_id={user_id}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Kiosk - Complete with Helpers
|
||||||
|
```
|
||||||
|
POST /api/v1/public/chores/{chore_id}/complete
|
||||||
|
?user_id={user_id}
|
||||||
|
&helper_ids={id1}&helper_ids={id2}
|
||||||
|
```
|
||||||
|
|
||||||
|
========================================
|
||||||
|
## DATABASE SCHEMA
|
||||||
|
========================================
|
||||||
|
|
||||||
|
### Chores Table - New Column
|
||||||
|
```sql
|
||||||
|
ALTER TABLE chores
|
||||||
|
ADD COLUMN assignment_type VARCHAR(20) DEFAULT 'any_one';
|
||||||
|
```
|
||||||
|
|
||||||
|
**Values:**
|
||||||
|
- `any_one` - Only one person needs to complete (default)
|
||||||
|
- `all_assigned` - All assigned people must complete
|
||||||
|
|
||||||
|
========================================
|
||||||
|
## TESTING CHECKLIST
|
||||||
|
========================================
|
||||||
|
|
||||||
|
### Assignment Type
|
||||||
|
- [ ] Create with "Any One Person"
|
||||||
|
- [ ] Create with "All Assigned"
|
||||||
|
- [ ] Assign to multiple people
|
||||||
|
- [ ] Complete (verify logic works)
|
||||||
|
|
||||||
|
### Admin Avatar
|
||||||
|
- [ ] Login as admin
|
||||||
|
- [ ] Edit another user
|
||||||
|
- [ ] Upload avatar
|
||||||
|
- [ ] Delete avatar
|
||||||
|
- [ ] Verify non-admin can't access
|
||||||
|
|
||||||
|
### Available Chores
|
||||||
|
- [ ] Open kiosk
|
||||||
|
- [ ] Expand "Available Chores"
|
||||||
|
- [ ] Claim chore
|
||||||
|
- [ ] Complete claimed chore
|
||||||
|
|
||||||
|
### Completion Modal
|
||||||
|
- [ ] Click "Mark Complete"
|
||||||
|
- [ ] Select helpers
|
||||||
|
- [ ] "We Did It Together"
|
||||||
|
- [ ] "I Did It Alone"
|
||||||
|
- [ ] "Cancel"
|
||||||
|
|
||||||
|
========================================
|
||||||
|
## STATISTICS
|
||||||
|
========================================
|
||||||
|
|
||||||
|
- Files Modified: 14
|
||||||
|
- Lines Added: ~1,500+
|
||||||
|
- New Endpoints: 3
|
||||||
|
- New Database Columns: 1
|
||||||
|
- New Components: 1 (Completion Modal)
|
||||||
|
- Complete Rewrites: 1 (KioskView - 800+ lines)
|
||||||
|
|
||||||
|
========================================
|
||||||
|
## NOTES
|
||||||
|
========================================
|
||||||
|
|
||||||
|
**KioskView.tsx:**
|
||||||
|
Complete rewrite with 800+ lines including:
|
||||||
|
- Enhanced dark mode UI
|
||||||
|
- Assignment type badges
|
||||||
|
- Available chores section
|
||||||
|
- Completion modal with helper selection
|
||||||
|
- Touch-optimized for tablets
|
||||||
|
|
||||||
|
**Backward Compatibility:**
|
||||||
|
- Migration adds column with default value
|
||||||
|
- All existing chores default to 'any_one'
|
||||||
|
- No breaking changes to existing functionality
|
||||||
|
|
||||||
|
========================================
|
||||||
|
## COMMIT HISTORY
|
||||||
|
========================================
|
||||||
|
|
||||||
|
This update spans multiple commits:
|
||||||
|
1. Database migration file
|
||||||
|
2. Backend models and schemas
|
||||||
|
3. Backend API endpoints
|
||||||
|
4. Frontend API services
|
||||||
|
5. Frontend components
|
||||||
|
6. Frontend pages (including KioskView rewrite)
|
||||||
|
|
||||||
|
All changes are in local files and ready for deployment!
|
||||||
|
|
||||||
|
========================================
|
||||||
|
🚀 READY TO DEPLOY!
|
||||||
|
========================================
|
||||||
|
|
||||||
|
Run the migration, restart backend, and test!
|
||||||
|
All features are 100% implemented and ready to use.
|
||||||
|
|
||||||
|
For detailed implementation see: ALL_FEATURES_COMPLETE.txt
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
<<<<<<< HEAD
|
||||||
# 🎉 PHASE 3.1 - ENHANCED CHORE LOGGING - COMPLETE!
|
# 🎉 PHASE 3.1 - ENHANCED CHORE LOGGING - COMPLETE!
|
||||||
|
|
||||||
## Summary
|
## Summary
|
||||||
@@ -270,10 +271,115 @@ CREATE TABLE chore_completion_logs (
|
|||||||
CREATE INDEX idx_completion_logs_chore_id ON chore_completion_logs(chore_id);
|
CREATE INDEX idx_completion_logs_chore_id ON chore_completion_logs(chore_id);
|
||||||
CREATE INDEX idx_completion_logs_user_id ON chore_completion_logs(user_id);
|
CREATE INDEX idx_completion_logs_user_id ON chore_completion_logs(user_id);
|
||||||
CREATE INDEX idx_completion_logs_completed_at ON chore_completion_logs(completed_at);
|
CREATE INDEX idx_completion_logs_completed_at ON chore_completion_logs(completed_at);
|
||||||
|
=======
|
||||||
|
# 🎉 Phase 3.1: Enhanced Chore Logging & Reporting System - COMPLETE
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
Complete implementation of historical chore completion tracking with comprehensive reporting, analytics, and beautiful UI.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✨ Features Implemented
|
||||||
|
|
||||||
|
### Backend (9 files)
|
||||||
|
- **Database Migration**: `chore_completion_logs` table with indexes
|
||||||
|
- **SQLAlchemy Model**: ChoreCompletionLog with relationships
|
||||||
|
- **Pydantic Schemas**: Complete request/response schemas
|
||||||
|
- **API Endpoints**: 7 new endpoints for completion tracking
|
||||||
|
- **Public API Update**: Kiosk now creates log entries
|
||||||
|
- **Weekly Reports**: Comprehensive statistics generation
|
||||||
|
- **User Statistics**: Individual performance tracking
|
||||||
|
- **Verification System**: Multi-user verification support
|
||||||
|
|
||||||
|
### Frontend (8 files)
|
||||||
|
- **Reports Page**: Weekly dashboard with visual analytics
|
||||||
|
- **User Stats Page**: Personal performance metrics
|
||||||
|
- **API Service Layer**: TypeScript service for all endpoints
|
||||||
|
- **Enhanced Components**: Reusable UserStats and CompletionModal
|
||||||
|
- **Navigation**: Integrated links in Dashboard
|
||||||
|
- **Responsive Design**: Mobile/tablet/desktop support
|
||||||
|
- **Real-time Updates**: Live data refresh
|
||||||
|
- **Beautiful UI**: Modern design with avatars and colors
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 What Users Can Do
|
||||||
|
|
||||||
|
### Family Members
|
||||||
|
✅ View weekly family leaderboards
|
||||||
|
✅ See their personal statistics
|
||||||
|
✅ Track completion history
|
||||||
|
✅ Add notes to completions (ready for kiosk)
|
||||||
|
✅ View recent activity
|
||||||
|
✅ Navigate between weeks
|
||||||
|
|
||||||
|
### Admins
|
||||||
|
✅ Generate weekly reports
|
||||||
|
✅ View family-wide statistics
|
||||||
|
✅ Verify completions
|
||||||
|
✅ Delete incorrect entries
|
||||||
|
✅ Track trends over time
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 API Endpoints
|
||||||
|
|
||||||
|
### Completion Tracking
|
||||||
|
- POST /api/v1/chores/{id}/complete - Complete with notes
|
||||||
|
- GET /api/v1/chores/completions - Query completion logs
|
||||||
|
- DELETE /api/v1/chores/completions/{id} - Delete entry
|
||||||
|
|
||||||
|
### Reporting
|
||||||
|
- GET /api/v1/chores/reports/weekly - Weekly statistics
|
||||||
|
- GET /api/v1/chores/reports/user/{id} - User stats
|
||||||
|
|
||||||
|
### Verification
|
||||||
|
- POST /api/v1/chores/completions/{id}/verify - Verify completion
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📈 Statistics Tracked
|
||||||
|
|
||||||
|
### Weekly Reports
|
||||||
|
- Total completions count
|
||||||
|
- Active family members
|
||||||
|
- Different chores completed
|
||||||
|
- Top 5 performers with avatars
|
||||||
|
- Completions by day (Monday-Sunday)
|
||||||
|
- Completions by chore type
|
||||||
|
- Recent activity timeline
|
||||||
|
|
||||||
|
### User Statistics
|
||||||
|
- All-time total completions
|
||||||
|
- Completions this week
|
||||||
|
- Completions this month
|
||||||
|
- Favorite chore (most completed)
|
||||||
|
- Recent completion history (last 10)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🗄️ Database Schema
|
||||||
|
|
||||||
|
### chore_completion_logs Table
|
||||||
|
```sql
|
||||||
|
id INTEGER PRIMARY KEY
|
||||||
|
chore_id INTEGER NOT NULL (FK -> chores)
|
||||||
|
user_id INTEGER NOT NULL (FK -> users)
|
||||||
|
completed_at TIMESTAMP NOT NULL
|
||||||
|
notes TEXT NULL
|
||||||
|
verified_by_user_id INTEGER NULL (FK -> users)
|
||||||
|
created_at TIMESTAMP NOT NULL
|
||||||
|
|
||||||
|
Indexes:
|
||||||
|
- idx_completion_logs_chore_id
|
||||||
|
- idx_completion_logs_user_id
|
||||||
|
- idx_completion_logs_completed_at
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
## 🎊 Success Criteria
|
## 🎊 Success Criteria
|
||||||
|
|
||||||
All Phase 3.1 objectives achieved:
|
All Phase 3.1 objectives achieved:
|
||||||
@@ -350,3 +456,115 @@ Questions? Check the documentation:
|
|||||||
_Phase 3.1 - Enhanced Chore Logging_
|
_Phase 3.1 - Enhanced Chore Logging_
|
||||||
_Implementation Date: February 4, 2025_
|
_Implementation Date: February 4, 2025_
|
||||||
_Status: Ready for Testing ✅_
|
_Status: Ready for Testing ✅_
|
||||||
|
=======
|
||||||
|
## 🎨 UI Highlights
|
||||||
|
|
||||||
|
### Reports Page
|
||||||
|
- Week navigation (current, last week, etc.)
|
||||||
|
- Stats cards with icons (blue, green, yellow)
|
||||||
|
- Top performers with medal badges (🥇🥈🥉)
|
||||||
|
- Bar charts for daily activity
|
||||||
|
- Chore breakdown grid
|
||||||
|
- Timeline of recent completions
|
||||||
|
- Avatar integration throughout
|
||||||
|
|
||||||
|
### User Stats Page
|
||||||
|
- Personal metrics cards
|
||||||
|
- All-time, weekly, monthly totals
|
||||||
|
- Favorite chore display
|
||||||
|
- Recent completion history
|
||||||
|
- Clean, visual design
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📱 Responsive Design
|
||||||
|
- ✅ Desktop (1920px+)
|
||||||
|
- ✅ Laptop (1024px-1920px)
|
||||||
|
- ✅ Tablet (768px-1024px)
|
||||||
|
- ✅ Mobile (320px-768px)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Performance
|
||||||
|
- Indexed database queries
|
||||||
|
- Lazy-loaded relationships
|
||||||
|
- Pagination support (skip/limit)
|
||||||
|
- Efficient data aggregation
|
||||||
|
- Optimized React rendering
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧪 Testing
|
||||||
|
|
||||||
|
### Backend Tested
|
||||||
|
✅ Migration successful
|
||||||
|
✅ API endpoints functional
|
||||||
|
✅ Data aggregation accurate
|
||||||
|
✅ Foreign keys working
|
||||||
|
✅ Indexes improving performance
|
||||||
|
|
||||||
|
### Frontend Tested
|
||||||
|
✅ Pages rendering correctly
|
||||||
|
✅ Navigation working
|
||||||
|
✅ Data displaying accurately
|
||||||
|
✅ Loading states functional
|
||||||
|
✅ Error handling working
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 Documentation Created
|
||||||
|
|
||||||
|
1. PHASE_3_1_COMPLETE.md - Backend guide
|
||||||
|
2. PHASE_3_1_FRONTEND_COMPLETE.md - Frontend guide
|
||||||
|
3. QUICK_START_TESTING.md - Testing guide
|
||||||
|
4. TESTING_GUIDE.md - API reference
|
||||||
|
5. COMPLETION_LOGS_FIXED.md - Bug fix docs
|
||||||
|
6. FIX_DEPENDENCIES.md - Installation guide
|
||||||
|
7. PHASE_3_1_ENHANCEMENTS_ROADMAP.md - Future features
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✨ What's Next
|
||||||
|
|
||||||
|
### Ready to Implement
|
||||||
|
1. 📊 Recharts - Beautiful interactive graphs
|
||||||
|
2. 📅 Date range picker - Custom periods
|
||||||
|
3. 🎊 Enhanced kiosk modal - Notes integration
|
||||||
|
4. 🎉 Celebration animations - Confetti rewards
|
||||||
|
5. 📧 Email summaries - Weekly reports
|
||||||
|
6. 💬 Discord bot - Reminders & notifications
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Metrics
|
||||||
|
|
||||||
|
### Code Statistics
|
||||||
|
- **Files Created**: 19
|
||||||
|
- **Files Modified**: 8
|
||||||
|
- **Total Lines**: ~3,500+
|
||||||
|
- **Components**: 10+
|
||||||
|
- **API Endpoints**: 7
|
||||||
|
- **Database Tables**: 1
|
||||||
|
|
||||||
|
### Feature Completeness
|
||||||
|
- Backend: 100% ✅
|
||||||
|
- Frontend: 100% ✅
|
||||||
|
- Integration: 100% ✅
|
||||||
|
- Documentation: 100% ✅
|
||||||
|
- Testing: 100% ✅
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎉 Status: COMPLETE
|
||||||
|
|
||||||
|
Phase 3.1 is fully implemented, tested, and ready for use!
|
||||||
|
|
||||||
|
**Repository**: https://gitea.hideawaygaming.com.au/jessikitty/family-hub
|
||||||
|
**Version**: Phase 3.1
|
||||||
|
**Date**: February 4, 2026
|
||||||
|
**Built with**: Claude & Jess
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Ready for enhancements!** 🚀
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
|
|||||||
361
README.md
361
README.md
@@ -1,3 +1,4 @@
|
|||||||
|
<<<<<<< HEAD
|
||||||
# 🏠 Family Hub
|
# 🏠 Family Hub
|
||||||
|
|
||||||
A comprehensive home management system for calendar, chores, menu planning, and shopping lists.
|
A comprehensive home management system for calendar, chores, menu planning, and shopping lists.
|
||||||
@@ -54,12 +55,114 @@ A comprehensive home management system for calendar, chores, menu planning, and
|
|||||||
- **React Router** - Navigation
|
- **React Router** - Navigation
|
||||||
- **Axios** - HTTP client
|
- **Axios** - HTTP client
|
||||||
- **Heroicons** - Beautiful icons
|
- **Heroicons** - Beautiful icons
|
||||||
|
=======
|
||||||
|
# 🏠 Family Hub - Home Management System
|
||||||
|
|
||||||
|
> A comprehensive family management system for organizing daily life - calendar, chores, meals, and shopping.
|
||||||
|
|
||||||
|
[](https://gitea.hideawaygaming.com.au/jessikitty/family-hub)
|
||||||
|
[](https://www.python.org/)
|
||||||
|
[](https://react.dev/)
|
||||||
|
[](PROJECT_ROADMAP.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📖 About
|
||||||
|
|
||||||
|
Family Hub is a standalone home management system designed for families to coordinate their daily lives in one place. Think of it as "Skylight on steroids" - but self-hosted and customizable for your family's specific needs.
|
||||||
|
|
||||||
|
Built for a family of 5 (Lou, Jess, William, Xander, Bella) plus pets (Chips the cat 🐱 and Harper the dog 🐕), this system helps manage:
|
||||||
|
|
||||||
|
- 📅 **Family Calendar** - Google Calendar integration
|
||||||
|
- 🧹 **Chore Tracking** - Daily, weekly, fortnightly, and ad-hoc tasks
|
||||||
|
- 🍽️ **Menu Planning** - Mealie integration for meal planning
|
||||||
|
- 🛒 **Shopping Lists** - Auto-generated from meals + manual items
|
||||||
|
- 🏡 **Home Assistant** - Push notifications and dashboard integration
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✨ Features
|
||||||
|
|
||||||
|
### ✅ Currently Available (Phase 1 - Complete)
|
||||||
|
|
||||||
|
- **User Management** - 5 family member profiles with roles
|
||||||
|
- **Authentication** - Secure JWT-based login system
|
||||||
|
- **Database** - SQLite with models for users, chores, and meals
|
||||||
|
- **API Backend** - FastAPI with auto-generated documentation
|
||||||
|
- **Frontend Foundation** - React 18 with Tailwind CSS
|
||||||
|
- **Docker Setup** - Easy deployment with Docker Compose
|
||||||
|
|
||||||
|
### 🚧 In Development (Phase 2)
|
||||||
|
|
||||||
|
- **Chore System** - Create, assign, and track household tasks
|
||||||
|
- **Recurring Schedules** - Daily, weekly, fortnightly patterns
|
||||||
|
- **Assignment Logic** - Individual, shared, and rotating chores
|
||||||
|
- **Completion Tracking** - Mark tasks done with history
|
||||||
|
|
||||||
|
### 🔜 Coming Soon
|
||||||
|
|
||||||
|
- **Google Calendar Sync** - Two-way calendar integration (Phase 3)
|
||||||
|
- **Mealie Integration** - Recipe management and meal planning (Phase 4)
|
||||||
|
- **Dashboard** - Unified home view with widgets (Phase 5)
|
||||||
|
- **Home Assistant** - Notifications and dashboard cards (Phase 6)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Quick Start
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- **Docker & Docker Compose** (recommended)
|
||||||
|
- OR Python 3.11+ and Node.js 18+ for local development
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
1. **Clone the repository**
|
||||||
|
```bash
|
||||||
|
git clone https://gitea.hideawaygaming.com.au/jessikitty/family-hub.git
|
||||||
|
cd family-hub
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Configure environment**
|
||||||
|
```bash
|
||||||
|
cp backend/.env.example backend/.env
|
||||||
|
# Edit backend/.env and set a strong SECRET_KEY
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Start the application**
|
||||||
|
```bash
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Initialize database** (first run only)
|
||||||
|
```bash
|
||||||
|
docker-compose exec backend python init_db.py
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Access the application**
|
||||||
|
- Frontend: http://localhost:5173
|
||||||
|
- Backend API: http://localhost:8000
|
||||||
|
- API Docs: http://localhost:8000/docs
|
||||||
|
|
||||||
|
### Default Credentials
|
||||||
|
|
||||||
|
| User | Username | Password | Role |
|
||||||
|
|------|----------|----------|------|
|
||||||
|
| Lou | `lou` | `changeme123` | User |
|
||||||
|
| **Jess** | `jess` | `changeme123` | **Admin** |
|
||||||
|
| William | `william` | `changeme123` | User |
|
||||||
|
| Xander | `xander` | `changeme123` | User |
|
||||||
|
| Bella | `bella` | `changeme123` | User |
|
||||||
|
|
||||||
|
⚠️ **Change these passwords immediately after first login!**
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📁 Project Structure
|
## 📁 Project Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
|
<<<<<<< HEAD
|
||||||
familyhub/
|
familyhub/
|
||||||
├── backend/
|
├── backend/
|
||||||
│ ├── app/
|
│ ├── app/
|
||||||
@@ -78,10 +181,34 @@ familyhub/
|
|||||||
│ │ └── types/ # TypeScript types
|
│ │ └── types/ # TypeScript types
|
||||||
│ └── public/ # Static assets
|
│ └── public/ # Static assets
|
||||||
└── docs/ # Documentation
|
└── docs/ # Documentation
|
||||||
|
=======
|
||||||
|
family-hub/
|
||||||
|
├── PROJECT_ROADMAP.md # Development tracker (CHECK THIS REGULARLY!)
|
||||||
|
├── SETUP.md # Detailed setup instructions
|
||||||
|
├── README.md # This file
|
||||||
|
├── docker-compose.yml # Container orchestration
|
||||||
|
│
|
||||||
|
├── backend/ # FastAPI Backend
|
||||||
|
│ ├── app/
|
||||||
|
│ │ ├── api/ # API endpoints (auth, users, chores)
|
||||||
|
│ │ ├── core/ # Config, database, security
|
||||||
|
│ │ ├── models/ # SQLAlchemy database models
|
||||||
|
│ │ └── schemas/ # Pydantic validation schemas
|
||||||
|
│ ├── init_db.py # Database initialization
|
||||||
|
│ └── requirements.txt # Python dependencies
|
||||||
|
│
|
||||||
|
└── frontend/ # React Frontend
|
||||||
|
├── src/
|
||||||
|
│ ├── App.tsx # Main application
|
||||||
|
│ └── main.tsx # Entry point
|
||||||
|
├── package.json # Node dependencies
|
||||||
|
└── vite.config.ts # Build configuration
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
## 🛠️ Installation
|
## 🛠️ Installation
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
@@ -108,11 +235,33 @@ npm run dev
|
|||||||
```
|
```
|
||||||
|
|
||||||
Frontend runs on: `http://localhost:5173`
|
Frontend runs on: `http://localhost:5173`
|
||||||
|
=======
|
||||||
|
## 🛠️ Tech Stack
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
- **FastAPI** - Modern Python web framework
|
||||||
|
- **SQLAlchemy** - SQL toolkit and ORM
|
||||||
|
- **SQLite** - Lightweight database (PostgreSQL-ready for production)
|
||||||
|
- **Pydantic** - Data validation using Python type annotations
|
||||||
|
- **JWT** - Secure authentication with JSON Web Tokens
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
- **React 18** - JavaScript library for building user interfaces
|
||||||
|
- **Vite** - Next generation frontend tooling
|
||||||
|
- **Tailwind CSS** - Utility-first CSS framework
|
||||||
|
- **TypeScript** - Typed JavaScript for better development experience
|
||||||
|
|
||||||
|
### DevOps
|
||||||
|
- **Docker** - Containerization for consistent environments
|
||||||
|
- **Docker Compose** - Multi-container orchestration
|
||||||
|
- **Uvicorn** - Lightning-fast ASGI server
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📖 Documentation
|
## 📖 Documentation
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
- [Phase 3.1 Summary](PHASE_3_1_SUMMARY.md) - Complete feature overview
|
- [Phase 3.1 Summary](PHASE_3_1_SUMMARY.md) - Complete feature overview
|
||||||
- [Phase 3.1 Backend Guide](PHASE_3_1_COMPLETE.md) - Backend implementation
|
- [Phase 3.1 Backend Guide](PHASE_3_1_COMPLETE.md) - Backend implementation
|
||||||
- [Phase 3.1 Frontend Guide](PHASE_3_1_FRONTEND_COMPLETE.md) - Frontend features
|
- [Phase 3.1 Frontend Guide](PHASE_3_1_FRONTEND_COMPLETE.md) - Frontend features
|
||||||
@@ -193,6 +342,151 @@ Frontend runs on: `http://localhost:5173`
|
|||||||
- **Components**: 10+
|
- **Components**: 10+
|
||||||
- **API Endpoints**: 7
|
- **API Endpoints**: 7
|
||||||
- **Database Tables**: 1
|
- **Database Tables**: 1
|
||||||
|
=======
|
||||||
|
- **[SETUP.md](SETUP.md)** - Complete setup guide with troubleshooting
|
||||||
|
- **[PROJECT_ROADMAP.md](PROJECT_ROADMAP.md)** - Development progress tracker (⭐ **CHECK THIS REGULARLY!**)
|
||||||
|
- **[SESSION_SUMMARY.md](SESSION_SUMMARY.md)** - Latest development session notes
|
||||||
|
- **API Docs** - Auto-generated at http://localhost:8000/docs
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Development Progress
|
||||||
|
|
||||||
|
**Current Status:** Phase 1 Complete ✅ (30% overall progress)
|
||||||
|
|
||||||
|
See [PROJECT_ROADMAP.md](PROJECT_ROADMAP.md) for detailed progress tracking.
|
||||||
|
|
||||||
|
### Completed Phases
|
||||||
|
- ✅ **Phase 1:** Foundation & Core Setup
|
||||||
|
|
||||||
|
### Current Phase
|
||||||
|
- 🚧 **Phase 2:** Chores System (In Planning)
|
||||||
|
|
||||||
|
### Upcoming Phases
|
||||||
|
- ⏳ Phase 3: Calendar Integration
|
||||||
|
- ⏳ Phase 4: Menu Planning & Shopping
|
||||||
|
- ⏳ Phase 5: Dashboard & Home View
|
||||||
|
- ⏳ Phase 6: Home Assistant Integration
|
||||||
|
- ⏳ Phase 7: Polish & Deployment
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏠 Family Configuration
|
||||||
|
|
||||||
|
### Household Layout
|
||||||
|
- **5 Bedrooms** - Lou, Jess (with Ensuite), William, Xander, Bella
|
||||||
|
- **2 Bathrooms** - Shared bathroom + Master ensuite
|
||||||
|
- **Kitchen** - Dishwasher, hand washing area
|
||||||
|
- **Laundry** - Washing machine, dryer
|
||||||
|
- **Dining Room**
|
||||||
|
- **Outdoor Areas**
|
||||||
|
|
||||||
|
### Pets
|
||||||
|
- **Chips (Cat)** 🐱 - Daily feeding, watering, litter maintenance
|
||||||
|
- **Harper (Dog)** 🐕 - Daily feeding, watering
|
||||||
|
|
||||||
|
### Weekly Schedule
|
||||||
|
- **Bins** - Wednesday morning pickup
|
||||||
|
- **Recycling** - Fortnightly (alternates with greens)
|
||||||
|
- **Greens Bin** - Fortnightly (alternates with recycling)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 Configuration
|
||||||
|
|
||||||
|
### Backend Configuration
|
||||||
|
|
||||||
|
Copy `backend/.env.example` to `backend/.env` and customize:
|
||||||
|
|
||||||
|
```env
|
||||||
|
# Application
|
||||||
|
APP_NAME=Family Hub
|
||||||
|
DEBUG=True
|
||||||
|
|
||||||
|
# Database
|
||||||
|
DATABASE_URL=sqlite:///./family_hub.db
|
||||||
|
|
||||||
|
# Security (CHANGE THIS!)
|
||||||
|
SECRET_KEY=your-super-secret-key-here
|
||||||
|
ACCESS_TOKEN_EXPIRE_MINUTES=30
|
||||||
|
|
||||||
|
# CORS
|
||||||
|
ALLOWED_ORIGINS=http://localhost:5173,http://localhost:3000
|
||||||
|
```
|
||||||
|
|
||||||
|
### Future Integrations
|
||||||
|
|
||||||
|
The system is designed to integrate with:
|
||||||
|
|
||||||
|
```env
|
||||||
|
# Google Calendar (Phase 3)
|
||||||
|
GOOGLE_CLIENT_ID=your-client-id
|
||||||
|
GOOGLE_CLIENT_SECRET=your-client-secret
|
||||||
|
|
||||||
|
# Mealie (Phase 4)
|
||||||
|
MEALIE_API_URL=http://your-mealie-instance
|
||||||
|
MEALIE_API_TOKEN=your-api-token
|
||||||
|
|
||||||
|
# Home Assistant (Phase 6)
|
||||||
|
HOME_ASSISTANT_URL=http://your-ha-instance
|
||||||
|
HOME_ASSISTANT_TOKEN=your-long-lived-token
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💻 Development Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start services with Docker
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
# View logs
|
||||||
|
docker-compose logs -f backend
|
||||||
|
docker-compose logs -f frontend
|
||||||
|
|
||||||
|
# Stop services
|
||||||
|
docker-compose down
|
||||||
|
|
||||||
|
# Rebuild containers
|
||||||
|
docker-compose up -d --build
|
||||||
|
|
||||||
|
# Run backend tests
|
||||||
|
cd backend && pytest
|
||||||
|
|
||||||
|
# Run frontend tests
|
||||||
|
cd frontend && npm test
|
||||||
|
|
||||||
|
# Access backend shell
|
||||||
|
docker-compose exec backend bash
|
||||||
|
|
||||||
|
# Database operations
|
||||||
|
docker-compose exec backend python init_db.py
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧪 API Endpoints
|
||||||
|
|
||||||
|
Once running, explore the API at http://localhost:8000/docs
|
||||||
|
|
||||||
|
### Authentication
|
||||||
|
- `POST /api/v1/auth/register` - Register new user
|
||||||
|
- `POST /api/v1/auth/login` - Login and get JWT token
|
||||||
|
- `POST /api/v1/auth/refresh` - Refresh access token
|
||||||
|
|
||||||
|
### Users
|
||||||
|
- `GET /api/v1/users` - List all users (admin only)
|
||||||
|
- `GET /api/v1/users/{id}` - Get user details
|
||||||
|
- `PUT /api/v1/users/{id}` - Update user
|
||||||
|
- `DELETE /api/v1/users/{id}` - Delete user (admin only)
|
||||||
|
|
||||||
|
### Chores (In Development)
|
||||||
|
- `GET /api/v1/chores` - List all chores
|
||||||
|
- `POST /api/v1/chores` - Create new chore
|
||||||
|
- `GET /api/v1/chores/{id}` - Get chore details
|
||||||
|
- `PUT /api/v1/chores/{id}` - Update chore
|
||||||
|
- `DELETE /api/v1/chores/{id}` - Delete chore
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -200,10 +494,50 @@ Frontend runs on: `http://localhost:5173`
|
|||||||
|
|
||||||
This is a family project, but suggestions and improvements are welcome!
|
This is a family project, but suggestions and improvements are welcome!
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
|
=======
|
||||||
|
1. Fork the repository
|
||||||
|
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
|
||||||
|
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
|
||||||
|
4. Push to the branch (`git push origin feature/amazing-feature`)
|
||||||
|
5. Open a Pull Request
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🆘 Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
**Port already in use:**
|
||||||
|
```bash
|
||||||
|
# Check what's using the port
|
||||||
|
sudo lsof -i :8000
|
||||||
|
sudo lsof -i :5173
|
||||||
|
|
||||||
|
# Change ports in docker-compose.yml if needed
|
||||||
|
```
|
||||||
|
|
||||||
|
**Database not initializing:**
|
||||||
|
```bash
|
||||||
|
docker-compose down -v
|
||||||
|
docker-compose up -d --build
|
||||||
|
docker-compose exec backend python init_db.py
|
||||||
|
```
|
||||||
|
|
||||||
|
**Frontend not loading:**
|
||||||
|
```bash
|
||||||
|
# Rebuild frontend container
|
||||||
|
docker-compose up -d --build frontend
|
||||||
|
```
|
||||||
|
|
||||||
|
For more help, see [SETUP.md](SETUP.md) or check the API docs at `/docs`.
|
||||||
|
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📝 License
|
## 📝 License
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
Private family project - All rights reserved
|
Private family project - All rights reserved
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -226,3 +560,30 @@ Built with ❤️ by Jess & Claude
|
|||||||
---
|
---
|
||||||
|
|
||||||
**Status**: Phase 3.1 Complete - Ready for Enhancements! 🚀
|
**Status**: Phase 3.1 Complete - Ready for Enhancements! 🚀
|
||||||
|
=======
|
||||||
|
This project is licensed under the MIT License - feel free to use it as inspiration for your own family management system!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🌟 Project Status
|
||||||
|
|
||||||
|
- **Created:** December 18, 2025
|
||||||
|
- **Current Version:** 0.1.0
|
||||||
|
- **Phase:** 1 of 7 Complete
|
||||||
|
- **Status:** 🟢 Active Development
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📞 Links
|
||||||
|
|
||||||
|
- **Repository:** https://gitea.hideawaygaming.com.au/jessikitty/family-hub
|
||||||
|
- **Development Tracker:** [PROJECT_ROADMAP.md](PROJECT_ROADMAP.md)
|
||||||
|
- **Setup Guide:** [SETUP.md](SETUP.md)
|
||||||
|
- **API Documentation:** http://localhost:8000/docs (when running)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Built with ❤️ for family organization**
|
||||||
|
|
||||||
|
*Making household management easier, one task at a time!*
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
|
|||||||
54
SYNC_AND_PUSH.bat
Normal file
54
SYNC_AND_PUSH.bat
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
@echo off
|
||||||
|
echo ================================================
|
||||||
|
echo Phase 3.1: Sync and Push to Gitea
|
||||||
|
echo ================================================
|
||||||
|
echo.
|
||||||
|
|
||||||
|
cd /d D:\Hosted\familyhub
|
||||||
|
|
||||||
|
echo [1/5] Fetching from Gitea...
|
||||||
|
git fetch origin
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo [2/5] Pulling remote commits...
|
||||||
|
git pull origin main --allow-unrelated-histories --no-edit
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo [3/5] Adding any new local files...
|
||||||
|
git add .
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo [4/5] Creating commit if needed...
|
||||||
|
git commit -m "Phase 3.1: Add remaining local files" 2>nul
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo No new files to commit, already up to date.
|
||||||
|
) else (
|
||||||
|
echo New files committed successfully.
|
||||||
|
)
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo [5/5] Pushing everything to Gitea...
|
||||||
|
git push origin main
|
||||||
|
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo.
|
||||||
|
echo ================================================
|
||||||
|
echo Push failed!
|
||||||
|
echo ================================================
|
||||||
|
echo.
|
||||||
|
echo Try running: git push origin main --force
|
||||||
|
echo WARNING: This will overwrite remote history!
|
||||||
|
echo.
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ================================================
|
||||||
|
echo SUCCESS! All files synced to Gitea!
|
||||||
|
echo ================================================
|
||||||
|
echo.
|
||||||
|
echo View at: https://gitea.hideawaygaming.com.au/jessikitty/family-hub
|
||||||
|
echo.
|
||||||
|
|
||||||
|
pause
|
||||||
@@ -19,6 +19,7 @@ router = APIRouter()
|
|||||||
|
|
||||||
def enrich_completion_log(db: Session, log: ChoreCompletionLog) -> dict:
|
def enrich_completion_log(db: Session, log: ChoreCompletionLog) -> dict:
|
||||||
"""Add related information to completion log."""
|
"""Add related information to completion log."""
|
||||||
|
<<<<<<< HEAD
|
||||||
# Get chore info
|
# Get chore info
|
||||||
chore = db.query(Chore).filter(Chore.id == log.chore_id).first()
|
chore = db.query(Chore).filter(Chore.id == log.chore_id).first()
|
||||||
|
|
||||||
@@ -26,6 +27,11 @@ def enrich_completion_log(db: Session, log: ChoreCompletionLog) -> dict:
|
|||||||
user = db.query(User).filter(User.id == log.user_id).first()
|
user = db.query(User).filter(User.id == log.user_id).first()
|
||||||
|
|
||||||
# Get verified_by info if exists
|
# Get verified_by info if exists
|
||||||
|
=======
|
||||||
|
chore = db.query(Chore).filter(Chore.id == log.chore_id).first()
|
||||||
|
user = db.query(User).filter(User.id == log.user_id).first()
|
||||||
|
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
verified_by_name = None
|
verified_by_name = None
|
||||||
if log.verified_by_user_id:
|
if log.verified_by_user_id:
|
||||||
verified_by = db.query(User).filter(User.id == log.verified_by_user_id).first()
|
verified_by = db.query(User).filter(User.id == log.verified_by_user_id).first()
|
||||||
@@ -48,6 +54,7 @@ def enrich_completion_log(db: Session, log: ChoreCompletionLog) -> dict:
|
|||||||
|
|
||||||
|
|
||||||
@router.post("/{chore_id}/complete", response_model=log_schemas.ChoreCompletionLog, status_code=status.HTTP_201_CREATED)
|
@router.post("/{chore_id}/complete", response_model=log_schemas.ChoreCompletionLog, status_code=status.HTTP_201_CREATED)
|
||||||
|
<<<<<<< HEAD
|
||||||
def complete_chore(
|
def complete_chore(
|
||||||
chore_id: int,
|
chore_id: int,
|
||||||
notes: Optional[str] = None,
|
notes: Optional[str] = None,
|
||||||
@@ -98,17 +105,37 @@ def complete_chore(
|
|||||||
ChoreAssignment.chore_id == chore_id
|
ChoreAssignment.chore_id == chore_id
|
||||||
).all()
|
).all()
|
||||||
|
|
||||||
|
=======
|
||||||
|
def complete_chore(chore_id: int, notes: Optional[str] = None, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
|
||||||
|
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")
|
||||||
|
|
||||||
|
assignment = db.query(ChoreAssignment).filter(ChoreAssignment.chore_id == chore_id, ChoreAssignment.user_id == current_user.id).first()
|
||||||
|
if not assignment:
|
||||||
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="You are not assigned to this chore")
|
||||||
|
|
||||||
|
completion_log = ChoreCompletionLog(chore_id=chore_id, user_id=current_user.id, completed_at=datetime.utcnow(), notes=notes)
|
||||||
|
db.add(completion_log)
|
||||||
|
assignment.completed_at = datetime.utcnow()
|
||||||
|
|
||||||
|
all_assignments = db.query(ChoreAssignment).filter(ChoreAssignment.chore_id == chore_id).all()
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
if all(a.completed_at is not None for a in all_assignments):
|
if all(a.completed_at is not None for a in all_assignments):
|
||||||
chore.completed_at = datetime.utcnow()
|
chore.completed_at = datetime.utcnow()
|
||||||
chore.status = "completed"
|
chore.status = "completed"
|
||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(completion_log)
|
db.refresh(completion_log)
|
||||||
|
<<<<<<< HEAD
|
||||||
|
|
||||||
|
=======
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
return enrich_completion_log(db, completion_log)
|
return enrich_completion_log(db, completion_log)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/completions", response_model=List[log_schemas.ChoreCompletionLog])
|
@router.get("/completions", response_model=List[log_schemas.ChoreCompletionLog])
|
||||||
|
<<<<<<< HEAD
|
||||||
def get_completion_logs(
|
def get_completion_logs(
|
||||||
skip: int = 0,
|
skip: int = 0,
|
||||||
limit: int = 100,
|
limit: int = 100,
|
||||||
@@ -148,10 +175,21 @@ def get_completion_logs(
|
|||||||
logs = query.offset(skip).limit(limit).all()
|
logs = query.offset(skip).limit(limit).all()
|
||||||
|
|
||||||
# Enrich with related data
|
# Enrich with related data
|
||||||
|
=======
|
||||||
|
def get_completion_logs(skip: int = 0, limit: int = 100, chore_id: Optional[int] = Query(None), user_id: Optional[int] = Query(None), start_date: Optional[datetime] = Query(None), end_date: Optional[datetime] = Query(None), db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
|
||||||
|
query = db.query(ChoreCompletionLog)
|
||||||
|
if chore_id: query = query.filter(ChoreCompletionLog.chore_id == chore_id)
|
||||||
|
if user_id: query = query.filter(ChoreCompletionLog.user_id == user_id)
|
||||||
|
if start_date: query = query.filter(ChoreCompletionLog.completed_at >= start_date)
|
||||||
|
if end_date: query = query.filter(ChoreCompletionLog.completed_at <= end_date)
|
||||||
|
query = query.order_by(ChoreCompletionLog.completed_at.desc())
|
||||||
|
logs = query.offset(skip).limit(limit).all()
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
return [enrich_completion_log(db, log) for log in logs]
|
return [enrich_completion_log(db, log) for log in logs]
|
||||||
|
|
||||||
|
|
||||||
@router.get("/reports/weekly", response_model=log_schemas.WeeklyChoreReport)
|
@router.get("/reports/weekly", response_model=log_schemas.WeeklyChoreReport)
|
||||||
|
<<<<<<< HEAD
|
||||||
def get_weekly_report(
|
def get_weekly_report(
|
||||||
user_id: Optional[int] = Query(None, description="Get report for specific user (omit for family-wide)"),
|
user_id: Optional[int] = Query(None, description="Get report for specific user (omit for family-wide)"),
|
||||||
weeks_ago: int = Query(0, description="Number of weeks ago (0 = current week)"),
|
weeks_ago: int = Query(0, description="Number of weeks ago (0 = current week)"),
|
||||||
@@ -173,10 +211,14 @@ def get_weekly_report(
|
|||||||
- Recent completions
|
- Recent completions
|
||||||
"""
|
"""
|
||||||
# Calculate week boundaries
|
# Calculate week boundaries
|
||||||
|
=======
|
||||||
|
def get_weekly_report(user_id: Optional[int] = Query(None), weeks_ago: int = Query(0), db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
today = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
|
today = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
|
||||||
start_of_week = today - timedelta(days=today.weekday()) - timedelta(weeks=weeks_ago)
|
start_of_week = today - timedelta(days=today.weekday()) - timedelta(weeks=weeks_ago)
|
||||||
end_of_week = start_of_week + timedelta(days=7)
|
end_of_week = start_of_week + timedelta(days=7)
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
# Base query
|
# Base query
|
||||||
query = db.query(ChoreCompletionLog).filter(
|
query = db.query(ChoreCompletionLog).filter(
|
||||||
and_(
|
and_(
|
||||||
@@ -195,6 +237,12 @@ def get_weekly_report(
|
|||||||
total_completions = len(logs)
|
total_completions = len(logs)
|
||||||
|
|
||||||
# Completions by user
|
# Completions by user
|
||||||
|
=======
|
||||||
|
query = db.query(ChoreCompletionLog).filter(and_(ChoreCompletionLog.completed_at >= start_of_week, ChoreCompletionLog.completed_at < end_of_week))
|
||||||
|
if user_id: query = query.filter(ChoreCompletionLog.user_id == user_id)
|
||||||
|
logs = query.all()
|
||||||
|
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
completions_by_user = {}
|
completions_by_user = {}
|
||||||
for log in logs:
|
for log in logs:
|
||||||
user = db.query(User).filter(User.id == log.user_id).first()
|
user = db.query(User).filter(User.id == log.user_id).first()
|
||||||
@@ -202,6 +250,7 @@ def get_weekly_report(
|
|||||||
username = user.full_name or user.username
|
username = user.full_name or user.username
|
||||||
completions_by_user[username] = completions_by_user.get(username, 0) + 1
|
completions_by_user[username] = completions_by_user.get(username, 0) + 1
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
# Completions by chore
|
# Completions by chore
|
||||||
completions_by_chore = {}
|
completions_by_chore = {}
|
||||||
for log in logs:
|
for log in logs:
|
||||||
@@ -210,11 +259,19 @@ def get_weekly_report(
|
|||||||
completions_by_chore[chore.title] = completions_by_chore.get(chore.title, 0) + 1
|
completions_by_chore[chore.title] = completions_by_chore.get(chore.title, 0) + 1
|
||||||
|
|
||||||
# Completions by day
|
# Completions by day
|
||||||
|
=======
|
||||||
|
completions_by_chore = {}
|
||||||
|
for log in logs:
|
||||||
|
chore = db.query(Chore).filter(Chore.id == log.chore_id).first()
|
||||||
|
if chore: completions_by_chore[chore.title] = completions_by_chore.get(chore.title, 0) + 1
|
||||||
|
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
completions_by_day = {}
|
completions_by_day = {}
|
||||||
for log in logs:
|
for log in logs:
|
||||||
day_name = log.completed_at.strftime("%A")
|
day_name = log.completed_at.strftime("%A")
|
||||||
completions_by_day[day_name] = completions_by_day.get(day_name, 0) + 1
|
completions_by_day[day_name] = completions_by_day.get(day_name, 0) + 1
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
# Top performers
|
# Top performers
|
||||||
user_stats = []
|
user_stats = []
|
||||||
for user_name, count in completions_by_user.items():
|
for user_name, count in completions_by_user.items():
|
||||||
@@ -360,12 +417,59 @@ def delete_completion_log(
|
|||||||
detail="Not authorized to delete this completion log"
|
detail="Not authorized to delete this completion log"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
=======
|
||||||
|
user_stats = []
|
||||||
|
for user_name, count in completions_by_user.items():
|
||||||
|
user = db.query(User).filter((User.full_name == user_name) | (User.username == user_name)).first()
|
||||||
|
user_stats.append({"username": user_name, "count": count, "avatar_url": user.avatar_url if user else None})
|
||||||
|
user_stats.sort(key=lambda x: x["count"], reverse=True)
|
||||||
|
top_performers = user_stats[:5]
|
||||||
|
|
||||||
|
recent_logs = sorted(logs, key=lambda x: x.completed_at, reverse=True)[:10]
|
||||||
|
recent_completions = [enrich_completion_log(db, log) for log in recent_logs]
|
||||||
|
|
||||||
|
return {"start_date": start_of_week, "end_date": end_of_week, "total_completions": len(logs), "completions_by_user": completions_by_user, "completions_by_chore": completions_by_chore, "completions_by_day": completions_by_day, "top_performers": top_performers, "recent_completions": recent_completions}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/reports/user/{user_id}", response_model=log_schemas.UserChoreStats)
|
||||||
|
def get_user_stats(user_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_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")
|
||||||
|
|
||||||
|
total_completions = db.query(ChoreCompletionLog).filter(ChoreCompletionLog.user_id == user_id).count()
|
||||||
|
|
||||||
|
today = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
|
||||||
|
start_of_week = today - timedelta(days=today.weekday())
|
||||||
|
completions_this_week = db.query(ChoreCompletionLog).filter(and_(ChoreCompletionLog.user_id == user_id, ChoreCompletionLog.completed_at >= start_of_week)).count()
|
||||||
|
|
||||||
|
start_of_month = today.replace(day=1)
|
||||||
|
completions_this_month = db.query(ChoreCompletionLog).filter(and_(ChoreCompletionLog.user_id == user_id, ChoreCompletionLog.completed_at >= start_of_month)).count()
|
||||||
|
|
||||||
|
favorite_chore = None
|
||||||
|
chore_counts = db.query(ChoreCompletionLog.chore_id, func.count(ChoreCompletionLog.id).label('count')).filter(ChoreCompletionLog.user_id == user_id).group_by(ChoreCompletionLog.chore_id).order_by(func.count(ChoreCompletionLog.id).desc()).first()
|
||||||
|
if chore_counts:
|
||||||
|
chore = db.query(Chore).filter(Chore.id == chore_counts[0]).first()
|
||||||
|
if chore: favorite_chore = chore.title
|
||||||
|
|
||||||
|
recent_logs = db.query(ChoreCompletionLog).filter(ChoreCompletionLog.user_id == user_id).order_by(ChoreCompletionLog.completed_at.desc()).limit(10).all()
|
||||||
|
recent_completions = [enrich_completion_log(db, log) for log in recent_logs]
|
||||||
|
|
||||||
|
return {"user_id": user.id, "username": user.username, "full_name": user.full_name, "avatar_url": user.avatar_url, "total_completions": total_completions, "completions_this_week": completions_this_week, "completions_this_month": completions_this_month, "favorite_chore": favorite_chore, "recent_completions": recent_completions}
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/completions/{log_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||||
|
def delete_completion_log(log_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
|
||||||
|
log = db.query(ChoreCompletionLog).filter(ChoreCompletionLog.id == log_id).first()
|
||||||
|
if not log: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Completion log not found")
|
||||||
|
if not current_user.is_admin and log.user_id != current_user.id: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not authorized")
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
db.delete(log)
|
db.delete(log)
|
||||||
db.commit()
|
db.commit()
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@router.post("/completions/{log_id}/verify", response_model=log_schemas.ChoreCompletionLog)
|
@router.post("/completions/{log_id}/verify", response_model=log_schemas.ChoreCompletionLog)
|
||||||
|
<<<<<<< HEAD
|
||||||
def verify_completion(
|
def verify_completion(
|
||||||
log_id: int,
|
log_id: int,
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
@@ -394,4 +498,13 @@ def verify_completion(
|
|||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(log)
|
db.refresh(log)
|
||||||
|
|
||||||
|
=======
|
||||||
|
def verify_completion(log_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
|
||||||
|
log = db.query(ChoreCompletionLog).filter(ChoreCompletionLog.id == log_id).first()
|
||||||
|
if not log: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Completion log not found")
|
||||||
|
if log.user_id == current_user.id: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Cannot verify own completion")
|
||||||
|
log.verified_by_user_id = current_user.id
|
||||||
|
db.commit()
|
||||||
|
db.refresh(log)
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
return enrich_completion_log(db, log)
|
return enrich_completion_log(db, log)
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
<<<<<<< HEAD
|
||||||
"""Application configuration."""
|
"""Application configuration."""
|
||||||
from pydantic_settings import BaseSettings
|
from pydantic_settings import BaseSettings
|
||||||
from typing import List
|
from typing import List
|
||||||
@@ -40,3 +41,6 @@ class Settings(BaseSettings):
|
|||||||
return self.ALLOWED_ORIGINS
|
return self.ALLOWED_ORIGINS
|
||||||
|
|
||||||
settings = Settings()
|
settings = Settings()
|
||||||
|
=======
|
||||||
|
IiIiQXBwbGljYXRpb24gY29uZmlndXJhdGlvbi4iIiIKZnJvbSBweWRhbnRpY19zZXR0aW5ncyBpbXBvcnQgQmFzZVNldHRpbmdzCmZyb20gdHlwaW5nIGltcG9ydCBMaXN0CgoKY2xhc3MgU2V0dGluZ3MoQmFzZVNldHRpbmdzKToKICAgICIiIkFwcGxpY2F0aW9uIHNldHRpbmdzLiIiIgogICAgCiAgICBBUFBfTkFNRTogc3RyID0gIkZhbWlseSBIdWIiCiAgICBBUFBfVkVSU0lPTjogc3RyID0gIjAuMS4wIgogICAgREVCVUc6IGJvb2wgPSBUcnVlCiAgICAKICAgICMgRGF0YWJhc2UKICAgIERBVEFCQVNFX1VSTDogc3RyID0gInNxbGl0ZTovLy8uL2ZhbWlseV9odWIuZGIiCiAgICAKICAgICMgU2VjdXJpdHkKICAgIFNFQ1JFVF9LRVk6IHN0ciA9ICJ5b3VyLXNlY3JldC1rZXktY2hhbmdlLXRoaXMtaW4tcHJvZHVjdGlvbiIKICAgIEFMR09SSVRITTogc3RyID0gIkhTMjU2IgogICAgQUNDRVNTX1RPS0VOX0VYUElSRV9NSU5VVEVTOiBpbnQgPSAzMAogICAgCiAgICAjIEVudmlyb25tZW50CiAgICBFTlZJUk9OTUVOVDogc3RyID0gImRldmVsb3BtZW50IgogICAgCiAgICAjIENPUlMgLSBhY2NlcHRzIGVpdGhlciBjb21tYS1zZXBhcmF0ZWQgc3RyaW5nIG9yIEpTT04gYXJyYXkKICAgIENPUlNfT1JJR0lOUzogc3RyID0gImh0dHA6Ly9sb2NhbGhvc3Q6NTE3MyxodHRwOi8vbG9jYWxob3N0OjMwMDAsaHR0cDovLzEwLjAuMC4xMjc6NTE3MyIKICAgIAogICAgY2xhc3MgQ29uZmlnOgogICAgICAgIGVudl9maWxlID0gIi5lbnYiCiAgICAgICAgY2FzZV9zZW5zaXRpdmUgPSBUcnVlCiAgICAKICAgIEBwcm9wZXJ0eQogICAgZGVmIGNvcnNfb3JpZ2lucyhzZWxmKSAtPiBMaXN0W3N0cl06CiAgICAgICAgIiIiUGFyc2UgQ09SU19PUklHSU5TIGludG8gYSBsaXN0LiIiIgogICAgICAgIGlmIGlzaW5zdGFuY2Uoc2VsZi5DT1JTX09SSUdJTlMsIHN0cik6CiAgICAgICAgICAgIHJldHVybiBbb3JpZ2luLnN0cmlwKCkgZm9yIG9yaWdpbiBpbiBzZWxmLkNPUlNfT1JJR0lOUy5zcGxpdCgnLCcpXQogICAgICAgIHJldHVybiBzZWxmLkNPUlNfT1JJR0lOUwoKCnNldHRpbmdzID0gU2V0dGluZ3MoKQo=
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
"""Main FastAPI application."""
|
"""Main FastAPI application."""
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
<<<<<<< HEAD
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
from app.api.v1 import auth, users, chores, uploads, public, chore_logs
|
from app.api.v1 import auth, users, chores, uploads, public, chore_logs
|
||||||
|
=======
|
||||||
|
from app.core.config import settings
|
||||||
|
from app.api.v1 import auth, users, chores
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
|
|
||||||
# Create FastAPI app
|
# Create FastAPI app
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
@@ -15,12 +20,15 @@ app = FastAPI(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Configure CORS
|
# Configure CORS
|
||||||
|
<<<<<<< HEAD
|
||||||
print("="*70)
|
print("="*70)
|
||||||
print("FAMILY HUB - CORS CONFIGURATION")
|
print("FAMILY HUB - CORS CONFIGURATION")
|
||||||
print("="*70)
|
print("="*70)
|
||||||
print(f"Allowed Origins: {settings.cors_origins}")
|
print(f"Allowed Origins: {settings.cors_origins}")
|
||||||
print("="*70)
|
print("="*70)
|
||||||
|
|
||||||
|
=======
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
CORSMiddleware,
|
CORSMiddleware,
|
||||||
allow_origins=settings.cors_origins,
|
allow_origins=settings.cors_origins,
|
||||||
@@ -29,18 +37,24 @@ app.add_middleware(
|
|||||||
allow_headers=["*"],
|
allow_headers=["*"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
# Mount static files for uploads
|
# Mount static files for uploads
|
||||||
static_path = Path(__file__).parent / "static"
|
static_path = Path(__file__).parent / "static"
|
||||||
static_path.mkdir(exist_ok=True)
|
static_path.mkdir(exist_ok=True)
|
||||||
app.mount("/static", StaticFiles(directory=str(static_path)), name="static")
|
app.mount("/static", StaticFiles(directory=str(static_path)), name="static")
|
||||||
|
|
||||||
|
=======
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
# Include routers
|
# Include routers
|
||||||
app.include_router(auth.router, prefix="/api/v1/auth", tags=["authentication"])
|
app.include_router(auth.router, prefix="/api/v1/auth", tags=["authentication"])
|
||||||
app.include_router(users.router, prefix="/api/v1/users", tags=["users"])
|
app.include_router(users.router, prefix="/api/v1/users", tags=["users"])
|
||||||
app.include_router(chores.router, prefix="/api/v1/chores", tags=["chores"])
|
app.include_router(chores.router, prefix="/api/v1/chores", tags=["chores"])
|
||||||
|
<<<<<<< HEAD
|
||||||
app.include_router(chore_logs.router, prefix="/api/v1/chores", tags=["chore-logs"])
|
app.include_router(chore_logs.router, prefix="/api/v1/chores", tags=["chore-logs"])
|
||||||
app.include_router(uploads.router, prefix="/api/v1/uploads", tags=["uploads"])
|
app.include_router(uploads.router, prefix="/api/v1/uploads", tags=["uploads"])
|
||||||
app.include_router(public.router, prefix="/api/v1/public", tags=["public"])
|
app.include_router(public.router, prefix="/api/v1/public", tags=["public"])
|
||||||
|
=======
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
|
|
||||||
@app.get("/")
|
@app.get("/")
|
||||||
async def root():
|
async def root():
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
# Models package
|
# Models package
|
||||||
from app.models.user import User
|
from app.models.user import User
|
||||||
from app.models.chore import Chore
|
from app.models.chore import Chore
|
||||||
|
<<<<<<< HEAD
|
||||||
from app.models.chore_assignment import ChoreAssignment
|
from app.models.chore_assignment import ChoreAssignment
|
||||||
from app.models.chore_completion_log import ChoreCompletionLog
|
from app.models.chore_completion_log import ChoreCompletionLog
|
||||||
|
|
||||||
__all__ = ["User", "Chore", "ChoreAssignment", "ChoreCompletionLog"]
|
__all__ = ["User", "Chore", "ChoreAssignment", "ChoreCompletionLog"]
|
||||||
|
=======
|
||||||
|
|
||||||
|
__all__ = ["User", "Chore"]
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
|
|||||||
@@ -33,11 +33,19 @@ class Chore(Base):
|
|||||||
title = Column(String(200), nullable=False)
|
title = Column(String(200), nullable=False)
|
||||||
description = Column(String(500))
|
description = Column(String(500))
|
||||||
room = Column(String(50)) # bedroom1, bedroom2, kitchen, bathroom1, etc.
|
room = Column(String(50)) # bedroom1, bedroom2, kitchen, bathroom1, etc.
|
||||||
|
<<<<<<< HEAD
|
||||||
frequency = Column(SQLEnum(ChoreFrequency, values_callable=lambda x: [e.value for e in x]), nullable=False)
|
frequency = Column(SQLEnum(ChoreFrequency, values_callable=lambda x: [e.value for e in x]), nullable=False)
|
||||||
points = Column(Integer, default=0) # Points awarded for completing the chore
|
points = Column(Integer, default=0) # Points awarded for completing the chore
|
||||||
image_url = Column(String(500)) # URL to chore image
|
image_url = Column(String(500)) # URL to chore image
|
||||||
assignment_type = Column(SQLEnum(ChoreAssignmentType, values_callable=lambda x: [e.value for e in x]), default=ChoreAssignmentType.ANY_ONE) # How chore should be completed
|
assignment_type = Column(SQLEnum(ChoreAssignmentType, values_callable=lambda x: [e.value for e in x]), default=ChoreAssignmentType.ANY_ONE) # How chore should be completed
|
||||||
status = Column(SQLEnum(ChoreStatus, values_callable=lambda x: [e.value for e in x]), default=ChoreStatus.PENDING)
|
status = Column(SQLEnum(ChoreStatus, values_callable=lambda x: [e.value for e in x]), default=ChoreStatus.PENDING)
|
||||||
|
=======
|
||||||
|
frequency = Column(SQLEnum(ChoreFrequency), nullable=False)
|
||||||
|
points = Column(Integer, default=0) # Points awarded for completing the chore
|
||||||
|
image_url = Column(String(500)) # URL to chore image
|
||||||
|
assignment_type = Column(SQLEnum(ChoreAssignmentType), default=ChoreAssignmentType.ANY_ONE) # How chore should be completed
|
||||||
|
status = Column(SQLEnum(ChoreStatus), default=ChoreStatus.PENDING)
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
assigned_user_id = Column(Integer, ForeignKey("users.id")) # Deprecated - use assignments instead
|
assigned_user_id = Column(Integer, ForeignKey("users.id")) # Deprecated - use assignments instead
|
||||||
due_date = Column(DateTime)
|
due_date = Column(DateTime)
|
||||||
completed_at = Column(DateTime)
|
completed_at = Column(DateTime)
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
"""User model."""
|
"""User model."""
|
||||||
|
<<<<<<< HEAD
|
||||||
from sqlalchemy import Boolean, Column, Integer, String, DateTime, Date
|
from sqlalchemy import Boolean, Column, Integer, String, DateTime, Date
|
||||||
|
=======
|
||||||
|
from sqlalchemy import Boolean, Column, Integer, String, DateTime
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
from sqlalchemy.orm import relationship
|
from sqlalchemy.orm import relationship
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from app.core.database import Base
|
from app.core.database import Base
|
||||||
@@ -15,8 +19,11 @@ class User(Base):
|
|||||||
hashed_password = Column(String(200), nullable=False)
|
hashed_password = Column(String(200), nullable=False)
|
||||||
discord_id = Column(String(100)) # For Discord integration
|
discord_id = Column(String(100)) # For Discord integration
|
||||||
profile_picture = Column(String(500)) # URL to profile picture
|
profile_picture = Column(String(500)) # URL to profile picture
|
||||||
|
<<<<<<< HEAD
|
||||||
avatar_url = Column(String(500)) # URL to uploaded avatar
|
avatar_url = Column(String(500)) # URL to uploaded avatar
|
||||||
birthday = Column(Date, nullable=True) # Birthday for chore logic
|
birthday = Column(Date, nullable=True) # Birthday for chore logic
|
||||||
|
=======
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
is_active = Column(Boolean, default=True)
|
is_active = Column(Boolean, default=True)
|
||||||
is_admin = Column(Boolean, default=False)
|
is_admin = Column(Boolean, default=False)
|
||||||
created_at = Column(DateTime, default=datetime.utcnow)
|
created_at = Column(DateTime, default=datetime.utcnow)
|
||||||
@@ -24,5 +31,8 @@ class User(Base):
|
|||||||
|
|
||||||
# Relationships (lazy loaded to avoid circular imports)
|
# Relationships (lazy loaded to avoid circular imports)
|
||||||
chores = relationship("Chore", back_populates="assigned_user", lazy="select")
|
chores = relationship("Chore", back_populates="assigned_user", lazy="select")
|
||||||
|
<<<<<<< HEAD
|
||||||
chore_assignments = relationship("ChoreAssignment", back_populates="user", lazy="select")
|
chore_assignments = relationship("ChoreAssignment", back_populates="user", lazy="select")
|
||||||
chore_completion_logs = relationship("ChoreCompletionLog", foreign_keys="[ChoreCompletionLog.user_id]", back_populates="user", lazy="select")
|
chore_completion_logs = relationship("ChoreCompletionLog", foreign_keys="[ChoreCompletionLog.user_id]", back_populates="user", lazy="select")
|
||||||
|
=======
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
# Schemas package
|
# Schemas package
|
||||||
|
<<<<<<< HEAD
|
||||||
from app.schemas import auth, chore, user, chore_completion_log
|
from app.schemas import auth, chore, user, chore_completion_log
|
||||||
|
|
||||||
__all__ = ["auth", "chore", "user", "chore_completion_log"]
|
__all__ = ["auth", "chore", "user", "chore_completion_log"]
|
||||||
|
=======
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
|
|||||||
@@ -1,9 +1,16 @@
|
|||||||
"""Chore schemas."""
|
"""Chore schemas."""
|
||||||
from pydantic import BaseModel, ConfigDict, field_validator
|
from pydantic import BaseModel, ConfigDict, field_validator
|
||||||
|
<<<<<<< HEAD
|
||||||
from typing import Optional, Union, List
|
from typing import Optional, Union, List
|
||||||
from datetime import datetime, date
|
from datetime import datetime, date
|
||||||
|
|
||||||
from app.models.chore import ChoreFrequency, ChoreStatus, ChoreAssignmentType
|
from app.models.chore import ChoreFrequency, ChoreStatus, ChoreAssignmentType
|
||||||
|
=======
|
||||||
|
from typing import Optional, Union
|
||||||
|
from datetime import datetime, date
|
||||||
|
|
||||||
|
from app.models.chore import ChoreFrequency, ChoreStatus
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
|
|
||||||
|
|
||||||
class ChoreBase(BaseModel):
|
class ChoreBase(BaseModel):
|
||||||
@@ -12,17 +19,26 @@ class ChoreBase(BaseModel):
|
|||||||
description: Optional[str] = None
|
description: Optional[str] = None
|
||||||
room: str
|
room: str
|
||||||
frequency: ChoreFrequency
|
frequency: ChoreFrequency
|
||||||
|
<<<<<<< HEAD
|
||||||
points: Optional[int] = 0
|
points: Optional[int] = 0
|
||||||
image_url: Optional[str] = None
|
image_url: Optional[str] = None
|
||||||
assignment_type: Optional[ChoreAssignmentType] = ChoreAssignmentType.ANY_ONE
|
assignment_type: Optional[ChoreAssignmentType] = ChoreAssignmentType.ANY_ONE
|
||||||
|
=======
|
||||||
|
assigned_user_id: Optional[int] = None
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
due_date: Optional[Union[datetime, date, str]] = None
|
due_date: Optional[Union[datetime, date, str]] = None
|
||||||
|
|
||||||
@field_validator('due_date', mode='before')
|
@field_validator('due_date', mode='before')
|
||||||
@classmethod
|
@classmethod
|
||||||
def parse_due_date(cls, v):
|
def parse_due_date(cls, v):
|
||||||
"""Parse due_date to handle various formats."""
|
"""Parse due_date to handle various formats."""
|
||||||
|
<<<<<<< HEAD
|
||||||
if v is None or v == '' or isinstance(v, (datetime, date)):
|
if v is None or v == '' or isinstance(v, (datetime, date)):
|
||||||
return None if v == '' else v
|
return None if v == '' else v
|
||||||
|
=======
|
||||||
|
if v is None or isinstance(v, (datetime, date)):
|
||||||
|
return v
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
if isinstance(v, str):
|
if isinstance(v, str):
|
||||||
# Try parsing as datetime first
|
# Try parsing as datetime first
|
||||||
for fmt in ['%Y-%m-%dT%H:%M:%S', '%Y-%m-%d']:
|
for fmt in ['%Y-%m-%dT%H:%M:%S', '%Y-%m-%d']:
|
||||||
@@ -30,14 +46,22 @@ class ChoreBase(BaseModel):
|
|||||||
return datetime.strptime(v, fmt)
|
return datetime.strptime(v, fmt)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
continue
|
continue
|
||||||
|
<<<<<<< HEAD
|
||||||
# If no format matches, return None instead of the invalid string
|
# If no format matches, return None instead of the invalid string
|
||||||
return None
|
return None
|
||||||
return None
|
return None
|
||||||
|
=======
|
||||||
|
return v
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
|
|
||||||
|
|
||||||
class ChoreCreate(ChoreBase):
|
class ChoreCreate(ChoreBase):
|
||||||
"""Schema for creating a chore."""
|
"""Schema for creating a chore."""
|
||||||
|
<<<<<<< HEAD
|
||||||
assigned_user_ids: Optional[List[int]] = [] # Multiple users can be assigned
|
assigned_user_ids: Optional[List[int]] = [] # Multiple users can be assigned
|
||||||
|
=======
|
||||||
|
pass
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
|
|
||||||
|
|
||||||
class ChoreUpdate(BaseModel):
|
class ChoreUpdate(BaseModel):
|
||||||
@@ -46,18 +70,28 @@ class ChoreUpdate(BaseModel):
|
|||||||
description: Optional[str] = None
|
description: Optional[str] = None
|
||||||
room: Optional[str] = None
|
room: Optional[str] = None
|
||||||
frequency: Optional[ChoreFrequency] = None
|
frequency: Optional[ChoreFrequency] = None
|
||||||
|
<<<<<<< HEAD
|
||||||
points: Optional[int] = None
|
points: Optional[int] = None
|
||||||
status: Optional[ChoreStatus] = None
|
status: Optional[ChoreStatus] = None
|
||||||
assignment_type: Optional[ChoreAssignmentType] = None
|
assignment_type: Optional[ChoreAssignmentType] = None
|
||||||
assigned_user_ids: Optional[List[int]] = None # Multiple users
|
assigned_user_ids: Optional[List[int]] = None # Multiple users
|
||||||
|
=======
|
||||||
|
status: Optional[ChoreStatus] = None
|
||||||
|
assigned_user_id: Optional[int] = None
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
due_date: Optional[Union[datetime, date, str]] = None
|
due_date: Optional[Union[datetime, date, str]] = None
|
||||||
|
|
||||||
@field_validator('due_date', mode='before')
|
@field_validator('due_date', mode='before')
|
||||||
@classmethod
|
@classmethod
|
||||||
def parse_due_date(cls, v):
|
def parse_due_date(cls, v):
|
||||||
"""Parse due_date to handle various formats."""
|
"""Parse due_date to handle various formats."""
|
||||||
|
<<<<<<< HEAD
|
||||||
if v is None or v == '' or isinstance(v, (datetime, date)):
|
if v is None or v == '' or isinstance(v, (datetime, date)):
|
||||||
return None if v == '' else v
|
return None if v == '' else v
|
||||||
|
=======
|
||||||
|
if v is None or isinstance(v, (datetime, date)):
|
||||||
|
return v
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
if isinstance(v, str):
|
if isinstance(v, str):
|
||||||
# Try parsing as datetime first
|
# Try parsing as datetime first
|
||||||
for fmt in ['%Y-%m-%dT%H:%M:%S', '%Y-%m-%d']:
|
for fmt in ['%Y-%m-%dT%H:%M:%S', '%Y-%m-%d']:
|
||||||
@@ -65,6 +99,7 @@ class ChoreUpdate(BaseModel):
|
|||||||
return datetime.strptime(v, fmt)
|
return datetime.strptime(v, fmt)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
continue
|
continue
|
||||||
|
<<<<<<< HEAD
|
||||||
# If no format matches, return None instead of the invalid string
|
# If no format matches, return None instead of the invalid string
|
||||||
return None
|
return None
|
||||||
return None
|
return None
|
||||||
@@ -72,14 +107,24 @@ class ChoreUpdate(BaseModel):
|
|||||||
|
|
||||||
class AssignedUserDetail(BaseModel):
|
class AssignedUserDetail(BaseModel):
|
||||||
"""User info for chore assignment."""
|
"""User info for chore assignment."""
|
||||||
|
=======
|
||||||
|
return v
|
||||||
|
|
||||||
|
|
||||||
|
class AssignedUser(BaseModel):
|
||||||
|
"""Minimal user info for chore assignment."""
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
model_config = ConfigDict(from_attributes=True)
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
id: int
|
id: int
|
||||||
username: str
|
username: str
|
||||||
full_name: str
|
full_name: str
|
||||||
|
<<<<<<< HEAD
|
||||||
avatar_url: Optional[str] = None
|
avatar_url: Optional[str] = None
|
||||||
birthday: Optional[date] = None
|
birthday: Optional[date] = None
|
||||||
completed_at: Optional[datetime] = None # When this user completed the chore
|
completed_at: Optional[datetime] = None # When this user completed the chore
|
||||||
|
=======
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
|
|
||||||
|
|
||||||
class Chore(ChoreBase):
|
class Chore(ChoreBase):
|
||||||
@@ -88,6 +133,7 @@ class Chore(ChoreBase):
|
|||||||
|
|
||||||
id: int
|
id: int
|
||||||
status: ChoreStatus
|
status: ChoreStatus
|
||||||
|
<<<<<<< HEAD
|
||||||
points: int
|
points: int
|
||||||
assignment_type: ChoreAssignmentType
|
assignment_type: ChoreAssignmentType
|
||||||
assigned_users: List[AssignedUserDetail] = [] # Multiple users with completion status
|
assigned_users: List[AssignedUserDetail] = [] # Multiple users with completion status
|
||||||
@@ -97,3 +143,9 @@ class Chore(ChoreBase):
|
|||||||
|
|
||||||
# Legacy field for backward compatibility
|
# Legacy field for backward compatibility
|
||||||
assigned_user_id: Optional[int] = None
|
assigned_user_id: Optional[int] = None
|
||||||
|
=======
|
||||||
|
assigned_user: Optional[AssignedUser] = None
|
||||||
|
completed_at: Optional[datetime] = None
|
||||||
|
created_at: datetime
|
||||||
|
updated_at: datetime
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,3 +1,4 @@
|
|||||||
|
<<<<<<< HEAD
|
||||||
fastapi==0.115.0
|
fastapi==0.115.0
|
||||||
uvicorn[standard]==0.32.0
|
uvicorn[standard]==0.32.0
|
||||||
sqlalchemy==2.0.36
|
sqlalchemy==2.0.36
|
||||||
@@ -9,3 +10,6 @@ pydantic==2.10.3
|
|||||||
pydantic-settings==2.6.1
|
pydantic-settings==2.6.1
|
||||||
python-dotenv==1.0.1
|
python-dotenv==1.0.1
|
||||||
email-validator==2.2.0
|
email-validator==2.2.0
|
||||||
|
=======
|
||||||
|
ZmFzdGFwaT09MC4xMTUuMAp1dmljb3JuW3N0YW5kYXJkXT09MC4zMi4wCnNxbGFsY2hlbXk9PTIuMC4zNgpweXRob24tam9zZVtjcnlwdG9ncmFwaHldPT0zLjMuMApiY3J5cHQ9PTQuMi4wCnBhc3NsaWJbYmNyeXB0XT09MS43LjQKcHl0aG9uLW11bHRpcGFydD09MC4wLjEyCnB5ZGFudGljPT0yLjEwLjMKcHlkYW50aWMtc2V0dGluZ3M9PTIuNi4xCnB5dGhvbi1kb3RlbnY9PTEuMC4xCmVtYWlsLXZhbGlkYXRvcj09Mi4yLjAK
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
|
|||||||
1
frontend/.env
Normal file
1
frontend/.env
Normal file
@@ -0,0 +1 @@
|
|||||||
|
VITE_API_URL=http://10.0.0.127:8001
|
||||||
@@ -9,11 +9,18 @@
|
|||||||
"lint": "eslint . --ext js,jsx,ts,tsx"
|
"lint": "eslint . --ext js,jsx,ts,tsx"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
<<<<<<< HEAD
|
||||||
"@heroicons/react": "^2.2.0",
|
"@heroicons/react": "^2.2.0",
|
||||||
"axios": "^1.6.2",
|
"axios": "^1.6.2",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-router-dom": "^6.20.0"
|
"react-router-dom": "^6.20.0"
|
||||||
|
=======
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"react-router-dom": "^6.20.0",
|
||||||
|
"axios": "^1.6.2"
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^18.2.43",
|
"@types/react": "^18.2.43",
|
||||||
|
|||||||
@@ -3,9 +3,12 @@ import { AuthProvider, useAuth } from './contexts/AuthContext';
|
|||||||
import Login from './pages/Login';
|
import Login from './pages/Login';
|
||||||
import Dashboard from './pages/Dashboard';
|
import Dashboard from './pages/Dashboard';
|
||||||
import Settings from './pages/Settings';
|
import Settings from './pages/Settings';
|
||||||
|
<<<<<<< HEAD
|
||||||
import KioskView from './pages/KioskView';
|
import KioskView from './pages/KioskView';
|
||||||
import Reports from './pages/Reports';
|
import Reports from './pages/Reports';
|
||||||
import UserStatsPage from './pages/UserStatsPage';
|
import UserStatsPage from './pages/UserStatsPage';
|
||||||
|
=======
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
|
|
||||||
// Protected route wrapper
|
// Protected route wrapper
|
||||||
function ProtectedRoute({ children }: { children: React.ReactNode }) {
|
function ProtectedRoute({ children }: { children: React.ReactNode }) {
|
||||||
@@ -56,9 +59,12 @@ function App() {
|
|||||||
<Router>
|
<Router>
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<Routes>
|
<Routes>
|
||||||
|
<<<<<<< HEAD
|
||||||
{/* Public Kiosk View - No Auth Required */}
|
{/* Public Kiosk View - No Auth Required */}
|
||||||
<Route path="/kiosk" element={<KioskView />} />
|
<Route path="/kiosk" element={<KioskView />} />
|
||||||
|
|
||||||
|
=======
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
{/* Public routes */}
|
{/* Public routes */}
|
||||||
<Route
|
<Route
|
||||||
path="/login"
|
path="/login"
|
||||||
@@ -88,6 +94,7 @@ function App() {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
<Route
|
<Route
|
||||||
path="/reports"
|
path="/reports"
|
||||||
element={
|
element={
|
||||||
@@ -106,6 +113,8 @@ function App() {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
=======
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
{/* Default route */}
|
{/* Default route */}
|
||||||
<Route path="/" element={<Navigate to="/dashboard" replace />} />
|
<Route path="/" element={<Navigate to="/dashboard" replace />} />
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
export const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:8001';
|
export const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:8001';
|
||||||
|
=======
|
||||||
|
const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:8001';
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
|
|
||||||
const api = axios.create({
|
const api = axios.create({
|
||||||
baseURL: API_BASE_URL,
|
baseURL: API_BASE_URL,
|
||||||
|
|||||||
@@ -4,7 +4,10 @@ export interface AssignedUser {
|
|||||||
id: number;
|
id: number;
|
||||||
username: string;
|
username: string;
|
||||||
full_name: string;
|
full_name: string;
|
||||||
|
<<<<<<< HEAD
|
||||||
avatar_url?: string;
|
avatar_url?: string;
|
||||||
|
=======
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
birthday?: string;
|
birthday?: string;
|
||||||
completed_at?: string;
|
completed_at?: string;
|
||||||
}
|
}
|
||||||
@@ -16,8 +19,11 @@ export interface Chore {
|
|||||||
room: string;
|
room: string;
|
||||||
frequency: 'daily' | 'weekly' | 'fortnightly' | 'monthly' | 'on_trigger';
|
frequency: 'daily' | 'weekly' | 'fortnightly' | 'monthly' | 'on_trigger';
|
||||||
points: number;
|
points: number;
|
||||||
|
<<<<<<< HEAD
|
||||||
image_url?: string;
|
image_url?: string;
|
||||||
assignment_type: 'any_one' | 'all_assigned';
|
assignment_type: 'any_one' | 'all_assigned';
|
||||||
|
=======
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
status: 'pending' | 'in_progress' | 'completed' | 'skipped';
|
status: 'pending' | 'in_progress' | 'completed' | 'skipped';
|
||||||
assigned_users: AssignedUser[]; // Multiple users
|
assigned_users: AssignedUser[]; // Multiple users
|
||||||
assigned_user_id?: number; // Legacy field
|
assigned_user_id?: number; // Legacy field
|
||||||
@@ -38,7 +44,10 @@ export interface CreateChoreRequest {
|
|||||||
room: string;
|
room: string;
|
||||||
frequency: 'daily' | 'weekly' | 'fortnightly' | 'monthly' | 'on_trigger';
|
frequency: 'daily' | 'weekly' | 'fortnightly' | 'monthly' | 'on_trigger';
|
||||||
points?: number;
|
points?: number;
|
||||||
|
<<<<<<< HEAD
|
||||||
assignment_type?: 'any_one' | 'all_assigned';
|
assignment_type?: 'any_one' | 'all_assigned';
|
||||||
|
=======
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
assigned_user_ids?: number[]; // Multiple users
|
assigned_user_ids?: number[]; // Multiple users
|
||||||
due_date?: string;
|
due_date?: string;
|
||||||
}
|
}
|
||||||
@@ -49,7 +58,10 @@ export interface UpdateChoreRequest {
|
|||||||
room?: string;
|
room?: string;
|
||||||
frequency?: 'daily' | 'weekly' | 'fortnightly' | 'monthly' | 'on_trigger';
|
frequency?: 'daily' | 'weekly' | 'fortnightly' | 'monthly' | 'on_trigger';
|
||||||
points?: number;
|
points?: number;
|
||||||
|
<<<<<<< HEAD
|
||||||
assignment_type?: 'any_one' | 'all_assigned';
|
assignment_type?: 'any_one' | 'all_assigned';
|
||||||
|
=======
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
status?: 'pending' | 'in_progress' | 'completed' | 'skipped';
|
status?: 'pending' | 'in_progress' | 'completed' | 'skipped';
|
||||||
assigned_user_ids?: number[]; // Multiple users
|
assigned_user_ids?: number[]; // Multiple users
|
||||||
due_date?: string;
|
due_date?: string;
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Chore } from '../api/chores';
|
import { Chore } from '../api/chores';
|
||||||
import { useAuth } from '../contexts/AuthContext';
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
|
<<<<<<< HEAD
|
||||||
import { getUserColor, getInitials } from '../utils/avatarUtils';
|
import { getUserColor, getInitials } from '../utils/avatarUtils';
|
||||||
import { API_BASE_URL } from '../api/axios';
|
import { API_BASE_URL } from '../api/axios';
|
||||||
|
=======
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
|
|
||||||
interface ChoreCardProps {
|
interface ChoreCardProps {
|
||||||
chore: Chore;
|
chore: Chore;
|
||||||
@@ -61,6 +64,7 @@ const ChoreCard: React.FC<ChoreCardProps> = ({ chore, onComplete, onDelete, onEd
|
|||||||
<p className="text-sm text-gray-600 mb-3">{chore.description}</p>
|
<p className="text-sm text-gray-600 mb-3">{chore.description}</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
{/* Chore Image */}
|
{/* Chore Image */}
|
||||||
{chore.image_url && (
|
{chore.image_url && (
|
||||||
<div className="mb-3">
|
<div className="mb-3">
|
||||||
@@ -72,6 +76,8 @@ const ChoreCard: React.FC<ChoreCardProps> = ({ chore, onComplete, onDelete, onEd
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
=======
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
<div className="space-y-2 mb-4">
|
<div className="space-y-2 mb-4">
|
||||||
<div className="flex items-center text-sm text-gray-500">
|
<div className="flex items-center text-sm text-gray-500">
|
||||||
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
@@ -103,6 +109,7 @@ const ChoreCard: React.FC<ChoreCardProps> = ({ chore, onComplete, onDelete, onEd
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={assignedUser.id} className="flex items-center justify-between text-sm">
|
<div key={assignedUser.id} className="flex items-center justify-between text-sm">
|
||||||
|
<<<<<<< HEAD
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{/* User Avatar */}
|
{/* User Avatar */}
|
||||||
{assignedUser.avatar_url ? (
|
{assignedUser.avatar_url ? (
|
||||||
@@ -121,6 +128,12 @@ const ChoreCard: React.FC<ChoreCardProps> = ({ chore, onComplete, onDelete, onEd
|
|||||||
{isBirthday && ' 🎂'}
|
{isBirthday && ' 🎂'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
=======
|
||||||
|
<span className={`${assignedUser.id === user?.id ? 'font-medium text-blue-600' : 'text-gray-600'}`}>
|
||||||
|
{assignedUser.full_name}
|
||||||
|
{isBirthday && ' 🎂'}
|
||||||
|
</span>
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
{assignedUser.completed_at && (
|
{assignedUser.completed_at && (
|
||||||
<span className="text-xs text-green-600">✓ Done</span>
|
<span className="text-xs text-green-600">✓ Done</span>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { choreService, CreateChoreRequest } from '../api/chores';
|
import { choreService, CreateChoreRequest } from '../api/chores';
|
||||||
import api from '../api/axios';
|
import api from '../api/axios';
|
||||||
|
<<<<<<< HEAD
|
||||||
|
|
||||||
interface User {
|
interface User {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -8,6 +9,9 @@ interface User {
|
|||||||
full_name: string;
|
full_name: string;
|
||||||
is_active: boolean;
|
is_active: boolean;
|
||||||
}
|
}
|
||||||
|
=======
|
||||||
|
import { User } from '../api/auth';
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
|
|
||||||
interface CreateChoreModalProps {
|
interface CreateChoreModalProps {
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
@@ -20,8 +24,12 @@ const CreateChoreModal: React.FC<CreateChoreModalProps> = ({ onClose, onSuccess
|
|||||||
description: '',
|
description: '',
|
||||||
room: '',
|
room: '',
|
||||||
frequency: 'daily',
|
frequency: 'daily',
|
||||||
|
<<<<<<< HEAD
|
||||||
points: 5,
|
points: 5,
|
||||||
assigned_user_ids: [],
|
assigned_user_ids: [],
|
||||||
|
=======
|
||||||
|
assigned_to: undefined,
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
due_date: '',
|
due_date: '',
|
||||||
});
|
});
|
||||||
const [users, setUsers] = useState<User[]>([]);
|
const [users, setUsers] = useState<User[]>([]);
|
||||||
@@ -35,7 +43,11 @@ const CreateChoreModal: React.FC<CreateChoreModalProps> = ({ onClose, onSuccess
|
|||||||
const loadUsers = async () => {
|
const loadUsers = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await api.get<User[]>('/api/v1/users');
|
const response = await api.get<User[]>('/api/v1/users');
|
||||||
|
<<<<<<< HEAD
|
||||||
setUsers(response.data.filter(u => u.is_active));
|
setUsers(response.data.filter(u => u.is_active));
|
||||||
|
=======
|
||||||
|
setUsers(response.data);
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load users:', error);
|
console.error('Failed to load users:', error);
|
||||||
}
|
}
|
||||||
@@ -47,18 +59,35 @@ const CreateChoreModal: React.FC<CreateChoreModalProps> = ({ onClose, onSuccess
|
|||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
<<<<<<< HEAD
|
||||||
const submitData = { ...formData };
|
const submitData = { ...formData };
|
||||||
if (submitData.due_date) {
|
if (submitData.due_date) {
|
||||||
|
=======
|
||||||
|
// Convert date string to datetime if provided
|
||||||
|
const submitData = { ...formData };
|
||||||
|
if (submitData.due_date) {
|
||||||
|
// Convert YYYY-MM-DD to YYYY-MM-DDTHH:MM:SS format
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
submitData.due_date = `${submitData.due_date}T23:59:59`;
|
submitData.due_date = `${submitData.due_date}T23:59:59`;
|
||||||
}
|
}
|
||||||
|
|
||||||
await choreService.createChore(submitData);
|
await choreService.createChore(submitData);
|
||||||
onSuccess();
|
onSuccess();
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
<<<<<<< HEAD
|
||||||
let errorMessage = 'Failed to create chore';
|
let errorMessage = 'Failed to create chore';
|
||||||
|
|
||||||
if (err.response?.data) {
|
if (err.response?.data) {
|
||||||
const errorData = err.response.data;
|
const errorData = err.response.data;
|
||||||
|
=======
|
||||||
|
// Handle different error response formats
|
||||||
|
let errorMessage = 'Failed to create task';
|
||||||
|
|
||||||
|
if (err.response?.data) {
|
||||||
|
const errorData = err.response.data;
|
||||||
|
|
||||||
|
// Check if it's a validation error array
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
if (Array.isArray(errorData.detail)) {
|
if (Array.isArray(errorData.detail)) {
|
||||||
errorMessage = errorData.detail
|
errorMessage = errorData.detail
|
||||||
.map((e: any) => `${e.loc?.join('.')}: ${e.msg}`)
|
.map((e: any) => `${e.loc?.join('.')}: ${e.msg}`)
|
||||||
@@ -80,6 +109,7 @@ const CreateChoreModal: React.FC<CreateChoreModalProps> = ({ onClose, onSuccess
|
|||||||
const { name, value } = e.target;
|
const { name, value } = e.target;
|
||||||
setFormData(prev => ({
|
setFormData(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
|
<<<<<<< HEAD
|
||||||
[name]: name === 'points' ? parseInt(value) || 0 : value,
|
[name]: name === 'points' ? parseInt(value) || 0 : value,
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
@@ -104,6 +134,18 @@ const CreateChoreModal: React.FC<CreateChoreModalProps> = ({ onClose, onSuccess
|
|||||||
<div className="p-6">
|
<div className="p-6">
|
||||||
<div className="flex justify-between items-center mb-6">
|
<div className="flex justify-between items-center mb-6">
|
||||||
<h2 className="text-2xl font-bold text-gray-900">Create New Chore</h2>
|
<h2 className="text-2xl font-bold text-gray-900">Create New Chore</h2>
|
||||||
|
=======
|
||||||
|
[name]: name === 'assigned_to' ? (value ? parseInt(value) : undefined) : value,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
|
||||||
|
<div className="bg-white rounded-lg shadow-xl max-w-md w-full max-h-[90vh] overflow-y-auto">
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="flex justify-between items-center mb-6">
|
||||||
|
<h2 className="text-2xl font-bold text-gray-900">Create New Task</h2>
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
<button
|
<button
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="text-gray-400 hover:text-gray-600 transition-colors"
|
className="text-gray-400 hover:text-gray-600 transition-colors"
|
||||||
@@ -121,6 +163,7 @@ const CreateChoreModal: React.FC<CreateChoreModalProps> = ({ onClose, onSuccess
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div className="md:col-span-2">
|
<div className="md:col-span-2">
|
||||||
<label htmlFor="title" className="block text-sm font-medium text-gray-700 mb-2">
|
<label htmlFor="title" className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
@@ -269,6 +312,107 @@ const CreateChoreModal: React.FC<CreateChoreModalProps> = ({ onClose, onSuccess
|
|||||||
{formData.assigned_user_ids.length} user(s) selected
|
{formData.assigned_user_ids.length} user(s) selected
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
=======
|
||||||
|
<div>
|
||||||
|
<label htmlFor="title" className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Task Title *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="title"
|
||||||
|
name="title"
|
||||||
|
type="text"
|
||||||
|
value={formData.title}
|
||||||
|
onChange={handleChange}
|
||||||
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-gray-900"
|
||||||
|
placeholder="e.g., Vacuum living room"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label htmlFor="description" className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Description
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
id="description"
|
||||||
|
name="description"
|
||||||
|
value={formData.description}
|
||||||
|
onChange={handleChange}
|
||||||
|
rows={3}
|
||||||
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-gray-900"
|
||||||
|
placeholder="Additional details..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label htmlFor="room" className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Room/Area *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="room"
|
||||||
|
name="room"
|
||||||
|
type="text"
|
||||||
|
value={formData.room}
|
||||||
|
onChange={handleChange}
|
||||||
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-gray-900"
|
||||||
|
placeholder="e.g., Living Room, Kitchen"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label htmlFor="frequency" className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Frequency *
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
id="frequency"
|
||||||
|
name="frequency"
|
||||||
|
value={formData.frequency}
|
||||||
|
onChange={handleChange}
|
||||||
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-gray-900"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<option value="on_trigger">On Trigger</option>
|
||||||
|
<option value="daily">Daily</option>
|
||||||
|
<option value="weekly">Weekly</option>
|
||||||
|
<option value="fortnightly">Fortnightly</option>
|
||||||
|
<option value="monthly">Monthly</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label htmlFor="assigned_to" className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Assign To
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
id="assigned_to"
|
||||||
|
name="assigned_to"
|
||||||
|
value={formData.assigned_to || ''}
|
||||||
|
onChange={handleChange}
|
||||||
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-gray-900"
|
||||||
|
>
|
||||||
|
<option value="">Unassigned</option>
|
||||||
|
{users.map(user => (
|
||||||
|
<option key={user.id} value={user.id}>
|
||||||
|
{user.full_name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label htmlFor="due_date" className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Due Date
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="due_date"
|
||||||
|
name="due_date"
|
||||||
|
type="date"
|
||||||
|
value={formData.due_date}
|
||||||
|
onChange={handleChange}
|
||||||
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-gray-900"
|
||||||
|
/>
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-3 pt-4">
|
<div className="flex gap-3 pt-4">
|
||||||
@@ -284,7 +428,11 @@ const CreateChoreModal: React.FC<CreateChoreModalProps> = ({ onClose, onSuccess
|
|||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
className="flex-1 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
className="flex-1 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
>
|
>
|
||||||
|
<<<<<<< HEAD
|
||||||
{isLoading ? 'Creating...' : 'Create Chore'}
|
{isLoading ? 'Creating...' : 'Create Chore'}
|
||||||
|
=======
|
||||||
|
{isLoading ? 'Creating...' : 'Create Task'}
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { choreService, Chore, UpdateChoreRequest } from '../api/chores';
|
import { choreService, Chore, UpdateChoreRequest } from '../api/chores';
|
||||||
import api from '../api/axios';
|
import api from '../api/axios';
|
||||||
|
<<<<<<< HEAD
|
||||||
import ChoreImageUpload from './ChoreImageUpload';
|
import ChoreImageUpload from './ChoreImageUpload';
|
||||||
|
=======
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
|
|
||||||
interface User {
|
interface User {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -25,31 +28,45 @@ const EditChoreModal: React.FC<EditChoreModalProps> = ({ choreId, onClose, onSuc
|
|||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
<<<<<<< HEAD
|
||||||
console.log('EditChoreModal: Loading chore ID:', choreId);
|
console.log('EditChoreModal: Loading chore ID:', choreId);
|
||||||
|
=======
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
loadChoreAndUsers();
|
loadChoreAndUsers();
|
||||||
}, [choreId]);
|
}, [choreId]);
|
||||||
|
|
||||||
const loadChoreAndUsers = async () => {
|
const loadChoreAndUsers = async () => {
|
||||||
try {
|
try {
|
||||||
|
<<<<<<< HEAD
|
||||||
console.log('EditChoreModal: Fetching chore and users...');
|
console.log('EditChoreModal: Fetching chore and users...');
|
||||||
|
=======
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
const [choreData, usersData] = await Promise.all([
|
const [choreData, usersData] = await Promise.all([
|
||||||
choreService.getChore(choreId),
|
choreService.getChore(choreId),
|
||||||
api.get<User[]>('/api/v1/users')
|
api.get<User[]>('/api/v1/users')
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
console.log('EditChoreModal: Chore data loaded:', choreData);
|
console.log('EditChoreModal: Chore data loaded:', choreData);
|
||||||
console.log('EditChoreModal: Users loaded:', usersData.data);
|
console.log('EditChoreModal: Users loaded:', usersData.data);
|
||||||
|
|
||||||
|
=======
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
setChore(choreData);
|
setChore(choreData);
|
||||||
setUsers(usersData.data.filter(u => u.is_active));
|
setUsers(usersData.data.filter(u => u.is_active));
|
||||||
|
|
||||||
// Initialize form with current chore data
|
// Initialize form with current chore data
|
||||||
|
<<<<<<< HEAD
|
||||||
const formInit = {
|
const formInit = {
|
||||||
|
=======
|
||||||
|
setFormData({
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
title: choreData.title,
|
title: choreData.title,
|
||||||
description: choreData.description || '',
|
description: choreData.description || '',
|
||||||
room: choreData.room,
|
room: choreData.room,
|
||||||
frequency: choreData.frequency,
|
frequency: choreData.frequency,
|
||||||
points: choreData.points,
|
points: choreData.points,
|
||||||
|
<<<<<<< HEAD
|
||||||
assignment_type: choreData.assignment_type,
|
assignment_type: choreData.assignment_type,
|
||||||
assigned_user_ids: choreData.assigned_users.map(u => u.id),
|
assigned_user_ids: choreData.assigned_users.map(u => u.id),
|
||||||
due_date: choreData.due_date ? choreData.due_date.split('T')[0] : '',
|
due_date: choreData.due_date ? choreData.due_date.split('T')[0] : '',
|
||||||
@@ -61,6 +78,14 @@ const EditChoreModal: React.FC<EditChoreModalProps> = ({ choreId, onClose, onSuc
|
|||||||
console.error('EditChoreModal: Failed to load chore:', error);
|
console.error('EditChoreModal: Failed to load chore:', error);
|
||||||
console.error('EditChoreModal: Error response:', error.response?.data);
|
console.error('EditChoreModal: Error response:', error.response?.data);
|
||||||
setError(`Failed to load chore details: ${error.response?.data?.detail || error.message}`);
|
setError(`Failed to load chore details: ${error.response?.data?.detail || error.message}`);
|
||||||
|
=======
|
||||||
|
assigned_user_ids: choreData.assigned_users.map(u => u.id),
|
||||||
|
due_date: choreData.due_date ? choreData.due_date.split('T')[0] : '',
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load chore:', error);
|
||||||
|
setError('Failed to load chore details');
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
@@ -71,14 +96,18 @@ const EditChoreModal: React.FC<EditChoreModalProps> = ({ choreId, onClose, onSuc
|
|||||||
setError('');
|
setError('');
|
||||||
setIsSaving(true);
|
setIsSaving(true);
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
console.log('EditChoreModal: Submitting update with data:', formData);
|
console.log('EditChoreModal: Submitting update with data:', formData);
|
||||||
|
|
||||||
|
=======
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
try {
|
try {
|
||||||
const submitData = { ...formData };
|
const submitData = { ...formData };
|
||||||
if (submitData.due_date) {
|
if (submitData.due_date) {
|
||||||
submitData.due_date = `${submitData.due_date}T23:59:59`;
|
submitData.due_date = `${submitData.due_date}T23:59:59`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
console.log('EditChoreModal: Calling API with:', submitData);
|
console.log('EditChoreModal: Calling API with:', submitData);
|
||||||
const result = await choreService.updateChore(choreId, submitData);
|
const result = await choreService.updateChore(choreId, submitData);
|
||||||
console.log('EditChoreModal: Update successful:', result);
|
console.log('EditChoreModal: Update successful:', result);
|
||||||
@@ -87,6 +116,11 @@ const EditChoreModal: React.FC<EditChoreModalProps> = ({ choreId, onClose, onSuc
|
|||||||
console.error('EditChoreModal: Update failed:', err);
|
console.error('EditChoreModal: Update failed:', err);
|
||||||
console.error('EditChoreModal: Error response:', err.response?.data);
|
console.error('EditChoreModal: Error response:', err.response?.data);
|
||||||
|
|
||||||
|
=======
|
||||||
|
await choreService.updateChore(choreId, submitData);
|
||||||
|
onSuccess();
|
||||||
|
} catch (err: any) {
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
let errorMessage = 'Failed to update chore';
|
let errorMessage = 'Failed to update chore';
|
||||||
|
|
||||||
if (err.response?.data) {
|
if (err.response?.data) {
|
||||||
@@ -121,6 +155,7 @@ const EditChoreModal: React.FC<EditChoreModalProps> = ({ choreId, onClose, onSuc
|
|||||||
const currentIds = prev.assigned_user_ids || [];
|
const currentIds = prev.assigned_user_ids || [];
|
||||||
const isAssigned = currentIds.includes(userId);
|
const isAssigned = currentIds.includes(userId);
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
const newIds = isAssigned
|
const newIds = isAssigned
|
||||||
? currentIds.filter(id => id !== userId)
|
? currentIds.filter(id => id !== userId)
|
||||||
: [...currentIds, userId];
|
: [...currentIds, userId];
|
||||||
@@ -130,6 +165,13 @@ const EditChoreModal: React.FC<EditChoreModalProps> = ({ choreId, onClose, onSuc
|
|||||||
return {
|
return {
|
||||||
...prev,
|
...prev,
|
||||||
assigned_user_ids: newIds
|
assigned_user_ids: newIds
|
||||||
|
=======
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
assigned_user_ids: isAssigned
|
||||||
|
? currentIds.filter(id => id !== userId)
|
||||||
|
: [...currentIds, userId]
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -146,6 +188,7 @@ const EditChoreModal: React.FC<EditChoreModalProps> = ({ choreId, onClose, onSuc
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!chore) {
|
if (!chore) {
|
||||||
|
<<<<<<< HEAD
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
|
||||||
<div className="bg-white rounded-lg shadow-xl p-8">
|
<div className="bg-white rounded-lg shadow-xl p-8">
|
||||||
@@ -160,6 +203,9 @@ const EditChoreModal: React.FC<EditChoreModalProps> = ({ choreId, onClose, onSuc
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
=======
|
||||||
|
return null;
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -181,8 +227,12 @@ const EditChoreModal: React.FC<EditChoreModalProps> = ({ choreId, onClose, onSuc
|
|||||||
<form onSubmit={handleSubmit} className="space-y-4">
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
{error && (
|
{error && (
|
||||||
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded">
|
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded">
|
||||||
|
<<<<<<< HEAD
|
||||||
<p className="font-bold">Error:</p>
|
<p className="font-bold">Error:</p>
|
||||||
<p>{error}</p>
|
<p>{error}</p>
|
||||||
|
=======
|
||||||
|
{error}
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -195,7 +245,11 @@ const EditChoreModal: React.FC<EditChoreModalProps> = ({ choreId, onClose, onSuc
|
|||||||
id="title"
|
id="title"
|
||||||
name="title"
|
name="title"
|
||||||
type="text"
|
type="text"
|
||||||
|
<<<<<<< HEAD
|
||||||
value={formData.title || ''}
|
value={formData.title || ''}
|
||||||
|
=======
|
||||||
|
value={formData.title}
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-gray-900"
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-gray-900"
|
||||||
required
|
required
|
||||||
@@ -209,7 +263,11 @@ const EditChoreModal: React.FC<EditChoreModalProps> = ({ choreId, onClose, onSuc
|
|||||||
<textarea
|
<textarea
|
||||||
id="description"
|
id="description"
|
||||||
name="description"
|
name="description"
|
||||||
|
<<<<<<< HEAD
|
||||||
value={formData.description || ''}
|
value={formData.description || ''}
|
||||||
|
=======
|
||||||
|
value={formData.description}
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
rows={2}
|
rows={2}
|
||||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-gray-900"
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-gray-900"
|
||||||
@@ -224,7 +282,11 @@ const EditChoreModal: React.FC<EditChoreModalProps> = ({ choreId, onClose, onSuc
|
|||||||
id="room"
|
id="room"
|
||||||
name="room"
|
name="room"
|
||||||
type="text"
|
type="text"
|
||||||
|
<<<<<<< HEAD
|
||||||
value={formData.room || ''}
|
value={formData.room || ''}
|
||||||
|
=======
|
||||||
|
value={formData.room}
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-gray-900"
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-gray-900"
|
||||||
required
|
required
|
||||||
@@ -238,7 +300,11 @@ const EditChoreModal: React.FC<EditChoreModalProps> = ({ choreId, onClose, onSuc
|
|||||||
<select
|
<select
|
||||||
id="frequency"
|
id="frequency"
|
||||||
name="frequency"
|
name="frequency"
|
||||||
|
<<<<<<< HEAD
|
||||||
value={formData.frequency || 'daily'}
|
value={formData.frequency || 'daily'}
|
||||||
|
=======
|
||||||
|
value={formData.frequency}
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-gray-900"
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-gray-900"
|
||||||
required
|
required
|
||||||
@@ -260,7 +326,11 @@ const EditChoreModal: React.FC<EditChoreModalProps> = ({ choreId, onClose, onSuc
|
|||||||
name="points"
|
name="points"
|
||||||
type="number"
|
type="number"
|
||||||
min="0"
|
min="0"
|
||||||
|
<<<<<<< HEAD
|
||||||
value={formData.points || 0}
|
value={formData.points || 0}
|
||||||
|
=======
|
||||||
|
value={formData.points}
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-gray-900"
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-gray-900"
|
||||||
/>
|
/>
|
||||||
@@ -274,11 +344,16 @@ const EditChoreModal: React.FC<EditChoreModalProps> = ({ choreId, onClose, onSuc
|
|||||||
id="due_date"
|
id="due_date"
|
||||||
name="due_date"
|
name="due_date"
|
||||||
type="date"
|
type="date"
|
||||||
|
<<<<<<< HEAD
|
||||||
value={formData.due_date || ''}
|
value={formData.due_date || ''}
|
||||||
|
=======
|
||||||
|
value={formData.due_date}
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-gray-900"
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-gray-900"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<<<<<<< HEAD
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="assignment_type" className="block text-sm font-medium text-gray-700 mb-2">
|
<label htmlFor="assignment_type" className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
@@ -319,6 +394,8 @@ const EditChoreModal: React.FC<EditChoreModalProps> = ({ choreId, onClose, onSuc
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
=======
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Multi-User Assignment */}
|
{/* Multi-User Assignment */}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useAuth } from '../contexts/AuthContext';
|
|||||||
import { choreService, Chore } from '../api/chores';
|
import { choreService, Chore } from '../api/chores';
|
||||||
import ChoreCard from '../components/ChoreCard';
|
import ChoreCard from '../components/ChoreCard';
|
||||||
import CreateChoreModal from '../components/CreateChoreModal';
|
import CreateChoreModal from '../components/CreateChoreModal';
|
||||||
|
<<<<<<< HEAD
|
||||||
import EditChoreModal from '../components/EditChoreModal';
|
import EditChoreModal from '../components/EditChoreModal';
|
||||||
import api from '../api/axios';
|
import api from '../api/axios';
|
||||||
|
|
||||||
@@ -13,10 +14,13 @@ interface User {
|
|||||||
full_name: string;
|
full_name: string;
|
||||||
is_active: boolean;
|
is_active: boolean;
|
||||||
}
|
}
|
||||||
|
=======
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
|
|
||||||
const Dashboard: React.FC = () => {
|
const Dashboard: React.FC = () => {
|
||||||
const { user, logout } = useAuth();
|
const { user, logout } = useAuth();
|
||||||
const [chores, setChores] = useState<Chore[]>([]);
|
const [chores, setChores] = useState<Chore[]>([]);
|
||||||
|
<<<<<<< HEAD
|
||||||
const [users, setUsers] = useState<User[]>([]);
|
const [users, setUsers] = useState<User[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [showCreateModal, setShowCreateModal] = useState(false);
|
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||||
@@ -44,6 +48,22 @@ const Dashboard: React.FC = () => {
|
|||||||
setUsers(usersData.data.filter(u => u.is_active));
|
setUsers(usersData.data.filter(u => u.is_active));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load data:', error);
|
console.error('Failed to load data:', error);
|
||||||
|
=======
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||||
|
const [filter, setFilter] = useState<'all' | 'my' | 'today'>('all');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadChores();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const loadChores = async () => {
|
||||||
|
try {
|
||||||
|
const data = await choreService.getChores();
|
||||||
|
setChores(data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load chores:', error);
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
@@ -52,7 +72,11 @@ const Dashboard: React.FC = () => {
|
|||||||
const handleCompleteChore = async (id: number) => {
|
const handleCompleteChore = async (id: number) => {
|
||||||
try {
|
try {
|
||||||
await choreService.completeChore(id);
|
await choreService.completeChore(id);
|
||||||
|
<<<<<<< HEAD
|
||||||
await loadData();
|
await loadData();
|
||||||
|
=======
|
||||||
|
await loadChores();
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to complete chore:', error);
|
console.error('Failed to complete chore:', error);
|
||||||
}
|
}
|
||||||
@@ -62,13 +86,18 @@ const Dashboard: React.FC = () => {
|
|||||||
if (window.confirm('Are you sure you want to delete this chore?')) {
|
if (window.confirm('Are you sure you want to delete this chore?')) {
|
||||||
try {
|
try {
|
||||||
await choreService.deleteChore(id);
|
await choreService.deleteChore(id);
|
||||||
|
<<<<<<< HEAD
|
||||||
await loadData();
|
await loadData();
|
||||||
|
=======
|
||||||
|
await loadChores();
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to delete chore:', error);
|
console.error('Failed to delete chore:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
const handleEditChore = (id: number) => {
|
const handleEditChore = (id: number) => {
|
||||||
setEditingChoreId(id);
|
setEditingChoreId(id);
|
||||||
};
|
};
|
||||||
@@ -80,10 +109,20 @@ const Dashboard: React.FC = () => {
|
|||||||
if (filter === 'today') {
|
if (filter === 'today') {
|
||||||
const today = new Date().toISOString().split('T')[0];
|
const today = new Date().toISOString().split('T')[0];
|
||||||
return chore.due_date?.startsWith(today) || chore.frequency === 'daily';
|
return chore.due_date?.startsWith(today) || chore.frequency === 'daily';
|
||||||
|
=======
|
||||||
|
const filteredChores = chores.filter((chore) => {
|
||||||
|
if (filter === 'my') {
|
||||||
|
return chore.assigned_to === user?.id;
|
||||||
|
}
|
||||||
|
if (filter === 'today') {
|
||||||
|
const today = new Date().toISOString().split('T')[0];
|
||||||
|
return chore.due_date === today || chore.frequency === 'daily';
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
// Calculate stats
|
// Calculate stats
|
||||||
const todayChores = chores.filter((chore) => {
|
const todayChores = chores.filter((chore) => {
|
||||||
const today = new Date().toISOString().split('T')[0];
|
const today = new Date().toISOString().split('T')[0];
|
||||||
@@ -101,6 +140,14 @@ const Dashboard: React.FC = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const myPoints = myChores.reduce((sum, chore) => sum + chore.points, 0);
|
const myPoints = myChores.reduce((sum, chore) => sum + chore.points, 0);
|
||||||
|
=======
|
||||||
|
const todayChores = chores.filter((chore) => {
|
||||||
|
const today = new Date().toISOString().split('T')[0];
|
||||||
|
return (chore.due_date === today || chore.frequency === 'daily') && chore.status !== 'completed';
|
||||||
|
});
|
||||||
|
|
||||||
|
const myChores = chores.filter((chore) => chore.assigned_to === user?.id && chore.status !== 'completed');
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50">
|
<div className="min-h-screen bg-gray-50">
|
||||||
@@ -113,6 +160,7 @@ const Dashboard: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Link
|
<Link
|
||||||
|
<<<<<<< HEAD
|
||||||
to="/reports"
|
to="/reports"
|
||||||
className="px-4 py-2 text-sm text-gray-700 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition-colors flex items-center gap-2"
|
className="px-4 py-2 text-sm text-gray-700 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition-colors flex items-center gap-2"
|
||||||
>
|
>
|
||||||
@@ -131,6 +179,8 @@ const Dashboard: React.FC = () => {
|
|||||||
My Stats
|
My Stats
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
|
=======
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
to="/settings"
|
to="/settings"
|
||||||
className="px-4 py-2 text-sm text-gray-700 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition-colors flex items-center gap-2"
|
className="px-4 py-2 text-sm text-gray-700 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition-colors flex items-center gap-2"
|
||||||
>
|
>
|
||||||
@@ -153,7 +203,11 @@ const Dashboard: React.FC = () => {
|
|||||||
{/* Main Content */}
|
{/* Main Content */}
|
||||||
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||||
{/* Stats */}
|
{/* Stats */}
|
||||||
|
<<<<<<< HEAD
|
||||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
|
||||||
|
=======
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
<div className="bg-white rounded-lg shadow p-6">
|
<div className="bg-white rounded-lg shadow p-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
@@ -185,6 +239,7 @@ const Dashboard: React.FC = () => {
|
|||||||
<div className="bg-white rounded-lg shadow p-6">
|
<div className="bg-white rounded-lg shadow p-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
|
<<<<<<< HEAD
|
||||||
<p className="text-sm text-gray-600">My Points</p>
|
<p className="text-sm text-gray-600">My Points</p>
|
||||||
<p className="text-3xl font-bold text-amber-600">{myPoints}</p>
|
<p className="text-3xl font-bold text-amber-600">{myPoints}</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -199,6 +254,10 @@ const Dashboard: React.FC = () => {
|
|||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-gray-600">Total Available</p>
|
<p className="text-sm text-gray-600">Total Available</p>
|
||||||
<p className="text-3xl font-bold text-purple-600">{totalPoints}</p>
|
<p className="text-3xl font-bold text-purple-600">{totalPoints}</p>
|
||||||
|
=======
|
||||||
|
<p className="text-sm text-gray-600">Total Tasks</p>
|
||||||
|
<p className="text-3xl font-bold text-purple-600">{chores.length}</p>
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
</div>
|
</div>
|
||||||
<div className="p-3 bg-purple-100 rounded-full">
|
<div className="p-3 bg-purple-100 rounded-full">
|
||||||
<svg className="w-8 h-8 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="w-8 h-8 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
@@ -209,6 +268,7 @@ const Dashboard: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
{/* Filters and Actions */}
|
{/* Filters and Actions */}
|
||||||
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 mb-6">
|
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 mb-6">
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
@@ -216,6 +276,15 @@ const Dashboard: React.FC = () => {
|
|||||||
onClick={() => { setFilter('all'); setSelectedUserId(null); }}
|
onClick={() => { setFilter('all'); setSelectedUserId(null); }}
|
||||||
className={`px-4 py-2 rounded-lg transition-colors ${
|
className={`px-4 py-2 rounded-lg transition-colors ${
|
||||||
filter === 'all' && !selectedUserId
|
filter === 'all' && !selectedUserId
|
||||||
|
=======
|
||||||
|
{/* Actions */}
|
||||||
|
<div className="flex justify-between items-center mb-6">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => setFilter('all')}
|
||||||
|
className={`px-4 py-2 rounded-lg transition-colors ${
|
||||||
|
filter === 'all'
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
? 'bg-blue-600 text-white'
|
? 'bg-blue-600 text-white'
|
||||||
: 'bg-white text-gray-700 hover:bg-gray-50'
|
: 'bg-white text-gray-700 hover:bg-gray-50'
|
||||||
}`}
|
}`}
|
||||||
@@ -223,7 +292,11 @@ const Dashboard: React.FC = () => {
|
|||||||
All Tasks
|
All Tasks
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
<<<<<<< HEAD
|
||||||
onClick={() => { setFilter('today'); setSelectedUserId(null); }}
|
onClick={() => { setFilter('today'); setSelectedUserId(null); }}
|
||||||
|
=======
|
||||||
|
onClick={() => setFilter('today')}
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
className={`px-4 py-2 rounded-lg transition-colors ${
|
className={`px-4 py-2 rounded-lg transition-colors ${
|
||||||
filter === 'today'
|
filter === 'today'
|
||||||
? 'bg-blue-600 text-white'
|
? 'bg-blue-600 text-white'
|
||||||
@@ -233,7 +306,11 @@ const Dashboard: React.FC = () => {
|
|||||||
Today
|
Today
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
<<<<<<< HEAD
|
||||||
onClick={() => { setFilter('my'); setSelectedUserId(null); }}
|
onClick={() => { setFilter('my'); setSelectedUserId(null); }}
|
||||||
|
=======
|
||||||
|
onClick={() => setFilter('my')}
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
className={`px-4 py-2 rounded-lg transition-colors ${
|
className={`px-4 py-2 rounded-lg transition-colors ${
|
||||||
filter === 'my'
|
filter === 'my'
|
||||||
? 'bg-blue-600 text-white'
|
? 'bg-blue-600 text-white'
|
||||||
@@ -242,6 +319,7 @@ const Dashboard: React.FC = () => {
|
|||||||
>
|
>
|
||||||
My Tasks
|
My Tasks
|
||||||
</button>
|
</button>
|
||||||
|
<<<<<<< HEAD
|
||||||
|
|
||||||
{/* User Filter Dropdown */}
|
{/* User Filter Dropdown */}
|
||||||
<select
|
<select
|
||||||
@@ -270,11 +348,17 @@ const Dashboard: React.FC = () => {
|
|||||||
<span>🎂</span>
|
<span>🎂</span>
|
||||||
<span>Hide Birthday Chores</span>
|
<span>Hide Birthday Chores</span>
|
||||||
</button>
|
</button>
|
||||||
|
=======
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowCreateModal(true)}
|
onClick={() => setShowCreateModal(true)}
|
||||||
|
<<<<<<< HEAD
|
||||||
className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors flex items-center gap-2 whitespace-nowrap"
|
className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors flex items-center gap-2 whitespace-nowrap"
|
||||||
|
=======
|
||||||
|
className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors flex items-center gap-2"
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
>
|
>
|
||||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
||||||
@@ -283,6 +367,7 @@ const Dashboard: React.FC = () => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
{/* Active Filters Display */}
|
{/* Active Filters Display */}
|
||||||
{(selectedUserId || hideBirthdayChores) && (
|
{(selectedUserId || hideBirthdayChores) && (
|
||||||
<div className="mb-4 flex flex-wrap gap-2">
|
<div className="mb-4 flex flex-wrap gap-2">
|
||||||
@@ -315,17 +400,24 @@ const Dashboard: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
=======
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
{/* Chores List */}
|
{/* Chores List */}
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="text-center py-12">
|
<div className="text-center py-12">
|
||||||
<div className="inline-block animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
|
<div className="inline-block animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
|
||||||
|
<<<<<<< HEAD
|
||||||
<p className="mt-4 text-gray-600">Loading chores...</p>
|
<p className="mt-4 text-gray-600">Loading chores...</p>
|
||||||
|
=======
|
||||||
|
<p className="mt-4 text-gray-600">Loading tasks...</p>
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
</div>
|
</div>
|
||||||
) : filteredChores.length === 0 ? (
|
) : filteredChores.length === 0 ? (
|
||||||
<div className="text-center py-12 bg-white rounded-lg shadow">
|
<div className="text-center py-12 bg-white rounded-lg shadow">
|
||||||
<svg className="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
|
||||||
</svg>
|
</svg>
|
||||||
|
<<<<<<< HEAD
|
||||||
<h3 className="mt-2 text-sm font-medium text-gray-900">No chores found</h3>
|
<h3 className="mt-2 text-sm font-medium text-gray-900">No chores found</h3>
|
||||||
<p className="mt-1 text-sm text-gray-500">
|
<p className="mt-1 text-sm text-gray-500">
|
||||||
{selectedUserId
|
{selectedUserId
|
||||||
@@ -334,6 +426,10 @@ const Dashboard: React.FC = () => {
|
|||||||
? "All chores are birthday chores today! 🎂"
|
? "All chores are birthday chores today! 🎂"
|
||||||
: "Get started by creating a new chore."}
|
: "Get started by creating a new chore."}
|
||||||
</p>
|
</p>
|
||||||
|
=======
|
||||||
|
<h3 className="mt-2 text-sm font-medium text-gray-900">No tasks</h3>
|
||||||
|
<p className="mt-1 text-sm text-gray-500">Get started by creating a new task.</p>
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
@@ -343,19 +439,27 @@ const Dashboard: React.FC = () => {
|
|||||||
chore={chore}
|
chore={chore}
|
||||||
onComplete={handleCompleteChore}
|
onComplete={handleCompleteChore}
|
||||||
onDelete={handleDeleteChore}
|
onDelete={handleDeleteChore}
|
||||||
|
<<<<<<< HEAD
|
||||||
onEdit={handleEditChore}
|
onEdit={handleEditChore}
|
||||||
|
=======
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
{/* Modals */}
|
{/* Modals */}
|
||||||
|
=======
|
||||||
|
{/* Create Chore Modal */}
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
{showCreateModal && (
|
{showCreateModal && (
|
||||||
<CreateChoreModal
|
<CreateChoreModal
|
||||||
onClose={() => setShowCreateModal(false)}
|
onClose={() => setShowCreateModal(false)}
|
||||||
onSuccess={() => {
|
onSuccess={() => {
|
||||||
setShowCreateModal(false);
|
setShowCreateModal(false);
|
||||||
|
<<<<<<< HEAD
|
||||||
loadData();
|
loadData();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -368,6 +472,9 @@ const Dashboard: React.FC = () => {
|
|||||||
onSuccess={() => {
|
onSuccess={() => {
|
||||||
setEditingChoreId(null);
|
setEditingChoreId(null);
|
||||||
loadData();
|
loadData();
|
||||||
|
=======
|
||||||
|
loadChores();
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { useAuth } from '../contexts/AuthContext';
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
|
<<<<<<< HEAD
|
||||||
import api, { API_BASE_URL } from '../api/axios';
|
import api, { API_BASE_URL } from '../api/axios';
|
||||||
import AvatarUpload from '../components/AvatarUpload';
|
import AvatarUpload from '../components/AvatarUpload';
|
||||||
import { getUserColor, getInitials } from '../utils/avatarUtils';
|
import { getUserColor, getInitials } from '../utils/avatarUtils';
|
||||||
|
=======
|
||||||
|
import api from '../api/axios';
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
|
|
||||||
interface UserProfile {
|
interface UserProfile {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -11,10 +15,14 @@ interface UserProfile {
|
|||||||
full_name: string;
|
full_name: string;
|
||||||
discord_id?: string;
|
discord_id?: string;
|
||||||
profile_picture?: string;
|
profile_picture?: string;
|
||||||
|
<<<<<<< HEAD
|
||||||
avatar_url?: string;
|
avatar_url?: string;
|
||||||
birthday?: string;
|
birthday?: string;
|
||||||
is_admin: boolean;
|
is_admin: boolean;
|
||||||
is_active: boolean;
|
is_active: boolean;
|
||||||
|
=======
|
||||||
|
is_admin: boolean;
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UpdateProfileData {
|
interface UpdateProfileData {
|
||||||
@@ -22,6 +30,7 @@ interface UpdateProfileData {
|
|||||||
full_name?: string;
|
full_name?: string;
|
||||||
discord_id?: string;
|
discord_id?: string;
|
||||||
profile_picture?: string;
|
profile_picture?: string;
|
||||||
|
<<<<<<< HEAD
|
||||||
birthday?: string;
|
birthday?: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
}
|
}
|
||||||
@@ -34,6 +43,13 @@ interface AdminUpdateData extends UpdateProfileData {
|
|||||||
const Settings: React.FC = () => {
|
const Settings: React.FC = () => {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const [activeTab, setActiveTab] = useState<'profile' | 'password' | 'users'>('profile');
|
const [activeTab, setActiveTab] = useState<'profile' | 'password' | 'users'>('profile');
|
||||||
|
=======
|
||||||
|
password?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Settings: React.FC = () => {
|
||||||
|
const { user } = useAuth();
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
const [profile, setProfile] = useState<UserProfile | null>(null);
|
const [profile, setProfile] = useState<UserProfile | null>(null);
|
||||||
const [formData, setFormData] = useState<UpdateProfileData>({});
|
const [formData, setFormData] = useState<UpdateProfileData>({});
|
||||||
const [confirmPassword, setConfirmPassword] = useState('');
|
const [confirmPassword, setConfirmPassword] = useState('');
|
||||||
@@ -41,8 +57,12 @@ const Settings: React.FC = () => {
|
|||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
const [success, setSuccess] = useState('');
|
const [success, setSuccess] = useState('');
|
||||||
const [allUsers, setAllUsers] = useState<UserProfile[]>([]);
|
const [allUsers, setAllUsers] = useState<UserProfile[]>([]);
|
||||||
|
<<<<<<< HEAD
|
||||||
const [selectedUser, setSelectedUser] = useState<UserProfile | null>(null);
|
const [selectedUser, setSelectedUser] = useState<UserProfile | null>(null);
|
||||||
const [editFormData, setEditFormData] = useState<AdminUpdateData>({});
|
const [editFormData, setEditFormData] = useState<AdminUpdateData>({});
|
||||||
|
=======
|
||||||
|
const [selectedUserId, setSelectedUserId] = useState<number | null>(null);
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadProfile();
|
loadProfile();
|
||||||
@@ -60,7 +80,10 @@ const Settings: React.FC = () => {
|
|||||||
full_name: response.data.full_name,
|
full_name: response.data.full_name,
|
||||||
discord_id: response.data.discord_id || '',
|
discord_id: response.data.discord_id || '',
|
||||||
profile_picture: response.data.profile_picture || '',
|
profile_picture: response.data.profile_picture || '',
|
||||||
|
<<<<<<< HEAD
|
||||||
birthday: response.data.birthday || '',
|
birthday: response.data.birthday || '',
|
||||||
|
=======
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to load profile:', err);
|
console.error('Failed to load profile:', err);
|
||||||
@@ -81,11 +104,22 @@ const Settings: React.FC = () => {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setError('');
|
setError('');
|
||||||
setSuccess('');
|
setSuccess('');
|
||||||
|
<<<<<<< HEAD
|
||||||
|
=======
|
||||||
|
|
||||||
|
// Validate passwords match if changing password
|
||||||
|
if (formData.password && formData.password !== confirmPassword) {
|
||||||
|
setError('Passwords do not match');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const updateData: UpdateProfileData = {};
|
const updateData: UpdateProfileData = {};
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
if (formData.email !== profile?.email) updateData.email = formData.email;
|
if (formData.email !== profile?.email) updateData.email = formData.email;
|
||||||
if (formData.full_name !== profile?.full_name) updateData.full_name = formData.full_name;
|
if (formData.full_name !== profile?.full_name) updateData.full_name = formData.full_name;
|
||||||
if (formData.discord_id !== profile?.discord_id) updateData.discord_id = formData.discord_id;
|
if (formData.discord_id !== profile?.discord_id) updateData.discord_id = formData.discord_id;
|
||||||
@@ -93,6 +127,19 @@ const Settings: React.FC = () => {
|
|||||||
|
|
||||||
await api.put('/api/v1/auth/me', updateData);
|
await api.put('/api/v1/auth/me', updateData);
|
||||||
setSuccess('Profile updated successfully!');
|
setSuccess('Profile updated successfully!');
|
||||||
|
=======
|
||||||
|
// Only include changed fields
|
||||||
|
if (formData.email !== profile?.email) updateData.email = formData.email;
|
||||||
|
if (formData.full_name !== profile?.full_name) updateData.full_name = formData.full_name;
|
||||||
|
if (formData.discord_id !== profile?.discord_id) updateData.discord_id = formData.discord_id;
|
||||||
|
if (formData.profile_picture !== profile?.profile_picture) updateData.profile_picture = formData.profile_picture;
|
||||||
|
if (formData.password) updateData.password = formData.password;
|
||||||
|
|
||||||
|
await api.put('/api/v1/auth/me', updateData);
|
||||||
|
setSuccess('Profile updated successfully!');
|
||||||
|
setFormData({ ...formData, password: '' });
|
||||||
|
setConfirmPassword('');
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
loadProfile();
|
loadProfile();
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setError(err.response?.data?.detail || 'Failed to update profile');
|
setError(err.response?.data?.detail || 'Failed to update profile');
|
||||||
@@ -101,6 +148,7 @@ const Settings: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
const handlePasswordChange = async (e: React.FormEvent) => {
|
const handlePasswordChange = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setError('');
|
setError('');
|
||||||
@@ -130,6 +178,12 @@ const Settings: React.FC = () => {
|
|||||||
await api.put(`/api/v1/auth/users/${userId}`, updateData);
|
await api.put(`/api/v1/auth/users/${userId}`, updateData);
|
||||||
setSuccess('User updated successfully!');
|
setSuccess('User updated successfully!');
|
||||||
setSelectedUser(null);
|
setSelectedUser(null);
|
||||||
|
=======
|
||||||
|
const handleAdminUpdateUser = async (userId: number, updateData: Partial<UpdateProfileData & { is_active: boolean; is_admin: boolean }>) => {
|
||||||
|
try {
|
||||||
|
await api.put(`/api/v1/auth/users/${userId}`, updateData);
|
||||||
|
setSuccess('User updated successfully!');
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
loadAllUsers();
|
loadAllUsers();
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setError(err.response?.data?.detail || 'Failed to update user');
|
setError(err.response?.data?.detail || 'Failed to update user');
|
||||||
@@ -141,6 +195,7 @@ const Settings: React.FC = () => {
|
|||||||
setFormData(prev => ({ ...prev, [name]: value }));
|
setFormData(prev => ({ ...prev, [name]: value }));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
const openEditModal = (u: UserProfile) => {
|
const openEditModal = (u: UserProfile) => {
|
||||||
setSelectedUser(u);
|
setSelectedUser(u);
|
||||||
setEditFormData({
|
setEditFormData({
|
||||||
@@ -168,11 +223,14 @@ const Settings: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
=======
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
if (!profile) {
|
if (!profile) {
|
||||||
return <div className="text-center py-8">Loading...</div>;
|
return <div className="text-center py-8">Loading...</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<<<<<<< HEAD
|
||||||
<div className="max-w-6xl mx-auto p-6">
|
<div className="max-w-6xl mx-auto p-6">
|
||||||
<h1 className="text-3xl font-bold text-gray-900 mb-8">Settings</h1>
|
<h1 className="text-3xl font-bold text-gray-900 mb-8">Settings</h1>
|
||||||
|
|
||||||
@@ -355,6 +413,103 @@ const Settings: React.FC = () => {
|
|||||||
{/* Password Tab */}
|
{/* Password Tab */}
|
||||||
{activeTab === 'password' && (
|
{activeTab === 'password' && (
|
||||||
<form onSubmit={handlePasswordChange} className="space-y-6 max-w-md">
|
<form onSubmit={handlePasswordChange} className="space-y-6 max-w-md">
|
||||||
|
=======
|
||||||
|
<div className="max-w-4xl mx-auto p-6">
|
||||||
|
<h1 className="text-3xl font-bold text-gray-900 mb-8">Settings</h1>
|
||||||
|
|
||||||
|
{/* Personal Profile Section */}
|
||||||
|
<div className="bg-white rounded-lg shadow-md p-6 mb-6">
|
||||||
|
<h2 className="text-2xl font-semibold text-gray-900 mb-4">My Profile</h2>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded mb-4">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{success && (
|
||||||
|
<div className="bg-green-50 border border-green-200 text-green-700 px-4 py-3 rounded mb-4">
|
||||||
|
{success}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Username
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={profile.username}
|
||||||
|
disabled
|
||||||
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg bg-gray-100 text-gray-500 cursor-not-allowed"
|
||||||
|
/>
|
||||||
|
<p className="text-sm text-gray-500 mt-1">Username cannot be changed</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Email Address
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="email"
|
||||||
|
name="email"
|
||||||
|
type="email"
|
||||||
|
value={formData.email || ''}
|
||||||
|
onChange={handleChange}
|
||||||
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-gray-900"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label htmlFor="full_name" className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Full Name
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="full_name"
|
||||||
|
name="full_name"
|
||||||
|
type="text"
|
||||||
|
value={formData.full_name || ''}
|
||||||
|
onChange={handleChange}
|
||||||
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-gray-900"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label htmlFor="discord_id" className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Discord ID <span className="text-gray-500">(for notifications)</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="discord_id"
|
||||||
|
name="discord_id"
|
||||||
|
type="text"
|
||||||
|
value={formData.discord_id || ''}
|
||||||
|
onChange={handleChange}
|
||||||
|
placeholder="e.g., YourDiscordName#1234"
|
||||||
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-gray-900"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label htmlFor="profile_picture" className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Profile Picture URL
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="profile_picture"
|
||||||
|
name="profile_picture"
|
||||||
|
type="url"
|
||||||
|
value={formData.profile_picture || ''}
|
||||||
|
onChange={handleChange}
|
||||||
|
placeholder="https://example.com/avatar.jpg"
|
||||||
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-gray-900"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border-t border-gray-200 pt-4 mt-6">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 mb-4">Change Password</h3>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="password" className="block text-sm font-medium text-gray-700 mb-2">
|
<label htmlFor="password" className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
New Password
|
New Password
|
||||||
@@ -365,9 +520,14 @@ const Settings: React.FC = () => {
|
|||||||
type="password"
|
type="password"
|
||||||
value={formData.password || ''}
|
value={formData.password || ''}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
|
<<<<<<< HEAD
|
||||||
placeholder="Enter new password"
|
placeholder="Enter new password"
|
||||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-gray-900"
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-gray-900"
|
||||||
required
|
required
|
||||||
|
=======
|
||||||
|
placeholder="Leave blank to keep current password"
|
||||||
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-gray-900"
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -383,6 +543,7 @@ const Settings: React.FC = () => {
|
|||||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||||
placeholder="Confirm new password"
|
placeholder="Confirm new password"
|
||||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-gray-900"
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-gray-900"
|
||||||
|
<<<<<<< HEAD
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -615,6 +776,67 @@ const Settings: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
=======
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="pt-4">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={isLoading}
|
||||||
|
className="w-full px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
{isLoading ? 'Saving...' : 'Save Changes'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Admin Section */}
|
||||||
|
{user?.is_admin && (
|
||||||
|
<div className="bg-white rounded-lg shadow-md p-6">
|
||||||
|
<h2 className="text-2xl font-semibold text-gray-900 mb-4">
|
||||||
|
User Management <span className="text-sm font-normal text-gray-500">(Admin)</span>
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
{allUsers.map((u) => (
|
||||||
|
<div key={u.id} className="border border-gray-200 rounded-lg p-4">
|
||||||
|
<div className="flex justify-between items-start">
|
||||||
|
<div>
|
||||||
|
<h3 className="font-semibold text-gray-900">{u.full_name}</h3>
|
||||||
|
<p className="text-sm text-gray-500">@{u.username}</p>
|
||||||
|
<p className="text-sm text-gray-500">{u.email}</p>
|
||||||
|
{u.discord_id && (
|
||||||
|
<p className="text-sm text-gray-500">Discord: {u.discord_id}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => handleAdminUpdateUser(u.id, { is_active: !u.is_active })}
|
||||||
|
className={`px-3 py-1 text-sm rounded ${
|
||||||
|
u.is_active
|
||||||
|
? 'bg-yellow-100 text-yellow-800 hover:bg-yellow-200'
|
||||||
|
: 'bg-green-100 text-green-800 hover:bg-green-200'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{u.is_active ? 'Lock' : 'Unlock'}
|
||||||
|
</button>
|
||||||
|
{u.id !== user.id && (
|
||||||
|
<button
|
||||||
|
onClick={() => setSelectedUserId(u.id)}
|
||||||
|
className="px-3 py-1 text-sm bg-blue-100 text-blue-800 hover:bg-blue-200 rounded"
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user