Phase 3.1: Add remaining local files

This commit is contained in:
2026-02-05 16:04:20 +11:00
26 changed files with 1780 additions and 1 deletions

View File

@@ -3,9 +3,12 @@ import { AuthProvider, useAuth } from './contexts/AuthContext';
import Login from './pages/Login';
import Dashboard from './pages/Dashboard';
import Settings from './pages/Settings';
<<<<<<< HEAD
import KioskView from './pages/KioskView';
import Reports from './pages/Reports';
import UserStatsPage from './pages/UserStatsPage';
=======
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
// Protected route wrapper
function ProtectedRoute({ children }: { children: React.ReactNode }) {
@@ -56,9 +59,12 @@ function App() {
<Router>
<AuthProvider>
<Routes>
<<<<<<< HEAD
{/* Public Kiosk View - No Auth Required */}
<Route path="/kiosk" element={<KioskView />} />
=======
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
{/* Public routes */}
<Route
path="/login"
@@ -88,6 +94,7 @@ function App() {
}
/>
<<<<<<< HEAD
<Route
path="/reports"
element={
@@ -106,6 +113,8 @@ function App() {
}
/>
=======
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
{/* Default route */}
<Route path="/" element={<Navigate to="/dashboard" replace />} />

View File

@@ -1,6 +1,10 @@
import axios from 'axios';
<<<<<<< HEAD
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({
baseURL: API_BASE_URL,

View File

@@ -4,7 +4,10 @@ export interface AssignedUser {
id: number;
username: string;
full_name: string;
<<<<<<< HEAD
avatar_url?: string;
=======
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
birthday?: string;
completed_at?: string;
}
@@ -16,8 +19,11 @@ export interface Chore {
room: string;
frequency: 'daily' | 'weekly' | 'fortnightly' | 'monthly' | 'on_trigger';
points: number;
<<<<<<< HEAD
image_url?: string;
assignment_type: 'any_one' | 'all_assigned';
=======
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
status: 'pending' | 'in_progress' | 'completed' | 'skipped';
assigned_users: AssignedUser[]; // Multiple users
assigned_user_id?: number; // Legacy field
@@ -38,7 +44,10 @@ export interface CreateChoreRequest {
room: string;
frequency: 'daily' | 'weekly' | 'fortnightly' | 'monthly' | 'on_trigger';
points?: number;
<<<<<<< HEAD
assignment_type?: 'any_one' | 'all_assigned';
=======
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
assigned_user_ids?: number[]; // Multiple users
due_date?: string;
}
@@ -49,7 +58,10 @@ export interface UpdateChoreRequest {
room?: string;
frequency?: 'daily' | 'weekly' | 'fortnightly' | 'monthly' | 'on_trigger';
points?: number;
<<<<<<< HEAD
assignment_type?: 'any_one' | 'all_assigned';
=======
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
status?: 'pending' | 'in_progress' | 'completed' | 'skipped';
assigned_user_ids?: number[]; // Multiple users
due_date?: string;

View File

@@ -1,8 +1,11 @@
import React from 'react';
import { Chore } from '../api/chores';
import { useAuth } from '../contexts/AuthContext';
<<<<<<< HEAD
import { getUserColor, getInitials } from '../utils/avatarUtils';
import { API_BASE_URL } from '../api/axios';
=======
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
interface ChoreCardProps {
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>
)}
<<<<<<< HEAD
{/* Chore Image */}
{chore.image_url && (
<div className="mb-3">
@@ -72,6 +76,8 @@ const ChoreCard: React.FC<ChoreCardProps> = ({ chore, onComplete, onDelete, onEd
</div>
)}
=======
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
<div className="space-y-2 mb-4">
<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">
@@ -103,6 +109,7 @@ const ChoreCard: React.FC<ChoreCardProps> = ({ chore, onComplete, onDelete, onEd
return (
<div key={assignedUser.id} className="flex items-center justify-between text-sm">
<<<<<<< HEAD
<div className="flex items-center gap-2">
{/* User Avatar */}
{assignedUser.avatar_url ? (
@@ -121,6 +128,12 @@ const ChoreCard: React.FC<ChoreCardProps> = ({ chore, onComplete, onDelete, onEd
{isBirthday && ' 🎂'}
</span>
</div>
=======
<span className={`${assignedUser.id === user?.id ? 'font-medium text-blue-600' : 'text-gray-600'}`}>
{assignedUser.full_name}
{isBirthday && ' 🎂'}
</span>
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
{assignedUser.completed_at && (
<span className="text-xs text-green-600"> Done</span>
)}

View File

@@ -1,6 +1,7 @@
import React, { useState, useEffect } from 'react';
import { choreService, CreateChoreRequest } from '../api/chores';
import api from '../api/axios';
<<<<<<< HEAD
interface User {
id: number;
@@ -8,6 +9,9 @@ interface User {
full_name: string;
is_active: boolean;
}
=======
import { User } from '../api/auth';
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
interface CreateChoreModalProps {
onClose: () => void;
@@ -20,8 +24,12 @@ const CreateChoreModal: React.FC<CreateChoreModalProps> = ({ onClose, onSuccess
description: '',
room: '',
frequency: 'daily',
<<<<<<< HEAD
points: 5,
assigned_user_ids: [],
=======
assigned_to: undefined,
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
due_date: '',
});
const [users, setUsers] = useState<User[]>([]);
@@ -35,7 +43,11 @@ const CreateChoreModal: React.FC<CreateChoreModalProps> = ({ onClose, onSuccess
const loadUsers = async () => {
try {
const response = await api.get<User[]>('/api/v1/users');
<<<<<<< HEAD
setUsers(response.data.filter(u => u.is_active));
=======
setUsers(response.data);
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
} catch (error) {
console.error('Failed to load users:', error);
}
@@ -47,18 +59,35 @@ const CreateChoreModal: React.FC<CreateChoreModalProps> = ({ onClose, onSuccess
setIsLoading(true);
try {
<<<<<<< HEAD
const submitData = { ...formData };
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`;
}
await choreService.createChore(submitData);
onSuccess();
} catch (err: any) {
<<<<<<< HEAD
let errorMessage = 'Failed to create chore';
if (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)) {
errorMessage = errorData.detail
.map((e: any) => `${e.loc?.join('.')}: ${e.msg}`)
@@ -80,6 +109,7 @@ const CreateChoreModal: React.FC<CreateChoreModalProps> = ({ onClose, onSuccess
const { name, value } = e.target;
setFormData(prev => ({
...prev,
<<<<<<< HEAD
[name]: name === 'points' ? parseInt(value) || 0 : value,
}));
};
@@ -104,6 +134,18 @@ const CreateChoreModal: React.FC<CreateChoreModalProps> = ({ onClose, onSuccess
<div className="p-6">
<div className="flex justify-between items-center mb-6">
<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
onClick={onClose}
className="text-gray-400 hover:text-gray-600 transition-colors"
@@ -121,6 +163,7 @@ const CreateChoreModal: React.FC<CreateChoreModalProps> = ({ onClose, onSuccess
</div>
)}
<<<<<<< HEAD
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="md:col-span-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
</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 className="flex gap-3 pt-4">
@@ -284,7 +428,11 @@ const CreateChoreModal: React.FC<CreateChoreModalProps> = ({ onClose, onSuccess
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"
>
<<<<<<< HEAD
{isLoading ? 'Creating...' : 'Create Chore'}
=======
{isLoading ? 'Creating...' : 'Create Task'}
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
</button>
</div>
</form>

View File

@@ -1,7 +1,10 @@
import React, { useState, useEffect } from 'react';
import { choreService, Chore, UpdateChoreRequest } from '../api/chores';
import api from '../api/axios';
<<<<<<< HEAD
import ChoreImageUpload from './ChoreImageUpload';
=======
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
interface User {
id: number;
@@ -25,31 +28,45 @@ const EditChoreModal: React.FC<EditChoreModalProps> = ({ choreId, onClose, onSuc
const [error, setError] = useState('');
useEffect(() => {
<<<<<<< HEAD
console.log('EditChoreModal: Loading chore ID:', choreId);
=======
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
loadChoreAndUsers();
}, [choreId]);
const loadChoreAndUsers = async () => {
try {
<<<<<<< HEAD
console.log('EditChoreModal: Fetching chore and users...');
=======
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
const [choreData, usersData] = await Promise.all([
choreService.getChore(choreId),
api.get<User[]>('/api/v1/users')
]);
<<<<<<< HEAD
console.log('EditChoreModal: Chore data loaded:', choreData);
console.log('EditChoreModal: Users loaded:', usersData.data);
=======
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
setChore(choreData);
setUsers(usersData.data.filter(u => u.is_active));
// Initialize form with current chore data
<<<<<<< HEAD
const formInit = {
=======
setFormData({
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
title: choreData.title,
description: choreData.description || '',
room: choreData.room,
frequency: choreData.frequency,
points: choreData.points,
<<<<<<< HEAD
assignment_type: choreData.assignment_type,
assigned_user_ids: choreData.assigned_users.map(u => u.id),
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: Error response:', error.response?.data);
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 {
setIsLoading(false);
}
@@ -71,14 +96,18 @@ const EditChoreModal: React.FC<EditChoreModalProps> = ({ choreId, onClose, onSuc
setError('');
setIsSaving(true);
<<<<<<< HEAD
console.log('EditChoreModal: Submitting update with data:', formData);
=======
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
try {
const submitData = { ...formData };
if (submitData.due_date) {
submitData.due_date = `${submitData.due_date}T23:59:59`;
}
<<<<<<< HEAD
console.log('EditChoreModal: Calling API with:', submitData);
const result = await choreService.updateChore(choreId, submitData);
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: Error response:', err.response?.data);
=======
await choreService.updateChore(choreId, submitData);
onSuccess();
} catch (err: any) {
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
let errorMessage = 'Failed to update chore';
if (err.response?.data) {
@@ -121,6 +155,7 @@ const EditChoreModal: React.FC<EditChoreModalProps> = ({ choreId, onClose, onSuc
const currentIds = prev.assigned_user_ids || [];
const isAssigned = currentIds.includes(userId);
<<<<<<< HEAD
const newIds = isAssigned
? currentIds.filter(id => id !== userId)
: [...currentIds, userId];
@@ -130,6 +165,13 @@ const EditChoreModal: React.FC<EditChoreModalProps> = ({ choreId, onClose, onSuc
return {
...prev,
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) {
<<<<<<< HEAD
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 p-8">
@@ -160,6 +203,9 @@ const EditChoreModal: React.FC<EditChoreModalProps> = ({ choreId, onClose, onSuc
</div>
</div>
);
=======
return null;
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
}
return (
@@ -181,8 +227,12 @@ const EditChoreModal: React.FC<EditChoreModalProps> = ({ choreId, onClose, onSuc
<form onSubmit={handleSubmit} className="space-y-4">
{error && (
<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>{error}</p>
=======
{error}
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
</div>
)}
@@ -195,7 +245,11 @@ const EditChoreModal: React.FC<EditChoreModalProps> = ({ choreId, onClose, onSuc
id="title"
name="title"
type="text"
<<<<<<< HEAD
value={formData.title || ''}
=======
value={formData.title}
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
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
@@ -209,7 +263,11 @@ const EditChoreModal: React.FC<EditChoreModalProps> = ({ choreId, onClose, onSuc
<textarea
id="description"
name="description"
<<<<<<< HEAD
value={formData.description || ''}
=======
value={formData.description}
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
onChange={handleChange}
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"
@@ -224,7 +282,11 @@ const EditChoreModal: React.FC<EditChoreModalProps> = ({ choreId, onClose, onSuc
id="room"
name="room"
type="text"
<<<<<<< HEAD
value={formData.room || ''}
=======
value={formData.room}
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
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
@@ -238,7 +300,11 @@ const EditChoreModal: React.FC<EditChoreModalProps> = ({ choreId, onClose, onSuc
<select
id="frequency"
name="frequency"
<<<<<<< HEAD
value={formData.frequency || 'daily'}
=======
value={formData.frequency}
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
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
@@ -260,7 +326,11 @@ const EditChoreModal: React.FC<EditChoreModalProps> = ({ choreId, onClose, onSuc
name="points"
type="number"
min="0"
<<<<<<< HEAD
value={formData.points || 0}
=======
value={formData.points}
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
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"
/>
@@ -274,11 +344,16 @@ const EditChoreModal: React.FC<EditChoreModalProps> = ({ choreId, onClose, onSuc
id="due_date"
name="due_date"
type="date"
<<<<<<< HEAD
value={formData.due_date || ''}
=======
value={formData.due_date}
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
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>
<<<<<<< HEAD
<div>
<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>
{/* Multi-User Assignment */}

View File

@@ -4,6 +4,7 @@ import { useAuth } from '../contexts/AuthContext';
import { choreService, Chore } from '../api/chores';
import ChoreCard from '../components/ChoreCard';
import CreateChoreModal from '../components/CreateChoreModal';
<<<<<<< HEAD
import EditChoreModal from '../components/EditChoreModal';
import api from '../api/axios';
@@ -13,10 +14,13 @@ interface User {
full_name: string;
is_active: boolean;
}
=======
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
const Dashboard: React.FC = () => {
const { user, logout } = useAuth();
const [chores, setChores] = useState<Chore[]>([]);
<<<<<<< HEAD
const [users, setUsers] = useState<User[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [showCreateModal, setShowCreateModal] = useState(false);
@@ -44,6 +48,22 @@ const Dashboard: React.FC = () => {
setUsers(usersData.data.filter(u => u.is_active));
} catch (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 {
setIsLoading(false);
}
@@ -52,7 +72,11 @@ const Dashboard: React.FC = () => {
const handleCompleteChore = async (id: number) => {
try {
await choreService.completeChore(id);
<<<<<<< HEAD
await loadData();
=======
await loadChores();
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
} catch (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?')) {
try {
await choreService.deleteChore(id);
<<<<<<< HEAD
await loadData();
=======
await loadChores();
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
} catch (error) {
console.error('Failed to delete chore:', error);
}
}
};
<<<<<<< HEAD
const handleEditChore = (id: number) => {
setEditingChoreId(id);
};
@@ -80,10 +109,20 @@ const Dashboard: React.FC = () => {
if (filter === 'today') {
const today = new Date().toISOString().split('T')[0];
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;
});
<<<<<<< HEAD
// Calculate stats
const todayChores = chores.filter((chore) => {
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 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 (
<div className="min-h-screen bg-gray-50">
@@ -113,6 +160,7 @@ const Dashboard: React.FC = () => {
</div>
<div className="flex items-center gap-3">
<Link
<<<<<<< HEAD
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"
>
@@ -131,6 +179,8 @@ const Dashboard: React.FC = () => {
My Stats
</Link>
<Link
=======
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
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"
>
@@ -153,7 +203,11 @@ const Dashboard: React.FC = () => {
{/* Main Content */}
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{/* 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-3 gap-6 mb-8">
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
<div className="bg-white rounded-lg shadow p-6">
<div className="flex items-center justify-between">
<div>
@@ -185,6 +239,7 @@ const Dashboard: React.FC = () => {
<div className="bg-white rounded-lg shadow p-6">
<div className="flex items-center justify-between">
<div>
<<<<<<< HEAD
<p className="text-sm text-gray-600">My Points</p>
<p className="text-3xl font-bold text-amber-600">{myPoints}</p>
</div>
@@ -199,6 +254,10 @@ const Dashboard: React.FC = () => {
<div>
<p className="text-sm text-gray-600">Total Available</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 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">
@@ -209,6 +268,7 @@ const Dashboard: React.FC = () => {
</div>
</div>
<<<<<<< HEAD
{/* 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-wrap gap-2">
@@ -216,6 +276,15 @@ const Dashboard: React.FC = () => {
onClick={() => { setFilter('all'); setSelectedUserId(null); }}
className={`px-4 py-2 rounded-lg transition-colors ${
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-white text-gray-700 hover:bg-gray-50'
}`}
@@ -223,7 +292,11 @@ const Dashboard: React.FC = () => {
All Tasks
</button>
<button
<<<<<<< HEAD
onClick={() => { setFilter('today'); setSelectedUserId(null); }}
=======
onClick={() => setFilter('today')}
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
className={`px-4 py-2 rounded-lg transition-colors ${
filter === 'today'
? 'bg-blue-600 text-white'
@@ -233,7 +306,11 @@ const Dashboard: React.FC = () => {
Today
</button>
<button
<<<<<<< HEAD
onClick={() => { setFilter('my'); setSelectedUserId(null); }}
=======
onClick={() => setFilter('my')}
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
className={`px-4 py-2 rounded-lg transition-colors ${
filter === 'my'
? 'bg-blue-600 text-white'
@@ -242,6 +319,7 @@ const Dashboard: React.FC = () => {
>
My Tasks
</button>
<<<<<<< HEAD
{/* User Filter Dropdown */}
<select
@@ -270,11 +348,17 @@ const Dashboard: React.FC = () => {
<span>🎂</span>
<span>Hide Birthday Chores</span>
</button>
=======
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
</div>
<button
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"
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
>
<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" />
@@ -283,6 +367,7 @@ const Dashboard: React.FC = () => {
</button>
</div>
<<<<<<< HEAD
{/* Active Filters Display */}
{(selectedUserId || hideBirthdayChores) && (
<div className="mb-4 flex flex-wrap gap-2">
@@ -315,17 +400,24 @@ const Dashboard: React.FC = () => {
</div>
)}
=======
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
{/* Chores List */}
{isLoading ? (
<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>
<<<<<<< HEAD
<p className="mt-4 text-gray-600">Loading chores...</p>
=======
<p className="mt-4 text-gray-600">Loading tasks...</p>
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
</div>
) : filteredChores.length === 0 ? (
<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">
<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>
<<<<<<< HEAD
<h3 className="mt-2 text-sm font-medium text-gray-900">No chores found</h3>
<p className="mt-1 text-sm text-gray-500">
{selectedUserId
@@ -334,6 +426,10 @@ const Dashboard: React.FC = () => {
? "All chores are birthday chores today! 🎂"
: "Get started by creating a new chore."}
</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 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}
onComplete={handleCompleteChore}
onDelete={handleDeleteChore}
<<<<<<< HEAD
onEdit={handleEditChore}
=======
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
/>
))}
</div>
)}
</main>
<<<<<<< HEAD
{/* Modals */}
=======
{/* Create Chore Modal */}
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
{showCreateModal && (
<CreateChoreModal
onClose={() => setShowCreateModal(false)}
onSuccess={() => {
setShowCreateModal(false);
<<<<<<< HEAD
loadData();
}}
/>
@@ -368,6 +472,9 @@ const Dashboard: React.FC = () => {
onSuccess={() => {
setEditingChoreId(null);
loadData();
=======
loadChores();
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
}}
/>
)}

View File

@@ -1,8 +1,12 @@
import React, { useState, useEffect } from 'react';
import { useAuth } from '../contexts/AuthContext';
<<<<<<< HEAD
import api, { API_BASE_URL } from '../api/axios';
import AvatarUpload from '../components/AvatarUpload';
import { getUserColor, getInitials } from '../utils/avatarUtils';
=======
import api from '../api/axios';
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
interface UserProfile {
id: number;
@@ -11,10 +15,14 @@ interface UserProfile {
full_name: string;
discord_id?: string;
profile_picture?: string;
<<<<<<< HEAD
avatar_url?: string;
birthday?: string;
is_admin: boolean;
is_active: boolean;
=======
is_admin: boolean;
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
}
interface UpdateProfileData {
@@ -22,6 +30,7 @@ interface UpdateProfileData {
full_name?: string;
discord_id?: string;
profile_picture?: string;
<<<<<<< HEAD
birthday?: string;
password?: string;
}
@@ -34,6 +43,13 @@ interface AdminUpdateData extends UpdateProfileData {
const Settings: React.FC = () => {
const { user } = useAuth();
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 [formData, setFormData] = useState<UpdateProfileData>({});
const [confirmPassword, setConfirmPassword] = useState('');
@@ -41,8 +57,12 @@ const Settings: React.FC = () => {
const [error, setError] = useState('');
const [success, setSuccess] = useState('');
const [allUsers, setAllUsers] = useState<UserProfile[]>([]);
<<<<<<< HEAD
const [selectedUser, setSelectedUser] = useState<UserProfile | null>(null);
const [editFormData, setEditFormData] = useState<AdminUpdateData>({});
=======
const [selectedUserId, setSelectedUserId] = useState<number | null>(null);
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
useEffect(() => {
loadProfile();
@@ -60,7 +80,10 @@ const Settings: React.FC = () => {
full_name: response.data.full_name,
discord_id: response.data.discord_id || '',
profile_picture: response.data.profile_picture || '',
<<<<<<< HEAD
birthday: response.data.birthday || '',
=======
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
});
} catch (err) {
console.error('Failed to load profile:', err);
@@ -81,11 +104,22 @@ const Settings: React.FC = () => {
e.preventDefault();
setError('');
setSuccess('');
<<<<<<< HEAD
=======
// Validate passwords match if changing password
if (formData.password && formData.password !== confirmPassword) {
setError('Passwords do not match');
return;
}
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
setIsLoading(true);
try {
const updateData: UpdateProfileData = {};
<<<<<<< HEAD
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;
@@ -93,6 +127,19 @@ const Settings: React.FC = () => {
await api.put('/api/v1/auth/me', updateData);
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();
} catch (err: any) {
setError(err.response?.data?.detail || 'Failed to update profile');
@@ -101,6 +148,7 @@ const Settings: React.FC = () => {
}
};
<<<<<<< HEAD
const handlePasswordChange = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
@@ -130,6 +178,12 @@ const Settings: React.FC = () => {
await api.put(`/api/v1/auth/users/${userId}`, updateData);
setSuccess('User updated successfully!');
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();
} catch (err: any) {
setError(err.response?.data?.detail || 'Failed to update user');
@@ -141,6 +195,7 @@ const Settings: React.FC = () => {
setFormData(prev => ({ ...prev, [name]: value }));
};
<<<<<<< HEAD
const openEditModal = (u: UserProfile) => {
setSelectedUser(u);
setEditFormData({
@@ -168,11 +223,14 @@ const Settings: React.FC = () => {
}
};
=======
>>>>>>> 65c71b3d67d462fe9ecc01a1c2aa17e54b626fe2
if (!profile) {
return <div className="text-center py-8">Loading...</div>;
}
return (
<<<<<<< HEAD
<div className="max-w-6xl mx-auto p-6">
<h1 className="text-3xl font-bold text-gray-900 mb-8">Settings</h1>
@@ -355,6 +413,103 @@ const Settings: React.FC = () => {
{/* Password Tab */}
{activeTab === 'password' && (
<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>
<label htmlFor="password" className="block text-sm font-medium text-gray-700 mb-2">
New Password
@@ -365,9 +520,14 @@ const Settings: React.FC = () => {
type="password"
value={formData.password || ''}
onChange={handleChange}
<<<<<<< HEAD
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"
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>
@@ -383,6 +543,7 @@ const Settings: React.FC = () => {
onChange={(e) => setConfirmPassword(e.target.value)}
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"
<<<<<<< HEAD
required
/>
</div>
@@ -615,6 +776,67 @@ const Settings: React.FC = () => {
</div>
</form>
</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>
)}