diff --git a/backend/time_tracking/migrations/0001_initial.py b/backend/time_tracking/migrations/0001_initial.py deleted file mode 100644 index 9a94ba0..0000000 --- a/backend/time_tracking/migrations/0001_initial.py +++ /dev/null @@ -1,33 +0,0 @@ -# Generated by Django 5.2.17 on 2026-09-12 17:26 - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='TimeEntry', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('date', models.DateField()), - ('start_time', models.TimeField()), - ('end_time', models.TimeField()), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='time_entries', to=settings.AUTH_USER_MODEL)), - ], - options={ - 'ordering': ['-date', '-start_time'], - 'unique_together': {('user', 'date', 'start_time')}, - }, - ), - ] diff --git a/backend/time_tracking/migrations/0002_allow_incomplete_time_entries.py b/backend/time_tracking/migrations/0002_allow_incomplete_time_entries.py deleted file mode 100644 index 7ab74f5..0000000 --- a/backend/time_tracking/migrations/0002_allow_incomplete_time_entries.py +++ /dev/null @@ -1,20 +0,0 @@ -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ('time_tracking', '0001_initial'), - ] - - operations = [ - migrations.AlterField( - model_name='timeentry', - name='start_time', - field=models.TimeField(blank=True, null=True), - ), - migrations.AlterField( - model_name='timeentry', - name='end_time', - field=models.TimeField(blank=True, null=True), - ), - ] \ No newline at end of file diff --git a/backend/time_tracking/urls.py b/backend/time_tracking/urls.py index 42ac4d2..83e7ac4 100644 --- a/backend/time_tracking/urls.py +++ b/backend/time_tracking/urls.py @@ -6,7 +6,6 @@ from .views import ( TimeEntryDetailView, TimeEntryUpdateView, WeeklySummaryView, - UserWeeklySummaryView ) urlpatterns = [ @@ -15,6 +14,6 @@ urlpatterns = [ path('entries/scan/', TimeEntryScanView.as_view(), name='time-entry-scan'), path('entries//', TimeEntryDetailView.as_view(), name='time-entry-detail'), path('entries//update/', TimeEntryUpdateView.as_view(), name='time-entry-update'), - path('weekly-summary/', UserWeeklySummaryView.as_view(), name='user-weekly-summary'), + path('weekly-summary/', WeeklySummaryView.as_view(), name='user-weekly-summary'), path('weekly-summary//', WeeklySummaryView.as_view(), name='weekly-summary'), ] \ No newline at end of file diff --git a/backend/time_tracking/views.py b/backend/time_tracking/views.py index 71d051c..66ada01 100644 --- a/backend/time_tracking/views.py +++ b/backend/time_tracking/views.py @@ -5,7 +5,7 @@ from rest_framework.views import APIView from django.contrib.auth.models import User from django.db import transaction from django.utils import timezone -from datetime import datetime, date, timedelta +from datetime import timedelta from datetime import time as datetime_time from decouple import config from .models import TimeEntry @@ -179,124 +179,48 @@ class TimeEntryUpdateView(APIView): status=status.HTTP_404_NOT_FOUND ) +def build_weekly_summary(user): + today = timezone.localdate() + start_of_week = today - timedelta(days=today.weekday()) + end_of_week = start_of_week + timedelta(days=6) + entries = list(TimeEntry.objects.filter( + user=user, + date__range=(start_of_week, end_of_week), + ).order_by('date')) + entries_by_date = {} + for entry in entries: + entries_by_date.setdefault(entry.date, entry) + + days_data = [] + for day_offset in range(7): + current_day = start_of_week + timedelta(days=day_offset) + entry = entries_by_date.get(current_day) + days_data.append({ + 'date': current_day.isoformat(), + 'day': current_day.strftime('%A'), + 'start_time': str(entry.start_time) if entry and entry.start_time else None, + 'end_time': str(entry.end_time) if entry and entry.end_time else None, + 'duration': entry.duration if entry else 0, + }) + + return { + 'user': user.username, + 'username': user.username, + 'total_hours': round(sum(entry.duration or 0 for entry in entries), 2), + 'days': days_data, + } + + class WeeklySummaryView(APIView): permission_classes = [IsAuthenticated] def get(self, request, user_id=None): - # If user_id is provided and user is staff, get that user's summary - # Otherwise, get current user's summary + user = request.user if user_id and (request.user.is_staff or request.user.is_superuser): try: user = User.objects.get(id=user_id) except User.DoesNotExist: - return Response( - {'error': 'Utilisateur non trouvé'}, - status=status.HTTP_404_NOT_FOUND - ) - else: - user = request.user + return Response({'error': 'Utilisateur non trouvé'}, status=status.HTTP_404_NOT_FOUND) - # Get current week (Monday to Sunday) - today = timezone.now().date() - start_of_week = today - timedelta(days=today.weekday()) - end_of_week = start_of_week + timedelta(days=6) - - # Get all time entries for the current week for this user - entries = TimeEntry.objects.filter( - user=user, - date__gte=start_of_week, - date__lte=end_of_week - ).order_by('date') - - # Group entries by day - days_data = [] - for i in range(7): - current_day = start_of_week + timedelta(days=i) - day_entries = entries.filter(date=current_day) - - if day_entries.exists(): - # Get the first entry for this day (assuming one entry per day) - entry = day_entries.first() - days_data.append({ - 'date': entry.date.isoformat(), - 'day': entry.date.strftime('%A'), - 'start_time': str(entry.start_time) if entry.start_time else None, - 'end_time': str(entry.end_time) if entry.end_time else None, - 'duration': entry.duration - }) - else: - days_data.append({ - 'date': current_day.isoformat(), - 'day': current_day.strftime('%A'), - 'start_time': None, - 'end_time': None, - 'duration': 0 - }) - - total_hours = sum(entry.duration or 0 for entry in entries) - - summary = { - 'user': user.username, - 'username': user.username, - 'total_hours': round(total_hours, 2), - 'days': days_data - } - - serializer = WeeklySummarySerializer(summary) - return Response(serializer.data) - -class UserWeeklySummaryView(APIView): - permission_classes = [IsAuthenticated] - - def get(self, request): - # Get current user's weekly summary - user = request.user - - # Get current week (Monday to Sunday) - today = timezone.now().date() - start_of_week = today - timedelta(days=today.weekday()) - end_of_week = start_of_week + timedelta(days=6) - - # Get all time entries for the current week for this user - entries = TimeEntry.objects.filter( - user=user, - date__gte=start_of_week, - date__lte=end_of_week - ).order_by('date') - - # Group entries by day - days_data = [] - for i in range(7): - current_day = start_of_week + timedelta(days=i) - day_entries = entries.filter(date=current_day) - - if day_entries.exists(): - # Get the first entry for this day (assuming one entry per day) - entry = day_entries.first() - days_data.append({ - 'date': entry.date.isoformat(), - 'day': entry.date.strftime('%A'), - 'start_time': str(entry.start_time) if entry.start_time else None, - 'end_time': str(entry.end_time) if entry.end_time else None, - 'duration': entry.duration - }) - else: - days_data.append({ - 'date': current_day.isoformat(), - 'day': current_day.strftime('%A'), - 'start_time': None, - 'end_time': None, - 'duration': 0 - }) - - total_hours = sum(entry.duration or 0 for entry in entries) - - summary = { - 'user': user.username, - 'username': user.username, - 'total_hours': round(total_hours, 2), - 'days': days_data - } - - serializer = WeeklySummarySerializer(summary) + serializer = WeeklySummarySerializer(build_weekly_summary(user)) return Response(serializer.data) diff --git a/backend/users/migrations/0001_invitation.py b/backend/users/migrations/0001_invitation.py deleted file mode 100644 index afecb80..0000000 --- a/backend/users/migrations/0001_invitation.py +++ /dev/null @@ -1,26 +0,0 @@ -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion -import uuid - - -class Migration(migrations.Migration): - initial = True - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='Invitation', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('token', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('expires_at', models.DateTimeField()), - ('used_at', models.DateTimeField(blank=True, null=True)), - ('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='created_invitations', to=settings.AUTH_USER_MODEL)), - ], - ), - ] diff --git a/frontend/src/api/users.js b/frontend/src/api/users.js index c507e54..9af5e17 100644 --- a/frontend/src/api/users.js +++ b/frontend/src/api/users.js @@ -11,36 +11,6 @@ export const userApi = { } }, - // Get a specific user by ID - getUserById: async (id) => { - try { - const response = await api.get(`/users/${id}/`); - return response.data; - } catch (error) { - throw new Error('Failed to fetch user: ' + error.message); - } - }, - - // Create a new user - createUser: async (userData) => { - try { - const response = await api.post('/users/', userData); - return response.data; - } catch (error) { - throw new Error('Failed to create user: ' + error.message); - } - }, - - // Update a user - updateUser: async (id, userData) => { - try { - const response = await api.put(`/users/${id}/`, userData); - return response.data; - } catch (error) { - throw new Error('Failed to update user: ' + error.message); - } - }, - // Delete a user deleteUser: async (id) => { try { @@ -62,13 +32,12 @@ export const userApi = { } }, - // Get user roles - getUserRoles: async () => { + createInvitation: async () => { try { - const response = await api.get('/users/roles/'); + const response = await api.post('/users/invitations/'); return response.data; } catch (error) { - throw new Error('Failed to fetch user roles: ' + error.message); + throw new Error('Failed to create invitation: ' + error.message); } }, }; diff --git a/frontend/src/components/AdminRoute.jsx b/frontend/src/components/AdminRoute.jsx index 82b0cf3..423dfb3 100644 --- a/frontend/src/components/AdminRoute.jsx +++ b/frontend/src/components/AdminRoute.jsx @@ -1,6 +1,6 @@ import React from 'react'; import { Navigate, useLocation } from 'react-router-dom'; -import { useAuth } from '../context/AuthContext'; +import { useAuth } from '../context/useAuth'; const AdminRoute = ({ children }) => { const { user, loading, isAdmin } = useAuth(); diff --git a/frontend/src/components/AttendanceApp.css b/frontend/src/components/AttendanceApp.css deleted file mode 100644 index 831aad8..0000000 --- a/frontend/src/components/AttendanceApp.css +++ /dev/null @@ -1,69 +0,0 @@ -.attendance-app { - min-height: 100vh; - background-color: #f0f2f5; -} - -.app-header { - background: #007bff; - color: white; - padding: 1rem 2rem; - box-shadow: 0 2px 4px rgba(0,0,0,0.1); - display: flex; - justify-content: space-between; - align-items: center; - flex-wrap: wrap; - gap: 15px; -} - -.app-header h1 { - margin: 0; - font-size: 1.8em; -} - -.app-nav { - display: flex; - gap: 10px; -} - -.nav-button { - background: rgba(255, 255, 255, 0.2); - color: white; - border: none; - padding: 10px 20px; - border-radius: 4px; - cursor: pointer; - transition: background 0.3s; - font-size: 1em; -} - -.nav-button:hover { - background: rgba(255, 255, 255, 0.3); -} - -.nav-button.active { - background: white; - color: #007bff; - font-weight: bold; -} - -.app-main { - padding: 20px; -} - -@media (max-width: 768px) { - .app-header { - padding: 1rem; - flex-direction: column; - align-items: stretch; - } - - .app-nav { - width: 100%; - justify-content: center; - } - - .nav-button { - flex: 1; - margin: 5px; - } -} \ No newline at end of file diff --git a/frontend/src/components/AttendanceApp.jsx b/frontend/src/components/AttendanceApp.jsx deleted file mode 100644 index ce466f8..0000000 --- a/frontend/src/components/AttendanceApp.jsx +++ /dev/null @@ -1,39 +0,0 @@ -import React, { useState } from 'react'; -import RecapTab from './RecapTab'; -import './AttendanceApp.css'; - -const AttendanceApp = () => { - const [activeTab, setActiveTab] = useState('recap'); - const [userId] = useState(1); // ID utilisateur simulé - - const renderTabContent = () => { - switch (activeTab) { - case 'recap': - return ; - default: - return ; - } - }; - - return ( -
-
-

Système de Pointage

- -
- -
- {renderTabContent()} -
-
- ); -}; - -export default AttendanceApp; \ No newline at end of file diff --git a/frontend/src/components/Header.jsx b/frontend/src/components/Header.jsx index ea95d43..08c5e4a 100644 --- a/frontend/src/components/Header.jsx +++ b/frontend/src/components/Header.jsx @@ -1,6 +1,6 @@ import React, { useEffect, useState } from 'react'; import { Link, useNavigate } from 'react-router-dom'; -import { useAuth } from '../context/AuthContext'; +import { useAuth } from '../context/useAuth'; import './Header.css'; const Header = () => { diff --git a/frontend/src/components/ProtectedRoute.jsx b/frontend/src/components/ProtectedRoute.jsx index 7b88395..bfbceff 100644 --- a/frontend/src/components/ProtectedRoute.jsx +++ b/frontend/src/components/ProtectedRoute.jsx @@ -1,6 +1,6 @@ import React from 'react'; import { Navigate, useLocation } from 'react-router-dom'; -import { useAuth } from '../context/AuthContext'; +import { useAuth } from '../context/useAuth'; const ProtectedRoute = ({ children }) => { const { user, loading } = useAuth(); diff --git a/frontend/src/components/RecapTab.css b/frontend/src/components/RecapTab.css deleted file mode 100644 index 9523ca9..0000000 --- a/frontend/src/components/RecapTab.css +++ /dev/null @@ -1,68 +0,0 @@ -.recap-tab { - padding: 20px; - background-color: #f5f5f5; - min-height: 100vh; -} - -.recap-tab.loading { - text-align: center; - padding: 40px; -} - -.user-info { - background: white; - border-radius: 8px; - box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); - padding: 20px; - margin-bottom: 30px; -} - -.user-info h1 { - color: #333; - margin-bottom: 20px; - font-size: 2em; -} - -.user-details { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); - gap: 15px; -} - -.detail-item { - display: flex; - flex-direction: column; -} - -.label { - font-weight: bold; - color: #555; - margin-bottom: 5px; - font-size: 0.9em; -} - -.value { - color: #333; - font-size: 1.1em; -} - -.schedule-section { - background: white; - border-radius: 8px; - box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); - padding: 20px; -} - -@media (max-width: 768px) { - .recap-tab { - padding: 10px; - } - - .user-info h1 { - font-size: 1.5em; - } - - .user-details { - grid-template-columns: 1fr; - } -} \ No newline at end of file diff --git a/frontend/src/components/RecapTab.jsx b/frontend/src/components/RecapTab.jsx deleted file mode 100644 index 7258d7f..0000000 --- a/frontend/src/components/RecapTab.jsx +++ /dev/null @@ -1,56 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import WeeklySchedule from './WeeklySchedule'; -import './RecapTab.css'; - -const RecapTab = ({ userId }) => { - const [user, setUser] = useState(null); - const [loading, setLoading] = useState(true); - - // Simuler le chargement des données utilisateur - useEffect(() => { - // Ici, nous utiliserions un service pour charger les données utilisateur - // Pour l'exemple, on simule avec des données statiques - const mockUser = { - id: userId, - name: 'Jean Dupont', - email: 'jean.dupont@example.com', - department: 'Développement', - position: 'Développeur' - }; - - setUser(mockUser); - setLoading(false); - }, [userId]); - - if (loading) { - return
Chargement...
; - } - - return ( -
-
-

Récapitulatif de {user.name}

-
-
- Email: - {user.email} -
-
- Département: - {user.department} -
-
- Poste: - {user.position} -
-
-
- -
- -
-
- ); -}; - -export default RecapTab; \ No newline at end of file diff --git a/frontend/src/components/TimeClockQR.css b/frontend/src/components/TimeClockQR.css deleted file mode 100644 index 50764ba..0000000 --- a/frontend/src/components/TimeClockQR.css +++ /dev/null @@ -1,303 +0,0 @@ -.time-clock-qr { - max-width: 600px; - margin: 0 auto; - padding: 20px; - background: white; - border-radius: 10px; - box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); -} - -.time-clock-qr h2 { - text-align: center; - color: #333; - margin-bottom: 30px; -} - -.scan-area { - margin-bottom: 30px; -} - -.qr-placeholder { - width: 100%; - height: 250px; - border: 2px dashed #ccc; - border-radius: 10px; - display: flex; - align-items: center; - justify-content: center; - background: #f9f9f9; - position: relative; - overflow: hidden; -} - -.qr-placeholder-content { - text-align: center; -} - -.qr-code { - width: 150px; - height: 150px; - background: #333; - margin: 0 auto 15px; - border-radius: 8px; - position: relative; - overflow: hidden; -} - -.qr-code::before { - content: ''; - position: absolute; - top: 20%; - left: 20%; - width: 60%; - height: 60%; - background: #fff; - border-radius: 4px; -} - -.qr-code::after { - content: ''; - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: linear-gradient(45deg, #333 25%, transparent 25%, transparent 75%, #333 75%), - linear-gradient(45deg, #333 25%, transparent 25%, transparent 75%, #333 75%); - background-size: 20px 20px; - background-position: 0 0, 10px 10px; -} - -.scanning { - text-align: center; -} - -.scanner-line { - width: 100%; - height: 2px; - background: #007bff; - position: absolute; - top: 50%; - animation: scan 2s infinite; -} - -@keyframes scan { - 0% { transform: translateY(-50%) translateY(-50%); } - 50% { transform: translateY(-50%) translateY(50%); } - 100% { transform: translateY(-50%) translateY(-50%); } -} - -.scan-result { - text-align: center; -} - -.result-icon { - font-size: 48px; - color: #28a745; - margin-bottom: 10px; -} - -.action-selection { - margin-bottom: 30px; -} - -.action-selection h3 { - text-align: center; - margin-bottom: 20px; - color: #333; -} - -.action-buttons { - display: flex; - justify-content: center; - gap: 20px; - flex-wrap: wrap; -} - -.action-btn { - padding: 20px 30px; - border: 2px solid #ddd; - border-radius: 8px; - background: white; - cursor: pointer; - transition: all 0.3s; - display: flex; - flex-direction: column; - align-items: center; - gap: 10px; - min-width: 120px; -} - -.action-btn:hover { - border-color: #007bff; - transform: translateY(-2px); - box-shadow: 0 4px 8px rgba(0, 123, 255, 0.1); -} - -.action-btn.selected { - border-color: #007bff; - background: #007bff; - color: white; -} - -.action-icon { - font-size: 24px; -} - -.action-text { - font-weight: 500; -} - -.scan-button { - width: 100%; - padding: 15px; - background: #007bff; - color: white; - border: none; - border-radius: 8px; - font-size: 18px; - font-weight: 500; - cursor: pointer; - transition: background 0.3s; -} - -.scan-button:hover:not(:disabled) { - background: #0056b3; -} - -.scan-button:disabled { - background: #ccc; - cursor: not-allowed; -} - -.message { - margin-top: 20px; - padding: 15px; - border-radius: 8px; - text-align: center; - font-weight: 500; -} - -.message.success { - background: #d4edda; - color: #155724; - border: 1px solid #c3e6cb; -} - -.message.error { - background: #f8d7da; - color: #721c24; - border: 1px solid #f5c6cb; -} - -.error-message { - margin-top: 15px; - padding: 10px; - background: #f8d7da; - color: #721c24; - border-radius: 5px; - text-align: center; -} - -/* Modal styles */ -.modal-overlay { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: rgba(0, 0, 0, 0.5); - display: flex; - align-items: center; - justify-content: center; - z-index: 1000; -} - -.modal-content { - background: white; - padding: 30px; - border-radius: 10px; - max-width: 400px; - width: 90%; - box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3); -} - -.modal-content h3 { - margin-top: 0; - color: #333; - text-align: center; -} - -.modal-content p { - margin-bottom: 20px; - text-align: center; - color: #666; -} - -.form-group { - margin-bottom: 20px; -} - -.form-group label { - display: block; - margin-bottom: 5px; - font-weight: 500; - color: #333; -} - -.form-group input { - width: 100%; - padding: 10px; - border: 1px solid #ddd; - border-radius: 5px; - font-size: 16px; -} - -.modal-buttons { - display: flex; - gap: 10px; - justify-content: center; -} - -.btn-cancel { - padding: 10px 20px; - background: #6c757d; - color: white; - border: none; - border-radius: 5px; - cursor: pointer; -} - -.btn-confirm { - padding: 10px 20px; - background: #28a745; - color: white; - border: none; - border-radius: 5px; - cursor: pointer; -} - -@media (max-width: 768px) { - .time-clock-qr { - margin: 10px; - padding: 15px; - } - - .action-buttons { - flex-direction: column; - align-items: center; - } - - .action-btn { - width: 100%; - max-width: 200px; - } - - .qr-placeholder { - height: 200px; - } - - .modal-content { - padding: 20px; - } -} \ No newline at end of file diff --git a/frontend/src/components/TimeClockQR.jsx b/frontend/src/components/TimeClockQR.jsx deleted file mode 100644 index 0d23522..0000000 --- a/frontend/src/components/TimeClockQR.jsx +++ /dev/null @@ -1,203 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import './TimeClockQR.css'; -import { useAuth } from '../context/AuthContext'; -import api from '../api/axios'; - -const TimeClockQR = () => { - const { user } = useAuth(); - const [scanResult, setScanResult] = useState(null); - const [isScanning, setIsScanning] = useState(false); - const [selectedAction, setSelectedAction] = useState(''); - const [entryTime, setEntryTime] = useState(''); - const [showEntryTimeModal, setShowEntryTimeModal] = useState(false); - const [loading, setLoading] = useState(false); - const [message, setMessage] = useState(''); - const [error, setError] = useState(''); - - // Simuler le scan QR (dans une vraie application, cela serait avec une librairie de scan) - const simulateQRScan = () => { - setIsScanning(true); - setMessage('Scan en cours...'); - - // Simuler un scan QR après un délai - setTimeout(() => { - const mockScanResult = { - userId: user?.id || 1, - timestamp: new Date().toISOString(), - action: selectedAction - }; - setScanResult(mockScanResult); - setIsScanning(false); - setMessage('Scan réussi !'); - - // Si c'est une sortie sans entrée, demander l'heure d'entrée - if (selectedAction === 'exit' && !hasEntryToday()) { - setShowEntryTimeModal(true); - } else { - processTimeEntry(mockScanResult); - } - }, 1500); - }; - - // Vérifier si l'utilisateur a déjà pointé une entrée aujourd'hui - const hasEntryToday = () => { - // Dans une vraie application, cela viendrait de l'API - return false; - }; - - // Traiter l'entrée de temps - const processTimeEntry = async (scanData) => { - setLoading(true); - setError(''); - - try { - const entryData = { - user: scanData.userId, - action: scanData.action, - timestamp: scanData.timestamp, - entry_time: selectedAction === 'exit' ? entryTime : null - }; - - const response = await api.post('/time/entries/', entryData); - - setMessage(`Pointage ${scanData.action} enregistré avec succès !`); - setSelectedAction(''); - setEntryTime(''); - - // Réinitialiser le scan - setTimeout(() => { - setScanResult(null); - setMessage(''); - }, 3000); - - } catch (err) { - setError('Erreur lors de l\'enregistrement du pointage'); - console.error('Erreur de pointage:', err); - } finally { - setLoading(false); - } - }; - - // Gérer la soumission du formulaire d'heure d'entrée - const handleEntryTimeSubmit = (e) => { - e.preventDefault(); - if (entryTime) { - setShowEntryTimeModal(false); - const scanData = { - userId: user?.id || 1, - timestamp: new Date().toISOString(), - action: 'exit' - }; - processTimeEntry(scanData); - } - }; - - return ( -
-
-

Pointage par QR Code

- -
-
- {isScanning ? ( -
-
-

Scan en cours...

-
- ) : scanResult ? ( -
-
✓
-

Scan réussi !

-

Action: {scanResult.action}

-
- ) : ( -
-
-

Positionnez le QR code dans le cadre

-
- )} -
-
- -
-

Sélectionnez votre action

-
- - -
-
- - - - {message && ( -
- {message} -
- )} - - {error && ( -
- {error} -
- )} -
- - {/* Modal pour saisir l'heure d'entrée */} - {showEntryTimeModal && ( -
-
-

Heure d'entrée requise

-

Vous avez indiqué une sortie sans avoir pointé l'entrée.

-
-
- - setEntryTime(e.target.value)} - required - /> -
-
- - -
-
-
-
- )} -
- ); -}; - -export default TimeClockQR; \ No newline at end of file diff --git a/frontend/src/components/WeeklySchedule.css b/frontend/src/components/WeeklySchedule.css deleted file mode 100644 index d29b501..0000000 --- a/frontend/src/components/WeeklySchedule.css +++ /dev/null @@ -1,212 +0,0 @@ -.weekly-schedule { background: #fff; border: 1px solid #e7e3dc; border-radius: 14px; padding: 24px; margin: 0; color: #2b2926; } -.weekly-schedule { - margin: 0; - padding: 24px; - color: var(--text); - background: var(--surface); - border: 1px solid var(--border); - border-radius: var(--radius); - box-shadow: var(--shadow); -} - -.weekly-schedule.loading, -.weekly-schedule.schedule-error { - padding: 40px 24px; - text-align: center; -} - -.schedule-error { - color: var(--danger); - background: #fff5f1; - border-color: #efcfc3; -} - -.schedule-header { - display: flex; - align-items: flex-end; - justify-content: space-between; - gap: 20px; - margin-bottom: 22px; -} - -.schedule-header h2 { - margin: 0; - color: var(--text); - font-size: 1.4rem; - line-height: 1.2; -} - -.week-navigation { - display: flex; - align-items: center; - justify-content: flex-end; - gap: 10px; -} - -.current-week { - min-width: 145px; - color: var(--text-muted); - font-size: .84rem; - font-weight: 700; - text-align: center; - white-space: nowrap; -} - -.nav-button { - padding: 9px 12px; - color: var(--text); - font-size: .78rem; - font-weight: 700; - background: var(--surface-muted); - border: 1px solid var(--border); - border-radius: var(--radius-sm); - cursor: pointer; - transition: background var(--transition), border-color var(--transition), transform var(--transition); -} - -.nav-button:hover { - background: var(--accent-soft); - border-color: #d9bca8; -} - -.nav-button:active { - transform: translateY(1px); -} - -.schedule-table { - display: grid; - grid-template-columns: 70px repeat(7, minmax(105px, 1fr)); - min-width: 810px; - overflow: auto; - background: var(--surface); - border: 1px solid var(--border); - border-radius: var(--radius-sm); - scrollbar-color: #cbbbae transparent; - scrollbar-width: thin; -} - -.schedule-day-header { - display: flex; - min-height: 58px; - flex-direction: column; - align-items: center; - justify-content: center; - gap: 3px; - padding: 10px 6px; - color: var(--text-muted); - font-size: .76rem; - font-weight: 700; - text-transform: capitalize; - background: var(--surface-muted); - border-right: 1px solid var(--border); - border-bottom: 2px solid #dbcfc3; -} - -.schedule-day-header:first-child { - color: #9a8a7d; - font-size: .68rem; - letter-spacing: .04em; - text-transform: uppercase; -} - -.schedule-day-header strong { - color: var(--text); - font-size: 1.15rem; - line-height: 1; -} - -.schedule-time-axis { - height: var(--schedule-height); - background: #f8f5f1; - border-right: 1px solid var(--border); -} - -.schedule-time-axis span { - display: block; - height: 64px; - padding: 7px 9px 0 0; - color: #998c81; - font-size: .68rem; - text-align: right; - transform: translateY(-8px); -} - -.schedule-day-column { - position: relative; - height: var(--schedule-height); - overflow: hidden; - background: var(--surface); - border-right: 1px solid var(--border); -} - -.schedule-day-column:last-child { - border-right: 0; -} - -.schedule-hour-line { - display: block; - height: 64px; - border-bottom: 1px solid #f0ebe6; -} - -.attendance-block { - position: absolute; - right: 5px; - left: 5px; - min-height: 28px; - padding: 7px 8px; - overflow: hidden; - color: #633c2d; - font-size: .72rem; - line-height: 1.25; - background: var(--accent-soft); - border-left: 4px solid var(--accent); - border-radius: 7px; - box-shadow: 0 3px 8px rgba(75, 45, 26, .12); -} - -.attendance-block strong, -.attendance-block span { - display: block; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.attendance-block strong { - color: #633c2d; -} - -.attendance-block span { - margin-top: 3px; - color: #8b5d48; - font-size: .65rem; -} - -@media (max-width: 820px) { - .weekly-schedule { - padding: 18px 12px; - } - - .schedule-header { - display: block; - } - - .schedule-header h2 { - margin-bottom: 16px; - } - - .week-navigation { - display: grid; - grid-template-columns: 1fr 1fr; - } - - .current-week { - grid-column: 1 / -1; - grid-row: 1; - } - - .nav-button { - width: 100%; - } -} \ No newline at end of file diff --git a/frontend/src/context/AuthContext.jsx b/frontend/src/context/AuthContext.jsx index 20858a0..03e436d 100644 --- a/frontend/src/context/AuthContext.jsx +++ b/frontend/src/context/AuthContext.jsx @@ -1,7 +1,6 @@ -import React, { createContext, useState, useEffect, useContext } from 'react'; +import { useState, useEffect } from 'react'; import api from '../api/axios'; - -const AuthContext = createContext(null); +import { AuthContext } from './authContextValue'; export const AuthProvider = ({ children }) => { const [user, setUser] = useState(null); @@ -20,7 +19,6 @@ export const AuthProvider = ({ children }) => { const userData = response.data; setUser(userData); setIsAdmin(userData.is_admin || false); - setUser(userData); } catch (error) { console.error('Failed to verify authentication', error); localStorage.removeItem('token'); @@ -64,4 +62,3 @@ export const AuthProvider = ({ children }) => { ); }; -export const useAuth = () => useContext(AuthContext); diff --git a/frontend/src/context/authContextValue.js b/frontend/src/context/authContextValue.js new file mode 100644 index 0000000..bdb84f6 --- /dev/null +++ b/frontend/src/context/authContextValue.js @@ -0,0 +1,3 @@ +import { createContext } from 'react'; + +export const AuthContext = createContext(null); \ No newline at end of file diff --git a/frontend/src/context/useAuth.js b/frontend/src/context/useAuth.js new file mode 100644 index 0000000..f497380 --- /dev/null +++ b/frontend/src/context/useAuth.js @@ -0,0 +1,4 @@ +import { useContext } from 'react'; +import { AuthContext } from './authContextValue'; + +export const useAuth = () => useContext(AuthContext); \ No newline at end of file diff --git a/frontend/src/pages/AdminInvitePage.jsx b/frontend/src/pages/AdminInvitePage.jsx deleted file mode 100644 index 3fed417..0000000 --- a/frontend/src/pages/AdminInvitePage.jsx +++ /dev/null @@ -1,75 +0,0 @@ -import React, { useState } from 'react'; -import api from '../api/axios'; - -const AdminInvitePage = () => { - const [inviteLink, setInviteLink] = useState(''); - const [isCopied, setIsCopied] = useState(false); - const [error, setError] = useState(''); - - const generateInviteLink = async () => { - try { - setError(''); - const response = await api.post('/users/invitations/'); - const newInviteLink = `${window.location.origin}/register?invite=${response.data.token}`; - setInviteLink(newInviteLink); - setIsCopied(false); - } catch (error) { - setError('Erreur lors de la génération du lien d\'invitation'); - console.error(error); - } - }; - - const copyToClipboard = () => { - if (inviteLink) { - navigator.clipboard.writeText(inviteLink).then(() => { - setIsCopied(true); - setTimeout(() => setIsCopied(false), 2000); - }).catch((error) => { - setError('Impossible de copier le lien.'); - console.error(error); - }); - } - }; - - return ( -
-

Générer un lien d'invitation

- - {error &&
{error}
} - -
- -
- - {inviteLink && ( -
-

Lien d'invitation généré :

-
- - -
-

- Partagez ce lien avec les nouveaux utilisateurs pour qu'ils puissent créer un compte. -

-
- )} -
- ); -}; - -export default AdminInvitePage; \ No newline at end of file diff --git a/frontend/src/pages/AdminUsersPage.jsx b/frontend/src/pages/AdminUsersPage.jsx index 47425f4..89406d2 100644 --- a/frontend/src/pages/AdminUsersPage.jsx +++ b/frontend/src/pages/AdminUsersPage.jsx @@ -1,5 +1,4 @@ import React, { useState, useEffect } from 'react'; -import api from '../api/axios'; import { userApi } from '../api/users'; const AdminUsersPage = () => { @@ -12,8 +11,8 @@ const AdminUsersPage = () => { const generateInviteLink = async () => { try { setError(''); - const response = await api.post('/users/invitations/'); - const newInviteLink = `${window.location.origin}/register?invite=${response.data.token}`; + const invitation = await userApi.createInvitation(); + const newInviteLink = `${window.location.origin}/register?invite=${invitation.token}`; setInviteLink(newInviteLink); setIsCopied(false); } catch (error) { @@ -38,8 +37,8 @@ const AdminUsersPage = () => { const loadUsers = async () => { try { setLoading(true); - const response = await api.get('/users/'); - setUsers(response.data); + const users = await userApi.getUsers(); + setUsers(users); } catch (requestError) { setError('Erreur lors du chargement des utilisateurs'); console.error(requestError); diff --git a/frontend/src/pages/HomePage.jsx b/frontend/src/pages/HomePage.jsx index 71ddace..abe4f46 100644 --- a/frontend/src/pages/HomePage.jsx +++ b/frontend/src/pages/HomePage.jsx @@ -1,5 +1,5 @@ import React, { useState, useEffect } from 'react'; -import { useAuth } from '../context/AuthContext'; +import { useAuth } from '../context/useAuth'; import './HomePage.css'; const HomePage = () => { diff --git a/frontend/src/pages/LoginPage.jsx b/frontend/src/pages/LoginPage.jsx index ae7f6b0..142c64a 100644 --- a/frontend/src/pages/LoginPage.jsx +++ b/frontend/src/pages/LoginPage.jsx @@ -1,6 +1,6 @@ import React, { useState } from 'react'; import { useNavigate, useLocation } from 'react-router-dom'; -import { useAuth } from '../context/AuthContext'; +import { useAuth } from '../context/useAuth'; import './LoginPage.css'; const LoginPage = () => { diff --git a/frontend/src/pages/TimeClockPage.jsx b/frontend/src/pages/TimeClockPage.jsx index d252f27..cc95e88 100644 --- a/frontend/src/pages/TimeClockPage.jsx +++ b/frontend/src/pages/TimeClockPage.jsx @@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from 'react'; import { BrowserQRCodeReader } from '@zxing/browser'; import './TimeClockPage.css'; import { timeApi } from '../api/time'; -import { useAuth } from '../context/AuthContext'; +import { useAuth } from '../context/useAuth'; const TimeClockPage = () => { const videoRef = useRef(null); @@ -11,7 +11,6 @@ const TimeClockPage = () => { const qrControlsRef = useRef(null); const { isAdmin } = useAuth(); const [isScanning, setIsScanning] = useState(false); - const [scanResult, setScanResult] = useState(''); const [scanMessage, setScanMessage] = useState(''); const [scanConflict, setScanConflict] = useState(null); const [conflictTime, setConflictTime] = useState(new Date().toTimeString().slice(0, 5)); @@ -40,17 +39,10 @@ const TimeClockPage = () => { return () => clearInterval(timer); }, []); - // Load time entries when component mounts - useEffect(() => { - loadTimeEntries(); - }, []); - const loadTimeEntries = async () => { try { - setLoading(true); const entries = await timeApi.getTimeEntries(); setTimeEntries(entries); - setError(''); } catch (err) { setError('Erreur lors du chargement des pointages'); console.error(err); @@ -59,6 +51,11 @@ const TimeClockPage = () => { } }; + // Load time entries when component mounts + useEffect(() => { + loadTimeEntries(); + }, []); + const handleAddEntry = async (e) => { e.preventDefault(); if (!newEntry.start_time || !newEntry.end_time) { @@ -172,7 +169,6 @@ const TimeClockPage = () => { }; const handleQrResult = async (code) => { - setScanResult(code); setScanConflict(null); stopScanner(); @@ -257,7 +253,6 @@ const TimeClockPage = () => { const startScanner = async () => { setError(''); - setScanResult(''); setScanMessage(''); setScanConflict(null); diff --git a/frontend/src/pages/WeeklySummaryPage.css b/frontend/src/pages/WeeklySummaryPage.css index e517404..82753c3 100644 --- a/frontend/src/pages/WeeklySummaryPage.css +++ b/frontend/src/pages/WeeklySummaryPage.css @@ -224,11 +224,10 @@ .multi-user-schedule .attendance-bar-group { position: absolute; - left: 50%; + left: 5px; display: flex; gap: 3px; overflow: visible; - transform: translateX(-50%); } .multi-user-schedule .attendance-user-bar { diff --git a/frontend/src/pages/WeeklySummaryPage.jsx b/frontend/src/pages/WeeklySummaryPage.jsx index eb1ffd3..39af15b 100644 --- a/frontend/src/pages/WeeklySummaryPage.jsx +++ b/frontend/src/pages/WeeklySummaryPage.jsx @@ -1,6 +1,6 @@ import React, { useEffect, useLayoutEffect, useRef, useState } from 'react'; import { timeApi } from '../api/time'; -import { useAuth } from '../context/AuthContext'; +import { useAuth } from '../context/useAuth'; import './WeeklySummaryPage.css'; const DEFAULT_START_HOUR = 8; @@ -45,7 +45,8 @@ const WeeklySummaryPage = ({ userId }) => { const bottomPadding = parseFloat(mainStyles.paddingBottom) || 0; const footerHeight = footer.getBoundingClientRect().height; const tableTop = scheduleTable.getBoundingClientRect().top; - const remainingHeight = window.innerHeight - tableTop - footerHeight - bottomPadding; + const tableHeaderHeight = 58; + const remainingHeight = window.innerHeight - tableTop - footerHeight - bottomPadding - tableHeaderHeight; setAvailableScheduleHeight(Math.max(180, remainingHeight)); }; @@ -199,7 +200,7 @@ const WeeklySummaryPage = ({ userId }) => { const { startHour, endHour } = getScheduleBounds(weekDays); const hourCount = endHour - startHour; const measuredHeight = Math.min(availableScheduleHeight, Math.max(180, viewportHeight - 180)); - const scheduleHeight = Math.min(520, Math.max(180, measuredHeight * 0.78)); + const scheduleHeight = Math.max(180, measuredHeight); const hourHeight = Math.min(HOUR_HEIGHT, Math.max(12, scheduleHeight / hourCount)); const hours = Array.from({ length: hourCount }, (_, index) => startHour + index); const formatHour = (hour) => `${String(hour).padStart(2, '0')}:00`; diff --git a/frontend/src/services/attendanceStorage.js b/frontend/src/services/attendanceStorage.js deleted file mode 100644 index f82c279..0000000 --- a/frontend/src/services/attendanceStorage.js +++ /dev/null @@ -1,70 +0,0 @@ -// Service pour le stockage des pointages -class AttendanceStorage { - constructor() { - this.attendances = []; - this.loadAttendances(); - } - - // Charger les pointages depuis le localStorage - loadAttendances() { - try { - const stored = localStorage.getItem('attendances'); - if (stored) { - this.attendances = JSON.parse(stored); - } - } catch (error) { - console.error('Erreur lors du chargement des pointages:', error); - this.attendances = []; - } - } - - // Sauvegarder les pointages dans le localStorage - saveAttendances() { - try { - localStorage.setItem('attendances', JSON.stringify(this.attendances)); - } catch (error) { - console.error('Erreur lors de la sauvegarde des pointages:', error); - } - } - - // Ajouter un nouveau pointage - addAttendance(attendance) { - this.attendances.push(attendance); - this.saveAttendances(); - } - - // Obtenir tous les pointages - getAllAttendances() { - return this.attendances; - } - - // Obtenir les pointages pour un utilisateur spécifique - getAttendancesByUser(userId) { - return this.attendances.filter(attendance => attendance.userId === userId); - } - - // Obtenir les pointages pour une période donnée - getAttendancesByPeriod(startDate, endDate) { - return this.attendances.filter(attendance => { - const attendanceDate = new Date(attendance.date); - return attendanceDate >= startDate && attendanceDate <= endDate; - }); - } - - // Mettre à jour un pointage - updateAttendance(id, updatedAttendance) { - const index = this.attendances.findIndex(a => a.id === id); - if (index !== -1) { - this.attendances[index] = { ...this.attendances[index], ...updatedAttendance }; - this.saveAttendances(); - } - } - - // Supprimer un pointage - deleteAttendance(id) { - this.attendances = this.attendances.filter(attendance => attendance.id !== id); - this.saveAttendances(); - } -} - -export default new AttendanceStorage(); \ No newline at end of file