Phase 3.1: Add remaining local files
This commit is contained in:
@@ -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 />} />
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
@@ -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
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user