mirror of
https://git.v0id.ovh/n3wt-innov/n3wt-school.git
synced 2026-01-29 07:53:23 +00:00
Merge remote-tracking branch 'origin/develop'
This commit is contained in:
4
Front-End/.babelrc
Normal file
4
Front-End/.babelrc
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"presets": ["next/babel"],
|
||||
"plugins": []
|
||||
}
|
||||
@ -1,2 +0,0 @@
|
||||
NEXT_PUBLIC_API_URL=http://localhost:8080
|
||||
NEXT_PUBLIC_USE_FAKE_DATA='false'
|
||||
3
Front-End/.eslintignore
Normal file
3
Front-End/.eslintignore
Normal file
@ -0,0 +1,3 @@
|
||||
node_modules/
|
||||
build/
|
||||
public/
|
||||
@ -1,3 +1,10 @@
|
||||
{
|
||||
"extends": "next/core-web-vitals"
|
||||
"extends": ["next", "next/core-web-vitals"],
|
||||
"rules": {
|
||||
// Ajoutez vos règles personnalisées ici
|
||||
"react/react-in-jsx-scope": "off", // Désactive l'obligation d'importer React
|
||||
"no-console": "error", // Avertissement pour les console.log
|
||||
"semi": ["error", "always"], // Exige un point-virgule à la fin des lignes
|
||||
"quotes": ["error", "single", { "avoidEscape": true }] // Exige des guillemets simples, sauf si l'on utilise des guillemets doubles à l'intérieur
|
||||
}
|
||||
}
|
||||
|
||||
3
Front-End/.prettierignore
Normal file
3
Front-End/.prettierignore
Normal file
@ -0,0 +1,3 @@
|
||||
node_modules/
|
||||
build/
|
||||
dist/
|
||||
6
Front-End/.prettierrc
Normal file
6
Front-End/.prettierrc
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "es5"
|
||||
}
|
||||
6
Front-End/.vscode/settings.json
vendored
6
Front-End/.vscode/settings.json
vendored
@ -1,6 +0,0 @@
|
||||
{
|
||||
"i18n-ally.localesPaths": [
|
||||
"messages"
|
||||
],
|
||||
"i18n-ally.keystyle": "nested"
|
||||
}
|
||||
45
Front-End/Dockerfile
Normal file
45
Front-End/Dockerfile
Normal file
@ -0,0 +1,45 @@
|
||||
# Build argument pour choisir le mode
|
||||
ARG BUILD_MODE=production
|
||||
|
||||
# Build stage pour production uniquement
|
||||
FROM node:18-alpine AS builder
|
||||
WORKDIR /app
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm ci
|
||||
COPY . .
|
||||
COPY prod.env .env
|
||||
RUN npm run build
|
||||
|
||||
# Development stage
|
||||
FROM node:18-alpine AS development
|
||||
ENV NODE_ENV=development
|
||||
WORKDIR /app
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm ci
|
||||
# Ajout de la surveillance des fichiers pour le hot reload
|
||||
ENV WATCHPACK_POLLING=true
|
||||
ENV CHOKIDAR_USEPOLLING=true
|
||||
COPY . .
|
||||
EXPOSE 3000
|
||||
CMD ["npm", "run", "dev"]
|
||||
|
||||
# Production stage
|
||||
FROM node:18-alpine AS production
|
||||
WORKDIR /app
|
||||
ENV NODE_ENV=production
|
||||
COPY --from=builder /app/.next/standalone ./
|
||||
COPY --from=builder /app/.next/static ./.next/static
|
||||
COPY --from=builder /app/messages ./messages
|
||||
COPY docker/entrypoint.sh /app/entrypoint.sh
|
||||
RUN chmod +x /app/entrypoint.sh
|
||||
RUN addgroup --system --gid 1001 nodejs
|
||||
RUN adduser --system --uid 1001 nextjs
|
||||
RUN chown 1001:1001 -R /app
|
||||
USER nextjs
|
||||
ENV HOSTNAME="0.0.0.0"
|
||||
EXPOSE 3000
|
||||
ENTRYPOINT ["/app/entrypoint.sh"]
|
||||
CMD ["node", "server.js"]
|
||||
|
||||
# Final stage selection
|
||||
FROM ${BUILD_MODE}
|
||||
28
Front-End/docker/entrypoint.sh
Normal file
28
Front-End/docker/entrypoint.sh
Normal file
@ -0,0 +1,28 @@
|
||||
#!/bin/sh
|
||||
|
||||
|
||||
# Fonction pour échapper les caractères spéciaux
|
||||
escape_value() {
|
||||
echo "$1" | sed 's/[\/&]/\\&/g'
|
||||
}
|
||||
|
||||
replace_value() {
|
||||
key=$1
|
||||
value=$2
|
||||
file=$3
|
||||
escaped_value=$(escape_value "$value")
|
||||
find . -type f -exec sed -i "s|_${key}_|${value}|g" {} \;
|
||||
}
|
||||
|
||||
# Lire les clés et valeurs depuis un fichier .env
|
||||
if [ -f .env ]; then
|
||||
while IFS='=' read -r key value; do
|
||||
# Ignorer les lignes vides et les commentaires
|
||||
[ -z "$key" ] && continue
|
||||
[ "${key#\#}" != "$key" ] && continue
|
||||
|
||||
replace_value $key $value /app/
|
||||
done < .env
|
||||
fi
|
||||
|
||||
exec "$@"
|
||||
340
Front-End/docs/api-messagerie-technique.md
Normal file
340
Front-End/docs/api-messagerie-technique.md
Normal file
@ -0,0 +1,340 @@
|
||||
# API Messagerie Instantanée - Guide Développeur
|
||||
|
||||
## Vue d'ensemble
|
||||
|
||||
Cette documentation technique présente l'implémentation du système de messagerie instantanée, incluant les APIs WebSocket et REST, l'architecture des composants React et les fonctions utilitaires.
|
||||
|
||||
## API WebSocket
|
||||
|
||||
### Connexion
|
||||
|
||||
**URL de connexion :**
|
||||
|
||||
```javascript
|
||||
// Développement
|
||||
ws://localhost:8000/ws/chat/{userId}/
|
||||
|
||||
// Production
|
||||
wss://[domaine]/ws/chat/{userId}/
|
||||
```
|
||||
|
||||
### Messages WebSocket
|
||||
|
||||
#### Messages entrants (serveur → client)
|
||||
|
||||
```javascript
|
||||
// Liste des conversations
|
||||
{
|
||||
"type": "conversations_list",
|
||||
"conversations": [...]
|
||||
}
|
||||
|
||||
// Nouveau message reçu
|
||||
{
|
||||
"type": "new_message",
|
||||
"message": {
|
||||
"id": 123,
|
||||
"conversation_id": 456,
|
||||
"sender_id": 789,
|
||||
"content": "Contenu du message",
|
||||
"timestamp": "2024-01-01T12:00:00Z"
|
||||
}
|
||||
}
|
||||
|
||||
// Utilisateur en train d'écrire
|
||||
{
|
||||
"type": "typing_start",
|
||||
"conversation_id": 456,
|
||||
"user_id": 789
|
||||
}
|
||||
|
||||
// Utilisateur a arrêté d'écrire
|
||||
{
|
||||
"type": "typing_stop",
|
||||
"conversation_id": 456,
|
||||
"user_id": 789
|
||||
}
|
||||
```
|
||||
|
||||
#### Messages sortants (client → serveur)
|
||||
|
||||
```javascript
|
||||
// Envoyer un message
|
||||
{
|
||||
"type": "chat_message",
|
||||
"conversation_id": 456,
|
||||
"message": "Contenu du message"
|
||||
}
|
||||
|
||||
// Signaler début de frappe
|
||||
{
|
||||
"type": "typing_start",
|
||||
"conversation_id": 456
|
||||
}
|
||||
|
||||
// Signaler fin de frappe
|
||||
{
|
||||
"type": "typing_stop",
|
||||
"conversation_id": 456
|
||||
}
|
||||
|
||||
// Marquer comme lu
|
||||
{
|
||||
"type": "mark_as_read",
|
||||
"conversation_id": 456
|
||||
}
|
||||
|
||||
// Rejoindre une conversation
|
||||
{
|
||||
"type": "join_conversation",
|
||||
"conversation_id": 456
|
||||
}
|
||||
```
|
||||
|
||||
## API REST
|
||||
|
||||
### Endpoints disponibles
|
||||
|
||||
```javascript
|
||||
// Récupérer les conversations
|
||||
GET /api/messagerie/conversations/{userId}/
|
||||
Response: Array<Conversation>
|
||||
|
||||
// Récupérer les messages d'une conversation
|
||||
GET /api/messagerie/messages/{conversationId}/
|
||||
Response: Array<Message>
|
||||
|
||||
// Rechercher des destinataires
|
||||
GET /api/messagerie/search/{establishmentId}/?q={query}
|
||||
Response: Array<User>
|
||||
|
||||
// Créer une conversation
|
||||
POST /api/messagerie/conversations/create/
|
||||
Body: { "participants": [userId1, userId2] }
|
||||
Response: Conversation
|
||||
|
||||
// Envoyer un email (séparé de la messagerie instantanée)
|
||||
POST /api/email/send/
|
||||
Body: { "recipients": [...], "subject": "...", "content": "..." }
|
||||
```
|
||||
|
||||
## Composants React
|
||||
|
||||
### InstantChat
|
||||
|
||||
**Props :**
|
||||
|
||||
```javascript
|
||||
{
|
||||
userProfileId: number, // ID de l'utilisateur connecté
|
||||
establishmentId: number // ID de l'établissement
|
||||
}
|
||||
```
|
||||
|
||||
**États principaux :**
|
||||
|
||||
- `conversations` : Liste des conversations
|
||||
- `selectedConversation` : Conversation active
|
||||
- `messages` : Messages de la conversation active
|
||||
- `searchQuery` : Terme de recherche
|
||||
- `searchResults` : Résultats de recherche de contacts
|
||||
|
||||
### useWebSocket Hook
|
||||
|
||||
**Paramètres :**
|
||||
|
||||
```javascript
|
||||
useWebSocket(
|
||||
userProfileId, // ID utilisateur
|
||||
onMessage, // Callback pour messages reçus
|
||||
onConnectionChange // Callback changement de connexion
|
||||
);
|
||||
```
|
||||
|
||||
**Valeurs retournées :**
|
||||
|
||||
```javascript
|
||||
{
|
||||
isConnected: boolean,
|
||||
connectionStatus: string,
|
||||
sendChatMessage: (conversationId, content) => boolean,
|
||||
sendTypingStart: (conversationId) => void,
|
||||
sendTypingStop: (conversationId) => void,
|
||||
markAsRead: (conversationId) => void,
|
||||
joinConversation: (conversationId) => void,
|
||||
reconnect: () => void
|
||||
}
|
||||
```
|
||||
|
||||
## Actions Redux/State
|
||||
|
||||
### messagerieAction.js
|
||||
|
||||
```javascript
|
||||
// Récupérer les conversations
|
||||
fetchConversations(userId): Promise<Array<Conversation>>
|
||||
|
||||
// Récupérer les messages
|
||||
fetchMessages(conversationId): Promise<Array<Message>>
|
||||
|
||||
// Rechercher des destinataires
|
||||
searchMessagerieRecipients(establishmentId, query): Promise<Array<User>>
|
||||
|
||||
// Créer une conversation
|
||||
createConversation(participants): Promise<Conversation>
|
||||
```
|
||||
|
||||
### emailAction.js
|
||||
|
||||
```javascript
|
||||
// Envoyer un email
|
||||
sendEmail(recipients, subject, content, csrfToken): Promise<Response>
|
||||
|
||||
// Rechercher des destinataires email
|
||||
searchEmailRecipients(establishmentId, query): Promise<Array<User>>
|
||||
```
|
||||
|
||||
## Modèles de Données
|
||||
|
||||
### Conversation
|
||||
|
||||
```javascript
|
||||
{
|
||||
conversation_id: number,
|
||||
participants: Array<User>,
|
||||
last_message: Message,
|
||||
created_at: string,
|
||||
updated_at: string
|
||||
}
|
||||
```
|
||||
|
||||
### Message
|
||||
|
||||
```javascript
|
||||
{
|
||||
id: number,
|
||||
conversation_id: number,
|
||||
sender_id: number,
|
||||
content: string,
|
||||
timestamp: string,
|
||||
is_read: boolean
|
||||
}
|
||||
```
|
||||
|
||||
### User
|
||||
|
||||
```javascript
|
||||
{
|
||||
id: number,
|
||||
first_name: string,
|
||||
last_name: string,
|
||||
email: string,
|
||||
role: string
|
||||
}
|
||||
```
|
||||
|
||||
## Gestion des Erreurs
|
||||
|
||||
### WebSocket
|
||||
|
||||
```javascript
|
||||
// Reconnexion automatique
|
||||
const reconnectWebSocket = () => {
|
||||
setConnectionStatus('reconnecting');
|
||||
// Logique de reconnexion avec backoff exponentiel
|
||||
};
|
||||
|
||||
// Gestion des erreurs de connexion
|
||||
wsRef.current.onerror = (error) => {
|
||||
logger.error('Erreur WebSocket:', error);
|
||||
setIsConnected(false);
|
||||
};
|
||||
```
|
||||
|
||||
### API REST
|
||||
|
||||
```javascript
|
||||
// Wrapper avec gestion d'erreur
|
||||
const apiCall = async (url, options) => {
|
||||
try {
|
||||
const response = await fetch(url, options);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Erreur ${response.status}`);
|
||||
}
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
logger.error('Erreur API:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## Configuration des Tests
|
||||
|
||||
### Jest Setup
|
||||
|
||||
```javascript
|
||||
// jest.setup.js
|
||||
global.WebSocket = class MockWebSocket {
|
||||
// Mock complet du WebSocket pour les tests
|
||||
};
|
||||
|
||||
global.fetch = jest.fn(() =>
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve([]),
|
||||
})
|
||||
);
|
||||
```
|
||||
|
||||
### Tests des Composants
|
||||
|
||||
```javascript
|
||||
// Exemple de test
|
||||
test('renders InstantChat component', async () => {
|
||||
await act(async () => {
|
||||
render(<InstantChat userProfileId={1} establishmentId={123} />);
|
||||
});
|
||||
|
||||
expect(screen.getByText('Messages')).toBeInTheDocument();
|
||||
});
|
||||
```
|
||||
|
||||
## Intégration Backend
|
||||
|
||||
### Consumer Django
|
||||
|
||||
```python
|
||||
# consumers.py
|
||||
class ChatConsumer(AsyncWebsocketConsumer):
|
||||
async def connect(self):
|
||||
# Logique de connexion
|
||||
|
||||
async def chat_message(self, event):
|
||||
# Traitement des messages
|
||||
```
|
||||
|
||||
### URLs Configuration
|
||||
|
||||
```python
|
||||
# routing.py
|
||||
websocket_urlpatterns = [
|
||||
re_path(r'ws/chat/(?P<user_id>\w+)/$', ChatConsumer.as_asgi()),
|
||||
]
|
||||
```
|
||||
|
||||
## Optimisations
|
||||
|
||||
### Performance
|
||||
|
||||
- Pagination des messages anciens (load on scroll)
|
||||
- Debounce pour la recherche de contacts (300ms)
|
||||
- Memoization des composants avec React.memo
|
||||
- Lazy loading des conversations
|
||||
|
||||
### UX
|
||||
|
||||
- Reconnexion automatique avec feedback visuel
|
||||
- Sauvegarde locale des messages en cours de frappe
|
||||
- Indicateurs de livraison des messages
|
||||
- Scrolling automatique vers les nouveaux messages
|
||||
126
Front-End/docs/messagerie-instantanee.md
Normal file
126
Front-End/docs/messagerie-instantanee.md
Normal file
@ -0,0 +1,126 @@
|
||||
# Système de Messagerie Instantanée
|
||||
|
||||
## Présentation
|
||||
|
||||
Le système de messagerie instantanée de N3WT-SCHOOL permet aux utilisateurs de l'établissement (administrateurs, professeurs, parents, étudiants) de communiquer en temps réel via une interface chat moderne et intuitive.
|
||||
|
||||
## Fonctionnalités
|
||||
|
||||
### Chat en Temps Réel
|
||||
|
||||
- Envoi et réception de messages instantanés
|
||||
- Notification de statut de frappe (utilisateur en train d'écrire)
|
||||
- Indicateur de statut de connexion WebSocket
|
||||
- Reconnexion automatique en cas de perte de connexion
|
||||
|
||||
### Gestion des Conversations
|
||||
|
||||
- Liste des conversations existantes
|
||||
- Création de nouvelles conversations
|
||||
- Recherche de destinataires par nom ou email
|
||||
- Compteur de messages non lus
|
||||
|
||||
### Interface Utilisateur
|
||||
|
||||
- Interface moderne en deux panneaux (conversations + chat)
|
||||
- Bulles de messages différenciées (expéditeur/destinataire)
|
||||
- Indicateurs visuels de statut de connexion
|
||||
- Recherche temps réel de contacts
|
||||
|
||||
## Utilisation
|
||||
|
||||
### Accès au Chat
|
||||
|
||||
Le système de messagerie est accessible via les pages suivantes :
|
||||
|
||||
- **Parents** : `/[locale]/parents/messagerie`
|
||||
- **Administrateurs** : Intégré dans le panneau d'administration
|
||||
|
||||
### Créer une Conversation
|
||||
|
||||
1. Cliquer sur le bouton "+" en haut à droite de la liste des conversations
|
||||
2. Rechercher un contact en tapant son nom ou email
|
||||
3. Sélectionner le destinataire dans les résultats
|
||||
4. La conversation se crée automatiquement
|
||||
|
||||
### Envoyer un Message
|
||||
|
||||
1. Sélectionner une conversation dans la liste de gauche
|
||||
2. Taper le message dans le champ de saisie en bas
|
||||
3. Appuyer sur Entrée ou cliquer sur le bouton d'envoi
|
||||
|
||||
## Architecture Technique
|
||||
|
||||
### Frontend (React/Next.js)
|
||||
|
||||
**Composants principaux :**
|
||||
|
||||
- `InstantChat` : Composant principal du chat
|
||||
- `ConnectionStatus` : Affichage du statut de connexion
|
||||
- `ConversationItem` : Élément de liste de conversation
|
||||
- `MessageBubble` : Bulle de message individuelle
|
||||
- `MessageInput` : Zone de saisie de message
|
||||
- `TypingIndicator` : Indicateur de frappe
|
||||
|
||||
**Hook personnalisé :**
|
||||
|
||||
- `useWebSocket` : Gestion de la connexion WebSocket et des événements
|
||||
|
||||
### Backend (Django)
|
||||
|
||||
**Module GestionMessagerie :**
|
||||
|
||||
- `consumers.py` : Consumer WebSocket pour la messagerie temps réel
|
||||
- `routing.py` : Configuration des routes WebSocket
|
||||
- `urls.py` : URLs API REST pour les conversations et messages
|
||||
|
||||
**Module GestionEmail :**
|
||||
|
||||
- `views.py` : Vues pour l'envoi d'emails classiques
|
||||
- `urls.py` : URLs pour les fonctions email
|
||||
|
||||
### Communication
|
||||
|
||||
- **WebSocket** : Communication bidirectionnelle temps réel
|
||||
- **REST API** : Chargement initial des données et recherche
|
||||
- **Channels** : Gestion des groupes de conversation Django
|
||||
|
||||
## Configuration
|
||||
|
||||
### URLs WebSocket
|
||||
|
||||
Les URLs sont configurées automatiquement selon l'environnement :
|
||||
|
||||
- **Développement** : `ws://localhost:8000/ws/chat/`
|
||||
- **Production** : `wss://[domaine]/ws/chat/`
|
||||
|
||||
### Variables d'Environnement
|
||||
|
||||
Le système utilise les configurations standard de l'application pour :
|
||||
|
||||
- Base de données (conversations, messages, utilisateurs)
|
||||
- Authentification (sessions Django)
|
||||
- Établissements (filtrage par établissement)
|
||||
|
||||
## Sécurité
|
||||
|
||||
- Authentification requise pour accéder au chat
|
||||
- Filtrage des conversations par établissement
|
||||
- Validation côté serveur de tous les messages
|
||||
- Gestion des permissions selon le rôle utilisateur
|
||||
|
||||
## Tests
|
||||
|
||||
Le système dispose de tests unitaires Jest couvrant :
|
||||
|
||||
- Rendu des composants
|
||||
- Gestion des connexions WebSocket
|
||||
- Recherche de contacts
|
||||
- Envoi de messages
|
||||
- Indicateurs de frappe
|
||||
|
||||
Exécution des tests :
|
||||
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
33
Front-End/jest.config.js
Normal file
33
Front-End/jest.config.js
Normal file
@ -0,0 +1,33 @@
|
||||
const nextJest = require('next/jest');
|
||||
|
||||
const createJestConfig = nextJest({
|
||||
// Provide the path to your Next.js app to load next.config.js and .env files
|
||||
dir: './',
|
||||
});
|
||||
|
||||
// Add any custom config to be passed to Jest
|
||||
const customJestConfig = {
|
||||
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
|
||||
testEnvironment: 'jest-environment-jsdom',
|
||||
testPathIgnorePatterns: ['<rootDir>/.next/', '<rootDir>/node_modules/'],
|
||||
moduleNameMapper: {
|
||||
'^@/(.*)$': '<rootDir>/src/$1',
|
||||
},
|
||||
collectCoverageFrom: [
|
||||
'src/**/*.{js,jsx}',
|
||||
'!src/**/*.stories.{js,jsx}',
|
||||
'!src/pages/_app.js',
|
||||
'!src/pages/_document.js',
|
||||
],
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
branches: 70,
|
||||
functions: 70,
|
||||
lines: 70,
|
||||
statements: 70,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async
|
||||
module.exports = createJestConfig(customJestConfig);
|
||||
95
Front-End/jest.setup.js
Normal file
95
Front-End/jest.setup.js
Normal file
@ -0,0 +1,95 @@
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
// Supprimer les avertissements React act() en environnement de test
|
||||
global.IS_REACT_ACT_ENVIRONMENT = true;
|
||||
|
||||
// Mock window.matchMedia
|
||||
Object.defineProperty(window, 'matchMedia', {
|
||||
writable: true,
|
||||
value: jest.fn().mockImplementation((query) => ({
|
||||
matches: false,
|
||||
media: query,
|
||||
onchange: null,
|
||||
addListener: jest.fn(), // deprecated
|
||||
removeListener: jest.fn(), // deprecated
|
||||
addEventListener: jest.fn(),
|
||||
removeEventListener: jest.fn(),
|
||||
dispatchEvent: jest.fn(),
|
||||
})),
|
||||
});
|
||||
|
||||
// Mock IntersectionObserver
|
||||
global.IntersectionObserver = class IntersectionObserver {
|
||||
constructor() {}
|
||||
observe() {
|
||||
return null;
|
||||
}
|
||||
disconnect() {
|
||||
return null;
|
||||
}
|
||||
unobserve() {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// Mock WebSocket
|
||||
global.WebSocket = class WebSocket {
|
||||
constructor(url) {
|
||||
this.url = url;
|
||||
this.readyState = WebSocket.CONNECTING;
|
||||
setTimeout(() => {
|
||||
this.readyState = WebSocket.OPEN;
|
||||
if (this.onopen) this.onopen();
|
||||
}, 10);
|
||||
}
|
||||
|
||||
send(data) {
|
||||
// Mock send
|
||||
}
|
||||
|
||||
close() {
|
||||
this.readyState = WebSocket.CLOSED;
|
||||
if (this.onclose) {
|
||||
this.onclose({
|
||||
code: 1000,
|
||||
reason: 'Normal closure',
|
||||
wasClean: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static get CONNECTING() {
|
||||
return 0;
|
||||
}
|
||||
static get OPEN() {
|
||||
return 1;
|
||||
}
|
||||
static get CLOSING() {
|
||||
return 2;
|
||||
}
|
||||
static get CLOSED() {
|
||||
return 3;
|
||||
}
|
||||
};
|
||||
|
||||
// Mock global pour fetch
|
||||
global.fetch = jest.fn(() =>
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve([]),
|
||||
})
|
||||
);
|
||||
|
||||
// Mock ResizeObserver
|
||||
global.ResizeObserver = class ResizeObserver {
|
||||
constructor() {}
|
||||
observe() {
|
||||
return null;
|
||||
}
|
||||
disconnect() {
|
||||
return null;
|
||||
}
|
||||
unobserve() {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
@ -1,4 +1,3 @@
|
||||
|
||||
{
|
||||
"responsable": "Guardian",
|
||||
"delete": "Delete",
|
||||
@ -10,4 +9,4 @@
|
||||
"profession": "Profession",
|
||||
"address": "Address",
|
||||
"add_responsible": "Add guardian"
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
{
|
||||
"dashboard": "Dashboard",
|
||||
"totalStudents": "Total Students",
|
||||
"averageInscriptionTime": "Average Registration Time",
|
||||
"reInscriptionRate": "Re-enrollment Rate",
|
||||
"structureCapacity": "Structure Capacity",
|
||||
"inscriptionTrends": "Enrollment Trends",
|
||||
"upcomingEvents": "Upcoming Events"
|
||||
}
|
||||
"dashboard": "Dashboard",
|
||||
"totalStudents": "Total Students",
|
||||
"pendingRegistrations": "Pending Registration",
|
||||
"reInscriptionRate": "Re-enrollment Rate",
|
||||
"structureCapacity": "Structure Capacity",
|
||||
"capacityRate": "Capacity Rate",
|
||||
"inscriptionTrends": "Enrollment Trends",
|
||||
"upcomingEvents": "Upcoming Events"
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"welcomeParents": "Welcome Parents",
|
||||
"pleaseLogin": "Please login to access your account",
|
||||
"loginButton": "Go to login page"
|
||||
}
|
||||
"welcomeParents": "Welcome Parents",
|
||||
"pleaseLogin": "Please login to access your account",
|
||||
"loginButton": "Go to login page"
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"page": "Page",
|
||||
"of": "of",
|
||||
"previous": "Previous",
|
||||
"next": "Next"
|
||||
}
|
||||
"page": "Page",
|
||||
"of": "of",
|
||||
"previous": "Previous",
|
||||
"next": "Next"
|
||||
}
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
{
|
||||
"dashboard": "Dashboard",
|
||||
"subscriptions": "Subscriptions",
|
||||
"structure": "Structure",
|
||||
"planning": "Schedule",
|
||||
"grades": "Grades",
|
||||
"settings": "Settings",
|
||||
"schoolAdmin": "School Administration"
|
||||
}
|
||||
"dashboard": "Dashboard",
|
||||
"subscriptions": "Subscriptions",
|
||||
"structure": "Structure",
|
||||
"directory": "Directory",
|
||||
"events": "Events",
|
||||
"educational_monitoring": "Educational Monitoring",
|
||||
"settings": "Settings",
|
||||
"schoolAdmin": "School Administration",
|
||||
"messagerie": "Messenger"
|
||||
}
|
||||
|
||||
@ -1,33 +1,35 @@
|
||||
{
|
||||
"headerBarTitle": "Administration",
|
||||
"addStudent": "New",
|
||||
"allStudents": "All Students",
|
||||
"pending": "Pending Registrations",
|
||||
"subscribed": "Subscribed",
|
||||
"archived": "Archived",
|
||||
"name": "Name",
|
||||
"class": "Class",
|
||||
"status": "Status",
|
||||
"attendance": "Attendance",
|
||||
"lastEvaluation": "Last Evaluation",
|
||||
"active": "Active",
|
||||
"pendingStatus": "Pending",
|
||||
"goodAttendance": "Good",
|
||||
"averageAttendance": "Average",
|
||||
"lowAttendance": "Poor",
|
||||
"searchStudent": "Search for a student...",
|
||||
"title": "Registration",
|
||||
"information": "Information",
|
||||
"no_records": "There are currently no registration records.",
|
||||
"add_button": "Add",
|
||||
"create_first_record": "Please click the ADD button to create your first registration record.",
|
||||
"studentName":"Student name",
|
||||
"studentFistName":"Student first name",
|
||||
"mainContactMail":"Main contact email",
|
||||
"phone":"Phone",
|
||||
"lastUpdateDate":"Last update",
|
||||
"classe":"Class",
|
||||
"registrationFileStatus":"Registration file status",
|
||||
"files":"Files",
|
||||
"subscribeFiles":"Subscribe files"
|
||||
}
|
||||
"headerBarTitle": "Administration",
|
||||
"addStudent": "New",
|
||||
"allStudents": "All Students",
|
||||
"pending": "Pending Registrations",
|
||||
"subscribed": "Subscribed",
|
||||
"archived": "Archived",
|
||||
"photo": "Photo",
|
||||
"name": "Name",
|
||||
"class": "Class",
|
||||
"status": "Status",
|
||||
"attendance": "Attendance",
|
||||
"lastEvaluation": "Last Evaluation",
|
||||
"active": "Active",
|
||||
"pendingStatus": "Pending",
|
||||
"goodAttendance": "Good",
|
||||
"averageAttendance": "Average",
|
||||
"lowAttendance": "Poor",
|
||||
"searchStudent": "Search for a student...",
|
||||
"title": "Registration",
|
||||
"information": "Information",
|
||||
"no_records": "There are currently no registration records.",
|
||||
"add_button": "Add",
|
||||
"create_first_record": "Please click the ADD button to create your first registration record.",
|
||||
"studentName": "Student name",
|
||||
"studentFistName": "Student first name",
|
||||
"mainContactMail": "Main contact email",
|
||||
"phone": "Phone",
|
||||
"lastUpdateDate": "Last update",
|
||||
"classe": "Class",
|
||||
"registrationFileStatus": "Registration file status",
|
||||
"files": "Files",
|
||||
"subscribeFiles": "Subscribe files",
|
||||
"historical": "Historical"
|
||||
}
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
|
||||
{
|
||||
"responsable": "Responsable",
|
||||
"delete": "Supprimer",
|
||||
@ -8,6 +7,5 @@
|
||||
"phone": "Téléphone",
|
||||
"birthdate": "Date de naissance",
|
||||
"profession": "Profession",
|
||||
"address": "Adresse",
|
||||
"add_responsible": "Ajouter un responsable"
|
||||
"address": "Adresse"
|
||||
}
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
{
|
||||
"dashboard": "Tableau de bord",
|
||||
"totalStudents": "Total des étudiants",
|
||||
"averageInscriptionTime": "Temps moyen d'inscription",
|
||||
"reInscriptionRate": "Taux de réinscription",
|
||||
"structureCapacity": "Remplissage de la structure",
|
||||
"inscriptionTrends": "Tendances d'inscription",
|
||||
"upcomingEvents": "Événements à venir"
|
||||
}
|
||||
"dashboard": "Tableau de bord",
|
||||
"totalStudents": "Total d'étudiants inscrits",
|
||||
"pendingRegistrations": "Inscriptions en attente",
|
||||
"reInscriptionRate": "Taux de réinscription",
|
||||
"structureCapacity": "Capacité de la structure",
|
||||
"capacityRate": "Remplissage de la structure",
|
||||
"inscriptionTrends": "Tendances d'inscription",
|
||||
"upcomingEvents": "Événements à venir"
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"welcomeParents": "Bienvenue aux parents",
|
||||
"pleaseLogin": "Veuillez vous connecter pour accéder à votre compte",
|
||||
"loginButton": "Accéder à la page de login"
|
||||
}
|
||||
"welcomeParents": "Bienvenue aux parents",
|
||||
"pleaseLogin": "Veuillez vous connecter pour accéder à votre compte",
|
||||
"loginButton": "Accéder à la page de login"
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"page": "Page",
|
||||
"of": "sur",
|
||||
"previous": "Précédent",
|
||||
"next": "Suivant"
|
||||
}
|
||||
"page": "Page",
|
||||
"of": "sur",
|
||||
"previous": "Précédent",
|
||||
"next": "Suivant"
|
||||
}
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
{
|
||||
"dashboard": "Tableau de bord",
|
||||
"subscriptions": "Inscriptions",
|
||||
"structure": "Structure",
|
||||
"planning": "Emploi du temps",
|
||||
"grades": "Notes",
|
||||
"settings": "Paramètres",
|
||||
"schoolAdmin": "Administration Scolaire"
|
||||
}
|
||||
{
|
||||
"dashboard": "Tableau de bord",
|
||||
"subscriptions": "Inscriptions",
|
||||
"structure": "Structure",
|
||||
"directory": "Annuaire",
|
||||
"events": "Evenements",
|
||||
"educational_monitoring": "Suivi pédagogique",
|
||||
"settings": "Paramètres",
|
||||
"schoolAdmin": "Administration Scolaire",
|
||||
"messagerie": "Messagerie"
|
||||
}
|
||||
|
||||
@ -1,33 +1,35 @@
|
||||
{
|
||||
"headerBarTitle":"Administration",
|
||||
"addStudent": "Nouveau",
|
||||
"allStudents": "Tous les élèves",
|
||||
"pending": "Inscriptions en attente",
|
||||
"subscribed": "Inscrits",
|
||||
"archived": "Archivés",
|
||||
"name": "Nom",
|
||||
"class": "Classe",
|
||||
"status": "Statut",
|
||||
"attendance": "Assiduité",
|
||||
"lastEvaluation": "Dernière évaluation",
|
||||
"active": "Actif",
|
||||
"pendingStatus": "En attente",
|
||||
"goodAttendance": "Bonne",
|
||||
"averageAttendance": "Moyenne",
|
||||
"lowAttendance": "Faible",
|
||||
"searchStudent": "Rechercher un élève...",
|
||||
"title": "Inscription",
|
||||
"information": "Information",
|
||||
"no_records": "Il n'y a actuellement aucun dossier d'inscription.",
|
||||
"add_button": "Ajouter",
|
||||
"create_first_record": "Veuillez cliquer sur le bouton AJOUTER pour créer votre premier dossier d'inscription.",
|
||||
"studentName":"Nom de l'élève",
|
||||
"studentFistName":"Prénom de l'élève",
|
||||
"mainContactMail":"Email de contact principal",
|
||||
"phone":"Téléphone",
|
||||
"lastUpdateDate":"Dernière mise à jour",
|
||||
"classe":"Classe",
|
||||
"registrationFileStatus":"État du dossier d'inscription",
|
||||
"files":"Fichiers",
|
||||
"subscribeFiles":"Fichiers d'inscription"
|
||||
}
|
||||
"headerBarTitle": "Administration",
|
||||
"addStudent": "Nouveau",
|
||||
"allStudents": "Tous les élèves",
|
||||
"pending": "Inscriptions en attente",
|
||||
"subscribed": "Inscrits",
|
||||
"archived": "Archivés",
|
||||
"photo": "Photo",
|
||||
"name": "Nom",
|
||||
"class": "Classe",
|
||||
"status": "Statut",
|
||||
"attendance": "Assiduité",
|
||||
"lastEvaluation": "Dernière évaluation",
|
||||
"active": "Actif",
|
||||
"pendingStatus": "En attente",
|
||||
"goodAttendance": "Bonne",
|
||||
"averageAttendance": "Moyenne",
|
||||
"lowAttendance": "Faible",
|
||||
"searchStudent": "Rechercher un élève...",
|
||||
"title": "Inscription",
|
||||
"information": "Information",
|
||||
"no_records": "Il n'y a actuellement aucun dossier d'inscription.",
|
||||
"add_button": "Ajouter",
|
||||
"create_first_record": "Veuillez cliquer sur le bouton AJOUTER pour créer votre premier dossier d'inscription.",
|
||||
"studentName": "Nom de l'élève",
|
||||
"studentFistName": "Prénom de l'élève",
|
||||
"mainContactMail": "Email de contact principal",
|
||||
"phone": "Téléphone",
|
||||
"lastUpdateDate": "Dernière mise à jour",
|
||||
"classe": "Classe",
|
||||
"registrationFileStatus": "État du dossier d'inscription",
|
||||
"files": "Fichiers",
|
||||
"subscribeFiles": "Fichiers d'inscription",
|
||||
"historical": "Historique"
|
||||
}
|
||||
|
||||
@ -1,8 +1,61 @@
|
||||
import createNextIntlPlugin from 'next-intl/plugin';
|
||||
import { createRequire } from 'module';
|
||||
const require = createRequire(import.meta.url);
|
||||
const pkg = require('./package.json');
|
||||
|
||||
const withNextIntl = createNextIntlPlugin();
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {};
|
||||
const nextConfig = {
|
||||
output: 'standalone',
|
||||
reactStrictMode: true,
|
||||
experimental: {
|
||||
instrumentationHook: true,
|
||||
},
|
||||
images: {
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: 'www.gravatar.com',
|
||||
},
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: 'api.demo.n3wtschool.com',
|
||||
},
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: 'api.prod.n3wtschool.com',
|
||||
},
|
||||
{
|
||||
protocol: 'http',
|
||||
hostname: 'localhost',
|
||||
port: '8080',
|
||||
},
|
||||
],
|
||||
},
|
||||
env: {
|
||||
NEXT_PUBLIC_APP_VERSION: pkg.version,
|
||||
NEXT_PUBLIC_API_URL:
|
||||
process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8080',
|
||||
NEXT_PUBLIC_WSAPI_URL:
|
||||
process.env.NEXT_PUBLIC_WSAPI_URL || 'ws://localhost:8080',
|
||||
NEXT_PUBLIC_USE_FAKE_DATA: process.env.NEXT_PUBLIC_USE_FAKE_DATA || 'false',
|
||||
AUTH_SECRET: process.env.AUTH_SECRET || 'false',
|
||||
NEXTAUTH_URL: process.env.NEXTAUTH_URL || 'http://localhost:3000',
|
||||
DOCUSEAL_API_KEY: process.env.DOCUSEAL_API_KEY,
|
||||
},
|
||||
async rewrites() {
|
||||
return [
|
||||
{
|
||||
source: '/api/documents/:path*',
|
||||
destination: 'https://api.docuseal.com/v1/documents/:path*',
|
||||
},
|
||||
{
|
||||
source: '/api/auth/:path*',
|
||||
destination: '/api/auth/:path*', // Exclure les routes NextAuth des réécritures de proxy
|
||||
},
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
export default withNextIntl(nextConfig);
|
||||
export default withNextIntl(nextConfig);
|
||||
|
||||
15299
Front-End/package-lock.json
generated
15299
Front-End/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,39 +1,54 @@
|
||||
{
|
||||
"name": "n3wt-school-front-end",
|
||||
"version": "0.0.1",
|
||||
"version": "0.0.2",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"check-strings": "node scripts/check-hardcoded-strings.js"
|
||||
"lint-light": "next lint --quiet",
|
||||
"check-strings": "node scripts/check-hardcoded-strings.js",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"test:coverage": "jest --coverage"
|
||||
},
|
||||
"dependencies": {
|
||||
"@docuseal/react": "^1.0.56",
|
||||
"@radix-ui/react-dialog": "^1.1.2",
|
||||
"@tailwindcss/forms": "^0.5.9",
|
||||
"date-fns": "^4.1.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"framer-motion": "^11.11.11",
|
||||
"ics": "^3.8.1",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.453.0",
|
||||
"next": "14.2.11",
|
||||
"next-auth": "^4.24.11",
|
||||
"next-intl": "^3.24.0",
|
||||
"next-logger": "^5.0.1",
|
||||
"pino": "^9.6.0",
|
||||
"react": "^18",
|
||||
"react-circular-progressbar": "^2.2.0",
|
||||
"react-cookie": "^7.2.0",
|
||||
"react-dnd": "^16.0.1",
|
||||
"react-dnd-html5-backend": "^16.0.1",
|
||||
"react-dom": "^18",
|
||||
"react-phone-number-input": "^3.4.8",
|
||||
"react-international-phone": "^4.5.0",
|
||||
"react-quill": "^2.0.0",
|
||||
"react-tooltip": "^5.28.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/parser": "^7.26.2",
|
||||
"@babel/traverse": "^7.25.9",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/user-event": "^14.4.3",
|
||||
"jest": "^29.7.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "14.2.11",
|
||||
"postcss": "^8.4.47",
|
||||
"tailwindcss": "^3.4.14"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -3,4 +3,4 @@ module.exports = {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
5
Front-End/prod.env
Normal file
5
Front-End/prod.env
Normal file
@ -0,0 +1,5 @@
|
||||
NEXT_PUBLIC_API_URL=_NEXT_PUBLIC_API_URL_
|
||||
NEXT_PUBLIC_WSAPI_URL=_NEXT_PUBLIC_WSAPI_URL_
|
||||
NEXT_PUBLIC_USE_FAKE_DATA=_NEXT_PUBLIC_USE_FAKE_DATA_
|
||||
AUTH_SECRET=_AUTH_SECRET_
|
||||
NEXTAUTH_URL=_NEXTAUTH_URL_
|
||||
1
Front-End/project.inlang/.gitignore
vendored
1
Front-End/project.inlang/.gitignore
vendored
@ -1 +0,0 @@
|
||||
cache
|
||||
@ -1 +0,0 @@
|
||||
2ff5cbbb4bc1c6d178400871dfa342ac4f0b18e9b86cb64a1110be1ec54238c1
|
||||
@ -1,12 +0,0 @@
|
||||
{
|
||||
// official schema ensures that your project file is valid
|
||||
"$schema": "https://inlang.com/schema/project-settings",
|
||||
// the "source" language tag that is used in your project
|
||||
"sourceLanguageTag": "fr",
|
||||
// all the language tags you want to support in your project
|
||||
"languageTags": ["fr", "en"],
|
||||
"modules": [
|
||||
"https://cdn.jsdelivr.net/npm/@inlang/plugin-json@4/dist/index.js"
|
||||
], // or use another storage module: https://inlang.com/c/plugins (i18next, json, inlang message format)
|
||||
"settings": {}
|
||||
}
|
||||
@ -29,7 +29,7 @@ const TAILWIND_PATTERNS = [
|
||||
// États
|
||||
/^(hover|focus|active|disabled|group|dark):/,
|
||||
// Couleurs
|
||||
/-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-[0-9]+$/
|
||||
/-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-[0-9]+$/,
|
||||
];
|
||||
|
||||
// Nouveaux patterns pour ignorer les logs et imports
|
||||
@ -58,7 +58,7 @@ function isHardcodedString(str) {
|
||||
|
||||
// Vérifier si la chaîne fait partie d'un console.log ou d'un import
|
||||
const context = str.trim();
|
||||
if (CODE_PATTERNS.some(pattern => pattern.test(context))) {
|
||||
if (CODE_PATTERNS.some((pattern) => pattern.test(context))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -68,19 +68,20 @@ function isHardcodedString(str) {
|
||||
}
|
||||
|
||||
// Vérifier si c'est une chaîne dans un import
|
||||
if (context.includes('from \'') || context.includes('from "')) {
|
||||
if (context.includes("from '") || context.includes('from "')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Vérifier si c'est une classe Tailwind
|
||||
const classes = str.split(' ');
|
||||
if (classes.some(cls =>
|
||||
TAILWIND_PATTERNS.some(pattern => pattern.test(cls))
|
||||
)) {
|
||||
if (
|
||||
classes.some((cls) =>
|
||||
TAILWIND_PATTERNS.some((pattern) => pattern.test(cls))
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// Autres patterns à ignorer
|
||||
const IGNORE_PATTERNS = [
|
||||
/^[A-Z][A-Za-z]+$/, // Noms de composants
|
||||
@ -95,7 +96,7 @@ function isHardcodedString(str) {
|
||||
/^className=/, // className attributes
|
||||
];
|
||||
|
||||
return !IGNORE_PATTERNS.some(pattern => pattern.test(str));
|
||||
return !IGNORE_PATTERNS.some((pattern) => pattern.test(str));
|
||||
}
|
||||
|
||||
async function scanFile(filePath) {
|
||||
@ -106,7 +107,7 @@ async function scanFile(filePath) {
|
||||
const ast = babel.parse(content, {
|
||||
sourceType: 'module',
|
||||
plugins: ['jsx', 'typescript'],
|
||||
locations: true // Active le tracking des positions
|
||||
locations: true, // Active le tracking des positions
|
||||
});
|
||||
|
||||
traverse(ast, {
|
||||
@ -140,7 +141,11 @@ async function scanDirectory(dir) {
|
||||
const filePath = path.join(dir, file);
|
||||
const stat = fs.statSync(filePath);
|
||||
|
||||
if (stat.isDirectory() && !file.startsWith('.') && file !== 'node_modules') {
|
||||
if (
|
||||
stat.isDirectory() &&
|
||||
!file.startsWith('.') &&
|
||||
file !== 'node_modules'
|
||||
) {
|
||||
Object.assign(results, await scanDirectory(filePath));
|
||||
} else if (
|
||||
stat.isFile() &&
|
||||
@ -189,4 +194,4 @@ async function main() {
|
||||
await logStringsToFile(results);
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
main().catch(console.error);
|
||||
|
||||
21
Front-End/src/app/500.js
Normal file
21
Front-End/src/app/500.js
Normal file
@ -0,0 +1,21 @@
|
||||
import Link from 'next/link';
|
||||
import Logo from '../components/Logo';
|
||||
|
||||
export default function Custom500() {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen bg-emerald-500">
|
||||
<div className="text-center p-6 ">
|
||||
<Logo className="w-32 h-32 mx-auto mb-4" />
|
||||
<h2 className="text-2xl font-bold text-emerald-900 mb-4">
|
||||
500 | Erreur interne
|
||||
</h2>
|
||||
<p className="text-emerald-900 mb-4">
|
||||
Une erreur interne est survenue.
|
||||
</p>
|
||||
<Link className="text-gray-900 hover:underline" href="/">
|
||||
Retour Accueil
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,49 +0,0 @@
|
||||
'use client'
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import Table from '@/components/Table';
|
||||
import Button from '@/components/Button';
|
||||
|
||||
const columns = [
|
||||
{ name: 'Nom', transform: (row) => row.Nom },
|
||||
{ name: 'Niveau', transform: (row) => row.Niveau },
|
||||
{ name: 'Effectif', transform: (row) => row.Effectif },
|
||||
];
|
||||
|
||||
export default function Page() {
|
||||
const [classes, setClasses] = useState([]);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [totalPages, setTotalPages] = useState(1);
|
||||
|
||||
useEffect(() => {
|
||||
fetchClasses();
|
||||
}, [currentPage]);
|
||||
|
||||
const fetchClasses = async () => {
|
||||
const fakeData = {
|
||||
classes: [
|
||||
{ Nom: 'Classe A', Niveau: '1ère année', Effectif: 30 },
|
||||
{ Nom: 'Classe B', Niveau: '2ème année', Effectif: 25 },
|
||||
{ Nom: 'Classe C', Niveau: '3ème année', Effectif: 28 },
|
||||
],
|
||||
totalPages: 3
|
||||
};
|
||||
setClasses(fakeData.classes);
|
||||
setTotalPages(fakeData.totalPages);
|
||||
};
|
||||
|
||||
const handlePageChange = (page) => {
|
||||
setCurrentPage(page);
|
||||
};
|
||||
|
||||
const handleCreateClass = () => {
|
||||
console.log('Créer une nouvelle classe');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='p-8'>
|
||||
<h1 className='heading-section'>Gestion des Classes</h1>
|
||||
<Button text="Créer une nouvelle classe" onClick={handleCreateClass} primary />
|
||||
<Table data={classes} columns={columns} itemsPerPage={5} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
573
Front-End/src/app/[locale]/admin/directory/page.js
Normal file
573
Front-End/src/app/[locale]/admin/directory/page.js
Normal file
@ -0,0 +1,573 @@
|
||||
'use client';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useEstablishment } from '@/context/EstablishmentContext';
|
||||
import { PARENT_FILTER, SCHOOL_FILTER } from '@/utils/constants';
|
||||
import { Trash2, ToggleLeft, ToggleRight, Info, XCircle } from 'lucide-react';
|
||||
import Table from '@/components/Table';
|
||||
import Popup from '@/components/Popup';
|
||||
import StatusLabel from '@/components/StatusLabel';
|
||||
import SpecialityItem from '@/components/Structure/Configuration/SpecialityItem';
|
||||
import SidebarTabs from '@/components/SidebarTabs';
|
||||
import {
|
||||
fetchProfileRoles,
|
||||
updateProfileRoles,
|
||||
deleteProfileRoles,
|
||||
} from '@/app/actions/authAction';
|
||||
import { dissociateGuardian } from '@/app/actions/subscriptionAction';
|
||||
import { useCsrfToken } from '@/context/CsrfContext';
|
||||
import DjangoCSRFToken from '@/components/DjangoCSRFToken';
|
||||
import logger from '@/utils/logger';
|
||||
import AlertMessage from '@/components/AlertMessage';
|
||||
|
||||
const roleTypeToLabel = (roleType) => {
|
||||
switch (roleType) {
|
||||
case 0:
|
||||
return 'ECOLE';
|
||||
case 1:
|
||||
return 'ADMIN';
|
||||
case 2:
|
||||
return 'PARENT';
|
||||
default:
|
||||
return 'UNKNOWN';
|
||||
}
|
||||
};
|
||||
|
||||
const roleTypeToBadgeClass = (roleType) => {
|
||||
switch (roleType) {
|
||||
case 0:
|
||||
return 'bg-blue-100 text-blue-600';
|
||||
case 1:
|
||||
return 'bg-red-100 text-red-600';
|
||||
case 2:
|
||||
return 'bg-green-100 text-green-600';
|
||||
default:
|
||||
return 'bg-gray-100 text-gray-600';
|
||||
}
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
const [profileRolesDatasParent, setProfileRolesDatasParent] = useState([]);
|
||||
const [profileRolesDatasSchool, setProfileRolesDatasSchool] = useState([]);
|
||||
const [reloadFetch, setReloadFetch] = useState(false);
|
||||
|
||||
const [popupVisible, setPopupVisible] = useState(false);
|
||||
const [popupMessage, setPopupMessage] = useState('');
|
||||
const [confirmPopupVisible, setConfirmPopupVisible] = useState(false);
|
||||
const [confirmPopupMessage, setConfirmPopupMessage] = useState('');
|
||||
const [confirmPopupOnConfirm, setConfirmPopupOnConfirm] = useState(() => {});
|
||||
const [visibleTooltipId, setVisibleTooltipId] = useState(null);
|
||||
const [activeTab, setActiveTab] = useState('parent'); // Onglet actif
|
||||
const [totalProfilesParentPages, setTotalProfilesParentPages] = useState(1);
|
||||
const [totalProfilesSchoolPages, setTotalProfilesSchoolPages] = useState(1);
|
||||
const [currentProfilesParentPage, setCurrentProfilesParentPage] = useState(1);
|
||||
|
||||
const [totalProfilesParent, setTotalProfilesParent] = useState(0);
|
||||
const [totalProfilesSchool, setTotalProfilesSchool] = useState(0);
|
||||
const [currentProfilesSchoolPage, setCurrentProfilesSchoolPage] = useState(1);
|
||||
const [profileRolesParent, setProfileRolesParent] = useState([]);
|
||||
const [profileRolesSchool, setProfileRolesSchool] = useState([]);
|
||||
const itemsPerPage = 15; // Nombre d'éléments par page
|
||||
|
||||
const csrfToken = useCsrfToken();
|
||||
|
||||
const { selectedEstablishmentId } = useEstablishment();
|
||||
|
||||
const requestErrorHandler = (err) => {
|
||||
logger.error('Error fetching data:', err);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedEstablishmentId) {
|
||||
// Fetch data for profileRolesParent
|
||||
handleProfiles();
|
||||
}
|
||||
}, [
|
||||
selectedEstablishmentId,
|
||||
reloadFetch,
|
||||
currentProfilesParentPage,
|
||||
currentProfilesSchoolPage,
|
||||
]);
|
||||
|
||||
const handleProfiles = () => {
|
||||
fetchProfileRoles(
|
||||
selectedEstablishmentId,
|
||||
PARENT_FILTER,
|
||||
currentProfilesParentPage,
|
||||
itemsPerPage
|
||||
)
|
||||
.then((data) => {
|
||||
setProfileRolesDatasParent(data);
|
||||
})
|
||||
.catch(requestErrorHandler);
|
||||
|
||||
fetchProfileRoles(
|
||||
selectedEstablishmentId,
|
||||
SCHOOL_FILTER,
|
||||
currentProfilesSchoolPage,
|
||||
itemsPerPage
|
||||
)
|
||||
.then((data) => {
|
||||
setProfileRolesDatasSchool(data);
|
||||
})
|
||||
.catch(requestErrorHandler);
|
||||
setReloadFetch(false);
|
||||
};
|
||||
|
||||
const handleEdit = (profileRole) => {
|
||||
const updatedData = { ...profileRole, is_active: !profileRole.is_active };
|
||||
return updateProfileRoles(profileRole.id, updatedData, csrfToken)
|
||||
.then((data) => {
|
||||
setProfileRolesParent((prevState) =>
|
||||
prevState.map((item) => (item.id === profileRole.id ? data : item))
|
||||
);
|
||||
return data;
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error('Error editing data:', error);
|
||||
throw error;
|
||||
});
|
||||
};
|
||||
|
||||
const handleDelete = (id) => {
|
||||
return deleteProfileRoles(id, csrfToken)
|
||||
.then(() => {
|
||||
setProfileRolesParent((prevState) =>
|
||||
prevState.filter((item) => item.id !== id)
|
||||
);
|
||||
logger.debug('Profile deleted successfully:', id);
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error('Error deleting profile:', error);
|
||||
throw error;
|
||||
});
|
||||
};
|
||||
|
||||
const handleDissociate = (studentId, guardianId) => {
|
||||
return dissociateGuardian(studentId, guardianId)
|
||||
.then((response) => {
|
||||
logger.debug('Guardian dissociated successfully:', guardianId);
|
||||
|
||||
// Vérifier si le Guardian a été supprimé
|
||||
const isGuardianDeleted = response?.isGuardianDeleted;
|
||||
|
||||
// Mettre à jour le modèle profileRolesParent
|
||||
setProfileRolesParent(
|
||||
(prevState) =>
|
||||
prevState
|
||||
.map((profileRole) => {
|
||||
if (profileRole.associated_person?.id === guardianId) {
|
||||
if (isGuardianDeleted) {
|
||||
// Si le Guardian est supprimé, retirer le profileRole
|
||||
return null;
|
||||
} else {
|
||||
// Si le Guardian n'est pas supprimé, mettre à jour les élèves associés
|
||||
const updatedStudents =
|
||||
profileRole.associated_person.students.filter(
|
||||
(student) => student.id !== studentId
|
||||
);
|
||||
return {
|
||||
...profileRole,
|
||||
associated_person: {
|
||||
...profileRole.associated_person,
|
||||
students: updatedStudents, // Mettre à jour les élèves associés
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
return profileRole; // Conserver les autres profileRolesParent
|
||||
})
|
||||
.filter(Boolean) // Supprimer les entrées nulles
|
||||
);
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error('Error dissociating guardian:', error);
|
||||
throw error;
|
||||
});
|
||||
};
|
||||
|
||||
const profilesRoleParentDataHandler = (data) => {
|
||||
if (data) {
|
||||
const { profilesRoles, count, page_size } = data;
|
||||
if (profilesRoles) {
|
||||
setProfileRolesParent(profilesRoles);
|
||||
}
|
||||
const calculatedTotalPages =
|
||||
count === 0 ? count : Math.ceil(count / page_size);
|
||||
setTotalProfilesParent(count);
|
||||
setTotalProfilesParentPages(calculatedTotalPages);
|
||||
}
|
||||
};
|
||||
|
||||
const profilesRoleSchoolDataHandler = (data) => {
|
||||
if (data) {
|
||||
const { profilesRoles, count, page_size } = data;
|
||||
if (profilesRoles) {
|
||||
setProfileRolesSchool(profilesRoles);
|
||||
}
|
||||
const calculatedTotalPages =
|
||||
count === 0 ? count : Math.ceil(count / page_size);
|
||||
setTotalProfilesSchool(count);
|
||||
setTotalProfilesSchoolPages(calculatedTotalPages);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
profilesRoleParentDataHandler(profileRolesDatasParent);
|
||||
profilesRoleSchoolDataHandler(profileRolesDatasSchool);
|
||||
|
||||
if (activeTab === 'parent') {
|
||||
setTotalProfilesParentPages(
|
||||
Math.ceil(totalProfilesParent / itemsPerPage)
|
||||
);
|
||||
} else if (activeTab === 'school') {
|
||||
setTotalProfilesSchoolPages(
|
||||
Math.ceil(totalProfilesSchool / itemsPerPage)
|
||||
);
|
||||
}
|
||||
}, [profileRolesDatasParent, profileRolesDatasSchool, activeTab]);
|
||||
|
||||
const handlePageChange = (newPage) => {
|
||||
if (activeTab === 'parent') {
|
||||
setCurrentProfilesParentPage(newPage);
|
||||
} else if (activeTab === 'school') {
|
||||
setCurrentProfilesSchoolPage(newPage);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTooltipVisibility = (id) => {
|
||||
setVisibleTooltipId(id); // Définir l'ID de la ligne pour laquelle la tooltip est visible
|
||||
};
|
||||
|
||||
const handleTooltipHide = () => {
|
||||
setVisibleTooltipId(null); // Cacher toutes les tooltips
|
||||
};
|
||||
|
||||
const handleConfirmActivateProfile = (profileRole) => {
|
||||
setConfirmPopupMessage(
|
||||
`Êtes-vous sûr de vouloir ${profileRole.is_active ? 'désactiver' : 'activer'} ce profil ?`
|
||||
);
|
||||
setConfirmPopupOnConfirm(() => () => {
|
||||
handleEdit(profileRole)
|
||||
.then(() => {
|
||||
setPopupMessage(
|
||||
`Le profil a été ${profileRole.is_active ? 'désactivé' : 'activé'} avec succès.`
|
||||
);
|
||||
setPopupVisible(true);
|
||||
})
|
||||
.catch((error) => {
|
||||
setPopupMessage(
|
||||
`Erreur lors de la ${profileRole.is_active ? 'désactivation' : 'activation'} du profil.`
|
||||
);
|
||||
setPopupVisible(true);
|
||||
});
|
||||
setConfirmPopupVisible(false);
|
||||
});
|
||||
setConfirmPopupVisible(true);
|
||||
};
|
||||
|
||||
const handleConfirmDeleteProfile = (id) => {
|
||||
setConfirmPopupMessage('Êtes-vous sûr de vouloir supprimer ce profil ?');
|
||||
setConfirmPopupOnConfirm(() => () => {
|
||||
handleDelete(id)
|
||||
.then(() => {
|
||||
setPopupMessage('Le profil a été supprimé avec succès.');
|
||||
setPopupVisible(true);
|
||||
})
|
||||
.catch((error) => {
|
||||
setPopupMessage(error.message);
|
||||
setPopupVisible(true);
|
||||
});
|
||||
setConfirmPopupVisible(false);
|
||||
});
|
||||
setConfirmPopupVisible(true);
|
||||
};
|
||||
|
||||
const handleConfirmDissociateGuardian = (profileRole, student) => {
|
||||
setVisibleTooltipId(null);
|
||||
setConfirmPopupMessage(
|
||||
`Vous êtes sur le point de dissocier le responsable ${profileRole.associated_person?.guardian_name} de l'élève ${student.student_name}. Êtes-vous sûr de vouloir poursuivre cette opération ?`
|
||||
);
|
||||
setConfirmPopupOnConfirm(() => () => {
|
||||
handleDissociate(student.id, profileRole.associated_person?.id)
|
||||
.then(() => {
|
||||
setPopupMessage('Le responsable a été dissocié avec succès.');
|
||||
setPopupVisible(true);
|
||||
})
|
||||
.catch((error) => {
|
||||
setPopupMessage(error.message);
|
||||
setPopupVisible(true);
|
||||
});
|
||||
setConfirmPopupVisible(false);
|
||||
});
|
||||
setConfirmPopupVisible(true);
|
||||
};
|
||||
|
||||
const parentColumns = [
|
||||
{ name: 'Identifiant', transform: (row) => row.associated_profile_email },
|
||||
{ name: 'Mise à jour', transform: (row) => row.updated_date_formatted },
|
||||
{
|
||||
name: 'Rôle',
|
||||
transform: (row) => (
|
||||
<span
|
||||
className={`px-2 py-1 rounded-full font-bold ${roleTypeToBadgeClass(row.role_type)}`}
|
||||
>
|
||||
{roleTypeToLabel(row.role_type)}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'Utilisateur',
|
||||
transform: (row) => (
|
||||
<div className="flex items-center justify-center space-x-2 relative">
|
||||
<span>{row.associated_person?.guardian_name}</span>
|
||||
{row.associated_person && (
|
||||
<div
|
||||
className="relative group"
|
||||
onMouseEnter={() => handleTooltipVisibility(row.id)} // Afficher la tooltip pour cette ligne
|
||||
onMouseLeave={handleTooltipHide} // Cacher la tooltip
|
||||
>
|
||||
<button className="relative text-blue-500 hover:text-blue-700 flex items-center justify-center">
|
||||
<div className="w-6 h-6 bg-blue-100 text-blue-700 rounded-full flex items-center justify-center font-bold">
|
||||
{row.associated_person?.students?.length || 0}
|
||||
</div>
|
||||
</button>
|
||||
{visibleTooltipId === row.id && ( // Afficher uniquement si l'ID correspond
|
||||
<div className="fixed z-50 w-96 p-4 bg-white border border-gray-200 rounded shadow-lg -translate-x-1/2">
|
||||
<div className="mb-2">
|
||||
<strong>Elève(s) associé(s):</strong>
|
||||
<div className="flex flex-col justify-center space-y-2 mt-4">
|
||||
{row.associated_person?.students?.map((student) => (
|
||||
<div
|
||||
key={student.student_name}
|
||||
className="flex justify-between items-center"
|
||||
>
|
||||
<span className="px-2 py-1 rounded-full text-gray-800 whitespace-nowrap inline-block min-w-0 max-w-fit">
|
||||
{student.student_name}
|
||||
</span>
|
||||
<div className="flex items-center space-x-2">
|
||||
<StatusLabel
|
||||
status={student.registration_status}
|
||||
showDropdown={false}
|
||||
/>
|
||||
<button
|
||||
className="text-red-500 hover:text-red-700 flex items-center space-x-1"
|
||||
onClick={() =>
|
||||
handleConfirmDissociateGuardian(row, student)
|
||||
}
|
||||
>
|
||||
<XCircle className="w-5 h-5" />
|
||||
<span className="text-sm">Dissocier</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'Actions',
|
||||
transform: (row) => (
|
||||
<div className="flex justify-center space-x-2">
|
||||
<button
|
||||
type="button"
|
||||
className={
|
||||
row.is_active
|
||||
? 'text-emerald-500 hover:text-emerald-700'
|
||||
: 'text-orange-500 hover:text-orange-700'
|
||||
}
|
||||
onClick={() => handleConfirmActivateProfile(row)}
|
||||
>
|
||||
{row.is_active ? (
|
||||
<ToggleRight className="w-5 h-5 " />
|
||||
) : (
|
||||
<ToggleLeft className="w-5 h-5" />
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="text-red-500 hover:text-red-700"
|
||||
onClick={() => handleConfirmDeleteProfile(row.id)}
|
||||
>
|
||||
<Trash2 className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const schoolAdminColumns = [
|
||||
{ name: 'Identifiant', transform: (row) => row.associated_profile_email },
|
||||
{ name: 'Mise à jour', transform: (row) => row.updated_date_formatted },
|
||||
{
|
||||
name: 'Rôle',
|
||||
transform: (row) => (
|
||||
<span
|
||||
className={`px-2 py-1 rounded-full font-bold ${roleTypeToBadgeClass(row.role_type)}`}
|
||||
>
|
||||
{roleTypeToLabel(row.role_type)}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'Utilisateur',
|
||||
transform: (row) => (
|
||||
<div className="flex items-center justify-center space-x-2 relative">
|
||||
<span>{row.associated_person?.teacher_name}</span>
|
||||
{row.associated_person && (
|
||||
<div
|
||||
className="relative group"
|
||||
onMouseEnter={() => handleTooltipVisibility(row.id)} // Afficher la tooltip pour cette ligne
|
||||
onMouseLeave={handleTooltipHide} // Cacher la tooltip
|
||||
>
|
||||
<button className="relative text-blue-500 hover:text-blue-700 flex items-center justify-center">
|
||||
<div className="w-6 h-6 bg-blue-100 text-blue-700 rounded-full flex items-center justify-center font-bold">
|
||||
<Info className="w-4 h-4" /> {/* Icône Info */}
|
||||
</div>
|
||||
</button>
|
||||
{visibleTooltipId === row.id && ( // Afficher uniquement si l'ID correspond
|
||||
<div className="fixed z-50 w-96 p-4 bg-white border border-gray-200 rounded shadow-lg -translate-x-1/2">
|
||||
<div className="mb-2">
|
||||
<strong>Classes associées:</strong>
|
||||
<div className="flex flex-wrap justify-center space-x-2 mt-4">
|
||||
{row.associated_person?.classes?.map((classe) => (
|
||||
<span
|
||||
key={classe.id}
|
||||
className="px-2 py-1 rounded-full bg-gray-200 text-gray-800"
|
||||
>
|
||||
{classe.name}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Spécialités:</strong>
|
||||
<div className="flex flex-wrap justify-center space-x-2 mt-4">
|
||||
{row.associated_person?.specialities?.map(
|
||||
(speciality) => (
|
||||
<SpecialityItem
|
||||
key={speciality.name}
|
||||
speciality={speciality}
|
||||
isDraggable={false}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'Actions',
|
||||
transform: (row) => (
|
||||
<div className="flex justify-center space-x-2">
|
||||
<button
|
||||
type="button"
|
||||
className={
|
||||
row.is_active
|
||||
? 'text-emerald-500 hover:text-emerald-700'
|
||||
: 'text-orange-500 hover:text-orange-700'
|
||||
}
|
||||
onClick={() => handleConfirmActivateProfile(row)}
|
||||
>
|
||||
{row.is_active ? (
|
||||
<ToggleRight className="w-5 h-5 " />
|
||||
) : (
|
||||
<ToggleLeft className="w-5 h-5" />
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="text-red-500 hover:text-red-700"
|
||||
onClick={() => handleConfirmDeleteProfile(row.id)}
|
||||
>
|
||||
<Trash2 className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<DjangoCSRFToken csrfToken={csrfToken} />
|
||||
<SidebarTabs
|
||||
tabs={[
|
||||
{
|
||||
id: 'parent',
|
||||
label: 'Parents',
|
||||
content: (
|
||||
<div className="h-full overflow-y-auto p-4">
|
||||
<Table
|
||||
key={`parent-${currentProfilesParentPage}`}
|
||||
data={profileRolesParent}
|
||||
columns={parentColumns}
|
||||
itemsPerPage={itemsPerPage}
|
||||
currentPage={currentProfilesParentPage}
|
||||
totalPages={totalProfilesParentPages}
|
||||
onPageChange={handlePageChange}
|
||||
emptyMessage={
|
||||
<AlertMessage
|
||||
type="info"
|
||||
title="Aucun profil PARENT enregistré"
|
||||
message="Un profil Parent est ajouté lors de la création d'un nouveau dossier d'inscription."
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'school',
|
||||
label: 'École',
|
||||
content: (
|
||||
<div className="h-full overflow-y-auto p-4">
|
||||
<Table
|
||||
key={`school-${currentProfilesSchoolPage}`}
|
||||
data={profileRolesSchool}
|
||||
columns={schoolAdminColumns}
|
||||
itemsPerPage={itemsPerPage}
|
||||
currentPage={currentProfilesSchoolPage}
|
||||
totalPages={totalProfilesSchoolPages}
|
||||
onPageChange={handlePageChange}
|
||||
emptyMessage={
|
||||
<AlertMessage
|
||||
type="info"
|
||||
title="Aucun profil ECOLE enregistré"
|
||||
message="Un profil ECOLE est ajouté lors de la création d'un nouvel enseignant."
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]}
|
||||
onTabChange={(newActiveTab) => {
|
||||
setActiveTab(newActiveTab);
|
||||
}}
|
||||
/>
|
||||
{/* Popups */}
|
||||
<Popup
|
||||
isOpen={popupVisible}
|
||||
message={popupMessage}
|
||||
onConfirm={() => setPopupVisible(false)}
|
||||
uniqueConfirmButton={true}
|
||||
/>
|
||||
<Popup
|
||||
isOpen={confirmPopupVisible}
|
||||
message={confirmPopupMessage}
|
||||
onConfirm={confirmPopupOnConfirm}
|
||||
onCancel={() => setConfirmPopupVisible(false)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -1,10 +1,479 @@
|
||||
'use client'
|
||||
'use client';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import SelectChoice from '@/components/SelectChoice';
|
||||
import AcademicResults from '@/components/Grades/AcademicResults';
|
||||
import Attendance from '@/components/Grades/Attendance';
|
||||
import Remarks from '@/components/Grades/Remarks';
|
||||
import WorkPlan from '@/components/Grades/WorkPlan';
|
||||
import Homeworks from '@/components/Grades/Homeworks';
|
||||
import SpecificEvaluations from '@/components/Grades/SpecificEvaluations';
|
||||
import Orientation from '@/components/Grades/Orientation';
|
||||
import GradesStatsCircle from '@/components/Grades/GradesStatsCircle';
|
||||
import Button from '@/components/Button';
|
||||
import logger from '@/utils/logger';
|
||||
import {
|
||||
FE_ADMIN_GRADES_STUDENT_COMPETENCIES_URL,
|
||||
BASE_URL,
|
||||
} from '@/utils/Url';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import {
|
||||
fetchStudents,
|
||||
fetchStudentCompetencies,
|
||||
searchStudents,
|
||||
fetchAbsences,
|
||||
editAbsences,
|
||||
deleteAbsences,
|
||||
} from '@/app/actions/subscriptionAction';
|
||||
import { useEstablishment } from '@/context/EstablishmentContext';
|
||||
import { useClasses } from '@/context/ClassesContext';
|
||||
import { Award, FileText } from 'lucide-react';
|
||||
import SectionHeader from '@/components/SectionHeader';
|
||||
import GradesDomainBarChart from '@/components/Grades/GradesDomainBarChart';
|
||||
import InputText from '@/components/InputText';
|
||||
import dayjs from 'dayjs';
|
||||
import { useCsrfToken } from '@/context/CsrfContext';
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<div className='p-8'>
|
||||
<h1 className='heading-section'>Statistiques</h1>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const router = useRouter();
|
||||
const csrfToken = useCsrfToken();
|
||||
const { selectedEstablishmentId, selectedEstablishmentEvaluationFrequency } =
|
||||
useEstablishment();
|
||||
const { getNiveauLabel } = useClasses();
|
||||
const [formData, setFormData] = useState({
|
||||
selectedStudent: null,
|
||||
});
|
||||
|
||||
const [students, setStudents] = useState([]);
|
||||
const [studentCompetencies, setStudentCompetencies] = useState(null);
|
||||
const [grades, setGrades] = useState({});
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [selectedPeriod, setSelectedPeriod] = useState(null);
|
||||
const [allAbsences, setAllAbsences] = useState([]);
|
||||
|
||||
// Définir les périodes selon la fréquence
|
||||
const getPeriods = () => {
|
||||
if (selectedEstablishmentEvaluationFrequency === 1) {
|
||||
return [
|
||||
{ label: 'Trimestre 1', value: 1, start: '09-01', end: '12-31' },
|
||||
{ label: 'Trimestre 2', value: 2, start: '01-01', end: '03-31' },
|
||||
{ label: 'Trimestre 3', value: 3, start: '04-01', end: '07-15' },
|
||||
];
|
||||
}
|
||||
if (selectedEstablishmentEvaluationFrequency === 2) {
|
||||
return [
|
||||
{ label: 'Semestre 1', value: 1, start: '09-01', end: '01-31' },
|
||||
{ label: 'Semestre 2', value: 2, start: '02-01', end: '07-15' },
|
||||
];
|
||||
}
|
||||
if (selectedEstablishmentEvaluationFrequency === 3) {
|
||||
return [{ label: 'Année', value: 1, start: '09-01', end: '07-15' }];
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
// Sélection automatique de la période courante
|
||||
useEffect(() => {
|
||||
if (!formData.selectedStudent) {
|
||||
setSelectedPeriod(null);
|
||||
return;
|
||||
}
|
||||
const periods = getPeriods();
|
||||
const today = dayjs();
|
||||
const current = periods.find((p) => {
|
||||
const start = dayjs(`${today.year()}-${p.start}`);
|
||||
const end = dayjs(`${today.year()}-${p.end}`);
|
||||
return (
|
||||
today.isAfter(start.subtract(1, 'day')) &&
|
||||
today.isBefore(end.add(1, 'day'))
|
||||
);
|
||||
});
|
||||
setSelectedPeriod(current ? current.value : null);
|
||||
}, [formData.selectedStudent, selectedEstablishmentEvaluationFrequency]);
|
||||
|
||||
const academicResults = [
|
||||
{
|
||||
subject: 'Mathématiques',
|
||||
grade: 16,
|
||||
average: 14,
|
||||
appreciation: 'Très bon travail',
|
||||
},
|
||||
{
|
||||
subject: 'Français',
|
||||
grade: 15,
|
||||
average: 13,
|
||||
appreciation: 'Bonne participation',
|
||||
},
|
||||
];
|
||||
|
||||
const remarks = [
|
||||
{
|
||||
date: '2023-09-10',
|
||||
teacher: 'Mme Dupont',
|
||||
comment: 'Participation active en classe.',
|
||||
},
|
||||
{
|
||||
date: '2023-09-20',
|
||||
teacher: 'M. Martin',
|
||||
comment: 'Doit améliorer la concentration.',
|
||||
},
|
||||
];
|
||||
|
||||
const workPlan = [
|
||||
{
|
||||
objective: 'Renforcer la lecture',
|
||||
support: 'Exercices hebdomadaires',
|
||||
followUp: 'En cours',
|
||||
},
|
||||
{
|
||||
objective: 'Maîtriser les tables de multiplication',
|
||||
support: 'Jeux éducatifs',
|
||||
followUp: 'À démarrer',
|
||||
},
|
||||
];
|
||||
|
||||
const homeworks = [
|
||||
{ title: 'Rédaction', dueDate: '2023-10-10', status: 'Rendu' },
|
||||
{ title: 'Exercices de maths', dueDate: '2023-10-12', status: 'À faire' },
|
||||
];
|
||||
|
||||
const specificEvaluations = [
|
||||
{
|
||||
test: 'Bilan de compétences',
|
||||
date: '2023-09-25',
|
||||
result: 'Bon niveau général',
|
||||
},
|
||||
];
|
||||
|
||||
const orientation = [
|
||||
{
|
||||
date: '2023-10-01',
|
||||
counselor: 'Mme Leroy',
|
||||
advice: 'Poursuivre en filière générale',
|
||||
},
|
||||
];
|
||||
|
||||
const handleChange = (field, value) =>
|
||||
setFormData((prev) => ({ ...prev, [field]: value }));
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedEstablishmentId) {
|
||||
fetchStudents(selectedEstablishmentId, null, 5)
|
||||
.then((studentsData) => {
|
||||
setStudents(studentsData);
|
||||
})
|
||||
.catch((error) => logger.error('Error fetching students:', error));
|
||||
}
|
||||
}, [selectedEstablishmentId]);
|
||||
|
||||
// Charger les compétences et générer les grades à chaque changement d'élève sélectionné
|
||||
useEffect(() => {
|
||||
if (formData.selectedStudent && selectedPeriod) {
|
||||
const periodString = getPeriodString(
|
||||
selectedPeriod,
|
||||
selectedEstablishmentEvaluationFrequency
|
||||
);
|
||||
fetchStudentCompetencies(formData.selectedStudent, periodString)
|
||||
.then((data) => {
|
||||
setStudentCompetencies(data);
|
||||
// Générer les grades à partir du retour API
|
||||
if (data && data.data) {
|
||||
const initialGrades = {};
|
||||
data.data.forEach((domaine) => {
|
||||
domaine.categories.forEach((cat) => {
|
||||
cat.competences.forEach((comp) => {
|
||||
initialGrades[comp.competence_id] = comp.score ?? 0;
|
||||
});
|
||||
});
|
||||
});
|
||||
setGrades(initialGrades);
|
||||
}
|
||||
})
|
||||
.catch((error) =>
|
||||
logger.error('Error fetching studentCompetencies:', error)
|
||||
);
|
||||
} else {
|
||||
setGrades({});
|
||||
setStudentCompetencies(null);
|
||||
}
|
||||
}, [formData.selectedStudent, selectedPeriod]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedEstablishmentId) {
|
||||
fetchAbsences(selectedEstablishmentId)
|
||||
.then((data) => setAllAbsences(data))
|
||||
.catch((error) =>
|
||||
logger.error('Erreur lors du fetch des absences:', error)
|
||||
);
|
||||
}
|
||||
}, [selectedEstablishmentId]);
|
||||
|
||||
// Transforme les absences backend pour l'élève sélectionné
|
||||
const absences = React.useMemo(() => {
|
||||
if (!formData.selectedStudent) return [];
|
||||
return allAbsences
|
||||
.filter((a) => a.student === formData.selectedStudent)
|
||||
.map((a) => ({
|
||||
id: a.id,
|
||||
date: a.day,
|
||||
type: [2, 1].includes(a.reason) ? 'Absence' : 'Retard',
|
||||
reason: a.reason, // tu peux mapper le code vers un label si besoin
|
||||
justified: [1, 3].includes(a.reason), // 1 et 3 = justifié
|
||||
moment: a.moment,
|
||||
commentaire: a.commentaire,
|
||||
}));
|
||||
}, [allAbsences, formData.selectedStudent]);
|
||||
|
||||
// Fonction utilitaire pour convertir la période sélectionnée en string backend
|
||||
function getPeriodString(selectedPeriod, frequency) {
|
||||
const year = dayjs().month() >= 8 ? dayjs().year() : dayjs().year() - 1; // année scolaire commence en septembre
|
||||
const nextYear = (year + 1).toString();
|
||||
const schoolYear = `${year}-${nextYear}`;
|
||||
if (frequency === 1) return `T${selectedPeriod}_${schoolYear}`;
|
||||
if (frequency === 2) return `S${selectedPeriod}_${schoolYear}`;
|
||||
if (frequency === 3) return `A_${schoolYear}`;
|
||||
return '';
|
||||
}
|
||||
|
||||
// Callback pour justifier/non justifier une absence
|
||||
const handleToggleJustify = (absence) => {
|
||||
// Inverser l'état justifié (1/3 = justifié, 2/4 = non justifié)
|
||||
const newReason =
|
||||
absence.type === 'Absence'
|
||||
? absence.justified
|
||||
? 2 // Absence non justifiée
|
||||
: 1 // Absence justifiée
|
||||
: absence.justified
|
||||
? 4 // Retard non justifié
|
||||
: 3; // Retard justifié
|
||||
|
||||
editAbsences(absence.id, { ...absence, reason: newReason }, csrfToken)
|
||||
.then(() => {
|
||||
setAllAbsences((prev) =>
|
||||
prev.map((a) =>
|
||||
a.id === absence.id ? { ...a, reason: newReason } : a
|
||||
)
|
||||
);
|
||||
})
|
||||
.catch((e) => {
|
||||
logger.error('Erreur lors du changement de justification', e);
|
||||
});
|
||||
};
|
||||
|
||||
// Callback pour supprimer une absence
|
||||
const handleDeleteAbsence = (absence) => {
|
||||
return deleteAbsences(absence.id, csrfToken)
|
||||
.then(() => {
|
||||
setAllAbsences((prev) => prev.filter((a) => a.id !== absence.id));
|
||||
})
|
||||
.catch((e) => {
|
||||
logger.error("Erreur lors de la suppression de l'absence", e);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-8 space-y-8">
|
||||
<SectionHeader
|
||||
icon={Award}
|
||||
title="Suivi pédagogique"
|
||||
description="Suivez le parcours d'un élève"
|
||||
/>
|
||||
|
||||
{/* Section haute : filtre + bouton + photo élève */}
|
||||
<div className="flex flex-row gap-8 items-start">
|
||||
{/* Colonne gauche : InputText + bouton */}
|
||||
<div className="w-4/5 flex items-end gap-4">
|
||||
<div className="flex-[3_3_0%]">
|
||||
<InputText
|
||||
name="studentSearch"
|
||||
type="text"
|
||||
label="Recherche élève"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
placeholder="Rechercher un élève"
|
||||
required={false}
|
||||
enable={true}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-[1_1_0%]">
|
||||
<SelectChoice
|
||||
name="period"
|
||||
label="Période"
|
||||
placeHolder="Choisir la période"
|
||||
choices={getPeriods().map((period) => {
|
||||
const today = dayjs();
|
||||
const start = dayjs(`${today.year()}-${period.start}`);
|
||||
const end = dayjs(`${today.year()}-${period.end}`);
|
||||
const isPast = today.isAfter(end);
|
||||
return {
|
||||
value: period.value,
|
||||
label: period.label,
|
||||
disabled: isPast,
|
||||
};
|
||||
})}
|
||||
selected={selectedPeriod || ''}
|
||||
callback={(e) => setSelectedPeriod(Number(e.target.value))}
|
||||
disabled={!formData.selectedStudent}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-[1_1_0%] flex items-end">
|
||||
<Button
|
||||
primary
|
||||
onClick={() => {
|
||||
const periodString = getPeriodString(
|
||||
selectedPeriod,
|
||||
selectedEstablishmentEvaluationFrequency
|
||||
);
|
||||
const url = `${FE_ADMIN_GRADES_STUDENT_COMPETENCIES_URL}?studentId=${formData.selectedStudent}&period=${periodString}`;
|
||||
router.push(url);
|
||||
}}
|
||||
className="mb-1 px-4 py-2 rounded-md shadow bg-emerald-500 text-white hover:bg-emerald-600 w-full"
|
||||
icon={<Award className="w-6 h-6" />}
|
||||
text="Evaluer"
|
||||
title="Evaluez l'élève"
|
||||
disabled={!formData.selectedStudent || !selectedPeriod}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* Colonne droite : Photo élève */}
|
||||
<div className="w-1/5 flex flex-col items-center justify-center">
|
||||
{formData.selectedStudent &&
|
||||
(() => {
|
||||
const student = students.find(
|
||||
(s) => s.id === formData.selectedStudent
|
||||
);
|
||||
if (!student) return null;
|
||||
return (
|
||||
<>
|
||||
{student.photo ? (
|
||||
<img
|
||||
src={`${BASE_URL}${student.photo}`}
|
||||
alt={`${student.first_name} ${student.last_name}`}
|
||||
className="w-32 h-32 object-cover rounded-full border-4 border-emerald-200 mb-4 shadow"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-24 h-24 flex items-center justify-center bg-gray-200 rounded-full text-gray-500 font-bold text-3xl mb-4 border-4 border-emerald-100">
|
||||
{student.first_name?.[0]}
|
||||
{student.last_name?.[0]}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section basse : liste élèves + infos */}
|
||||
<div className="flex flex-row gap-8 items-start mt-8">
|
||||
{/* Colonne 1 : Liste des élèves */}
|
||||
<div className="w-full max-w-xs">
|
||||
<h3 className="text-lg font-semibold text-emerald-700 mb-4">
|
||||
Liste des élèves
|
||||
</h3>
|
||||
<ul className="rounded-lg bg-stone-50 shadow border border-gray-100">
|
||||
{students
|
||||
.filter(
|
||||
(student) =>
|
||||
!searchTerm ||
|
||||
`${student.last_name} ${student.first_name}`
|
||||
.toLowerCase()
|
||||
.includes(searchTerm.toLowerCase())
|
||||
)
|
||||
.map((student) => (
|
||||
<li
|
||||
key={student.id}
|
||||
className={`flex items-center gap-4 px-4 py-3 hover:bg-emerald-100 cursor-pointer transition ${
|
||||
formData.selectedStudent === student.id
|
||||
? 'bg-emerald-100 border-l-4 border-emerald-400'
|
||||
: 'border-l-2 border-gray-200'
|
||||
}`}
|
||||
onClick={() => handleChange('selectedStudent', student.id)}
|
||||
>
|
||||
{student.photo ? (
|
||||
<img
|
||||
src={`${BASE_URL}${student.photo}`}
|
||||
alt={`${student.first_name} ${student.last_name}`}
|
||||
className="w-10 h-10 object-cover rounded-full border-2 border-emerald-200"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-10 h-10 flex items-center justify-center bg-gray-200 rounded-full text-gray-500 font-bold text-lg border-2 border-emerald-100">
|
||||
{student.first_name?.[0]}
|
||||
{student.last_name?.[0]}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex-1">
|
||||
<div className="font-semibold text-emerald-800">
|
||||
{student.last_name} {student.first_name}
|
||||
</div>
|
||||
<div className="text-xs text-gray-600">
|
||||
Niveau :{' '}
|
||||
<span className="font-medium">
|
||||
{getNiveauLabel(student.level)}
|
||||
</span>
|
||||
{' | '}
|
||||
Classe :{' '}
|
||||
<span className="font-medium">
|
||||
{student.associated_class_name}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{/* Icône PDF si bilan dispo pour la période sélectionnée */}
|
||||
{selectedPeriod &&
|
||||
student.bilans &&
|
||||
Array.isArray(student.bilans) &&
|
||||
(() => {
|
||||
// Génère la string de période attendue
|
||||
const periodString = getPeriodString(
|
||||
selectedPeriod,
|
||||
selectedEstablishmentEvaluationFrequency
|
||||
);
|
||||
const bilan = student.bilans.find(
|
||||
(b) => b.period === periodString && b.file
|
||||
);
|
||||
if (bilan) {
|
||||
return (
|
||||
<a
|
||||
href={`${BASE_URL}${bilan.file}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="ml-2 text-emerald-600 hover:text-emerald-800"
|
||||
title="Télécharger le bilan de compétences"
|
||||
onClick={(e) => e.stopPropagation()} // Pour ne pas sélectionner à nouveau l'élève
|
||||
>
|
||||
<FileText className="w-5 h-5" />
|
||||
</a>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})()}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
{/* Colonne 2 : Reste des infos */}
|
||||
<div className="flex-1">
|
||||
{formData.selectedStudent && (
|
||||
<div className="flex flex-col gap-8 w-full justify-center items-stretch">
|
||||
<div className="w-full flex flex-row items-stretch gap-4">
|
||||
<div className="flex-1 flex items-stretch justify-center h-full">
|
||||
<Attendance
|
||||
absences={absences}
|
||||
onToggleJustify={handleToggleJustify}
|
||||
onDelete={handleDeleteAbsence}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 flex items-stretch justify-center h-full">
|
||||
<GradesStatsCircle grades={grades} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-center">
|
||||
<GradesDomainBarChart
|
||||
studentCompetencies={studentCompetencies}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -0,0 +1,123 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useSearchParams, useRouter } from 'next/navigation';
|
||||
import Button from '@/components/Button';
|
||||
import GradeView from '@/components/Grades/GradeView';
|
||||
import {
|
||||
fetchStudentCompetencies,
|
||||
editStudentCompetencies,
|
||||
} from '@/app/actions/subscriptionAction';
|
||||
import SectionHeader from '@/components/SectionHeader';
|
||||
import { Award } from 'lucide-react';
|
||||
import { useCsrfToken } from '@/context/CsrfContext';
|
||||
import { useNotification } from '@/context/NotificationContext';
|
||||
|
||||
export default function StudentCompetenciesPage() {
|
||||
const searchParams = useSearchParams();
|
||||
const router = useRouter();
|
||||
const csrfToken = useCsrfToken();
|
||||
const { showNotification } = useNotification();
|
||||
const [studentCompetencies, setStudentCompetencies] = useState([]);
|
||||
const [grades, setGrades] = useState({});
|
||||
const studentId = searchParams.get('studentId');
|
||||
const period = searchParams.get('period');
|
||||
|
||||
useEffect(() => {
|
||||
fetchStudentCompetencies(studentId, period)
|
||||
.then((data) => {
|
||||
setStudentCompetencies(data);
|
||||
})
|
||||
.catch((error) =>
|
||||
logger.error('Error fetching studentCompetencies:', error)
|
||||
);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (studentCompetencies.data) {
|
||||
const initialGrades = {};
|
||||
studentCompetencies.data.forEach((domaine) => {
|
||||
domaine.categories.forEach((cat) => {
|
||||
cat.competences.forEach((comp) => {
|
||||
initialGrades[comp.competence_id] = comp.score ?? 0;
|
||||
});
|
||||
});
|
||||
});
|
||||
setGrades(initialGrades);
|
||||
}
|
||||
}, [studentCompetencies.data]);
|
||||
|
||||
const handleGradeChange = (competenceId, level) => {
|
||||
setGrades((prev) => ({
|
||||
...prev,
|
||||
[competenceId]: level,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
const data = Object.entries(grades)
|
||||
.filter(([_, score]) => [1, 2, 3].includes(score))
|
||||
.map(([competenceId, score]) => ({
|
||||
studentId,
|
||||
competenceId,
|
||||
grade: score,
|
||||
period: period,
|
||||
}));
|
||||
editStudentCompetencies(data, csrfToken)
|
||||
.then(() => {
|
||||
showNotification(
|
||||
'Bilan de compétence sauvegardé avec succès',
|
||||
'success',
|
||||
'Succès'
|
||||
);
|
||||
router.back();
|
||||
})
|
||||
.catch((error) => {
|
||||
showNotification(
|
||||
"Erreur lors de l'enregistrement du bilan de compétence",
|
||||
'error',
|
||||
'Erreur'
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col p-4">
|
||||
<SectionHeader
|
||||
icon={Award}
|
||||
title="Bilan de compétence"
|
||||
description="Evaluez les compétence de l'élève"
|
||||
/>
|
||||
<div className="flex-1 min-h-0 flex flex-col">
|
||||
<form
|
||||
className="flex-1 min-h-0 flex flex-col"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
handleSubmit();
|
||||
}}
|
||||
>
|
||||
{/* Zone scrollable pour les compétences */}
|
||||
<div className="flex-1 min-h-0 overflow-y-auto">
|
||||
<GradeView
|
||||
data={studentCompetencies.data}
|
||||
grades={grades}
|
||||
onGradeChange={handleGradeChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-6 flex justify-end">
|
||||
<Button
|
||||
text="Retour"
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
router.back();
|
||||
}}
|
||||
className="mr-2 bg-gray-200 text-gray-700 hover:bg-gray-300"
|
||||
/>
|
||||
<Button text="Enregistrer" primary type="submit" />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,96 +1,188 @@
|
||||
'use client'
|
||||
// src/components/Layout.js
|
||||
import React from 'react';
|
||||
'use client';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import Sidebar from '@/components/Sidebar';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import {useTranslations} from 'next-intl';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import {
|
||||
LayoutDashboard,
|
||||
FileText,
|
||||
School,
|
||||
Users,
|
||||
Building,
|
||||
Home,
|
||||
Award,
|
||||
Calendar,
|
||||
Settings,
|
||||
FileText,
|
||||
LogOut
|
||||
LogOut,
|
||||
MessageSquare,
|
||||
} from 'lucide-react';
|
||||
import DropdownMenu from '@/components/DropdownMenu';
|
||||
import Logo from '@/components/Logo';
|
||||
|
||||
import Popup from '@/components/Popup';
|
||||
import {
|
||||
FR_ADMIN_HOME_URL,
|
||||
FR_ADMIN_SUBSCRIPTIONS_URL,
|
||||
FR_ADMIN_STRUCTURE_URL,
|
||||
FR_ADMIN_GRADES_URL,
|
||||
FR_ADMIN_PLANNING_URL,
|
||||
FR_ADMIN_SETTINGS_URL
|
||||
FE_ADMIN_HOME_URL,
|
||||
FE_ADMIN_SUBSCRIPTIONS_URL,
|
||||
FE_ADMIN_STRUCTURE_URL,
|
||||
FE_ADMIN_DIRECTORY_URL,
|
||||
FE_ADMIN_GRADES_URL,
|
||||
FE_ADMIN_PLANNING_URL,
|
||||
FE_ADMIN_SETTINGS_URL,
|
||||
FE_ADMIN_MESSAGERIE_URL,
|
||||
} from '@/utils/Url';
|
||||
|
||||
import { disconnect } from '@/app/lib/actions';
|
||||
import { disconnect } from '@/app/actions/authAction';
|
||||
import ProtectedRoute from '@/components/ProtectedRoute';
|
||||
import { getGravatarUrl } from '@/utils/gravatar';
|
||||
import Footer from '@/components/Footer';
|
||||
import { getRightStr, RIGHTS } from '@/utils/rights';
|
||||
import { useEstablishment } from '@/context/EstablishmentContext';
|
||||
import ProfileSelector from '@/components/ProfileSelector';
|
||||
|
||||
export default function Layout({
|
||||
children,
|
||||
}) {
|
||||
const t = useTranslations('sidebar');
|
||||
export default function Layout({ children }) {
|
||||
const t = useTranslations('sidebar');
|
||||
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
||||
const { profileRole, establishments, user, clearContext } =
|
||||
useEstablishment();
|
||||
|
||||
const sidebarItems = {
|
||||
"admin": { "id": "admin", "name": t('dashboard'), "url": FR_ADMIN_HOME_URL, "icon": Home },
|
||||
"subscriptions": { "id": "subscriptions", "name": t('subscriptions'), "url": FR_ADMIN_SUBSCRIPTIONS_URL, "icon": Users },
|
||||
"structure": { "id": "structure", "name": t('structure'), "url": FR_ADMIN_STRUCTURE_URL, "icon": Building },
|
||||
"grades": { "id": "grades", "name": t('grades'), "url": FR_ADMIN_GRADES_URL, "icon": FileText },
|
||||
"planning": { "id": "planning", "name": t('planning'), "url": FR_ADMIN_PLANNING_URL, "icon": Calendar },
|
||||
"settings": { "id": "settings", "name": t('settings'), "url": FR_ADMIN_SETTINGS_URL, "icon": Settings }
|
||||
admin: {
|
||||
id: 'admin',
|
||||
name: t('dashboard'),
|
||||
url: FE_ADMIN_HOME_URL,
|
||||
icon: LayoutDashboard,
|
||||
},
|
||||
subscriptions: {
|
||||
id: 'subscriptions',
|
||||
name: t('subscriptions'),
|
||||
url: FE_ADMIN_SUBSCRIPTIONS_URL,
|
||||
icon: FileText,
|
||||
},
|
||||
structure: {
|
||||
id: 'structure',
|
||||
name: t('structure'),
|
||||
url: FE_ADMIN_STRUCTURE_URL,
|
||||
icon: School,
|
||||
},
|
||||
directory: {
|
||||
id: 'directory',
|
||||
name: t('directory'),
|
||||
url: FE_ADMIN_DIRECTORY_URL,
|
||||
icon: Users,
|
||||
},
|
||||
grades: {
|
||||
id: 'grades',
|
||||
name: t('educational_monitoring'),
|
||||
url: FE_ADMIN_GRADES_URL,
|
||||
icon: Award,
|
||||
},
|
||||
planning: {
|
||||
id: 'planning',
|
||||
name: t('events'),
|
||||
url: FE_ADMIN_PLANNING_URL,
|
||||
icon: Calendar,
|
||||
},
|
||||
messagerie: {
|
||||
id: 'messagerie',
|
||||
name: t('messagerie'),
|
||||
url: FE_ADMIN_MESSAGERIE_URL,
|
||||
icon: MessageSquare,
|
||||
},
|
||||
settings: {
|
||||
id: 'settings',
|
||||
name: t('settings'),
|
||||
url: FE_ADMIN_SETTINGS_URL,
|
||||
icon: Settings,
|
||||
},
|
||||
};
|
||||
|
||||
const [isPopupVisible, setIsPopupVisible] = useState(false);
|
||||
|
||||
const pathname = usePathname();
|
||||
const currentPage = pathname.split('/').pop();
|
||||
|
||||
const headerTitle = sidebarItems[currentPage]?.name || t('dashboard');
|
||||
|
||||
const softwareName = "N3WT School";
|
||||
const softwareVersion = "v1.0.0";
|
||||
const softwareName = 'N3WT School';
|
||||
const softwareVersion = `${process.env.NEXT_PUBLIC_APP_VERSION}`;
|
||||
|
||||
const handleDisconnect = () => {
|
||||
setIsPopupVisible(true);
|
||||
};
|
||||
|
||||
const confirmDisconnect = () => {
|
||||
setIsPopupVisible(false);
|
||||
disconnect();
|
||||
clearContext();
|
||||
};
|
||||
|
||||
const dropdownItems = [
|
||||
{
|
||||
type: 'info',
|
||||
content: (
|
||||
<div className="px-4 py-2">
|
||||
<div className="font-medium">{user?.email || 'Utilisateur'}</div>
|
||||
<div className="text-xs text-gray-400">
|
||||
{getRightStr(profileRole) || ''}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
type: 'separator',
|
||||
content: <hr className="my-2 border-gray-200" />,
|
||||
},
|
||||
{
|
||||
type: 'item',
|
||||
label: 'Déconnexion',
|
||||
onClick: disconnect,
|
||||
onClick: handleDisconnect,
|
||||
icon: LogOut,
|
||||
},
|
||||
];
|
||||
|
||||
const toggleSidebar = () => {
|
||||
setIsSidebarOpen(!isSidebarOpen);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// Fermer la sidebar quand on change de page sur mobile
|
||||
setIsSidebarOpen(false);
|
||||
}, [pathname]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex min-h-screen bg-gray-50">
|
||||
<Sidebar currentPage={currentPage} items={Object.values(sidebarItems)} className="h-full" />
|
||||
<div className="flex flex-col flex-1">
|
||||
{/* Header - h-16 = 64px */}
|
||||
<header className="h-16 bg-white border-b border-gray-200 px-8 py-4 flex items-center justify-between z-9">
|
||||
<div className="text-xl font-semibold">{headerTitle}</div>
|
||||
<DropdownMenu
|
||||
buttonContent={<img src="https://i.pravatar.cc/32" alt="Profile" className="w-8 h-8 rounded-full cursor-pointer" />}
|
||||
items={dropdownItems}
|
||||
buttonClassName=""
|
||||
menuClassName="absolute right-0 mt-2 w-48 bg-white border border-gray-200 rounded shadow-lg"
|
||||
/>
|
||||
</header>
|
||||
{/* Main Content */}
|
||||
<div className="flex-1 flex flex-col">
|
||||
{/* Content avec scroll si nécessaire */}
|
||||
<div className="flex-1 overflow-auto">
|
||||
{children}
|
||||
</div>
|
||||
{/* Footer - h-16 = 64px */}
|
||||
<footer className="h-16 bg-white border-t border-gray-200 px-8 py-4 flex items-center justify-between">
|
||||
<div>
|
||||
<span>© {new Date().getFullYear()} N3WT-INNOV Tous droits réservés.</span>
|
||||
<div>{softwareName} - {softwareVersion}</div>
|
||||
</div>
|
||||
<Logo className="w-8 h-8" />
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
<ProtectedRoute requiredRight={[RIGHTS.ADMIN, RIGHTS.TEACHER]}>
|
||||
{/* Sidebar */}
|
||||
<div
|
||||
className={`absolute top-0 bottom-0 left-0 z-30 w-64 bg-white border-r border-gray-200 box-border ${
|
||||
isSidebarOpen ? 'block' : 'hidden md:block'
|
||||
}`}
|
||||
>
|
||||
<Sidebar
|
||||
establishments={establishments}
|
||||
currentPage={currentPage}
|
||||
items={Object.values(sidebarItems)}
|
||||
onCloseMobile={toggleSidebar}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
|
||||
{/* Overlay for mobile */}
|
||||
{isSidebarOpen && (
|
||||
<div
|
||||
className="fixed inset-0 bg-black bg-opacity-50 z-20 md:hidden"
|
||||
onClick={toggleSidebar}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Main container */}
|
||||
<div className="absolute overflow-auto bg-gradient-to-br from-emerald-50 via-sky-50 to-emerald-100 top-0 bottom-16 left-64 right-0">
|
||||
{children}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<Footer softwareName={softwareName} softwareVersion={softwareVersion} />
|
||||
|
||||
<Popup
|
||||
isOpen={isPopupVisible}
|
||||
message="Êtes-vous sûr(e) de vouloir vous déconnecter ?"
|
||||
onConfirm={confirmDisconnect}
|
||||
onCancel={() => setIsPopupVisible(false)}
|
||||
/>
|
||||
</ProtectedRoute>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
27
Front-End/src/app/[locale]/admin/messagerie/page.js
Normal file
27
Front-End/src/app/[locale]/admin/messagerie/page.js
Normal file
@ -0,0 +1,27 @@
|
||||
'use client';
|
||||
import React from 'react';
|
||||
import SidebarTabs from '@/components/SidebarTabs';
|
||||
import EmailSender from '@/components/Admin/EmailSender';
|
||||
import InstantMessaging from '@/components/Admin/InstantMessaging';
|
||||
import logger from '@/utils/logger';
|
||||
|
||||
export default function MessageriePage({ csrfToken }) {
|
||||
const tabs = [
|
||||
{
|
||||
id: 'email',
|
||||
label: 'Envoyer un Mail',
|
||||
content: <EmailSender csrfToken={csrfToken} />,
|
||||
},
|
||||
{
|
||||
id: 'instant',
|
||||
label: 'Messagerie Instantanée',
|
||||
content: <InstantMessaging csrfToken={csrfToken} />,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col p-0 m-0">
|
||||
<SidebarTabs tabs={tabs} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,169 +1,252 @@
|
||||
|
||||
'use client'
|
||||
'use client';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { Users, Clock, CalendarCheck, School, TrendingUp, UserCheck } from 'lucide-react';
|
||||
import {
|
||||
Users,
|
||||
Clock,
|
||||
CalendarCheck,
|
||||
School,
|
||||
AlertTriangle,
|
||||
CheckCircle2,
|
||||
} from 'lucide-react';
|
||||
import Loader from '@/components/Loader';
|
||||
import { BK_GESTIONENSEIGNANTS_CLASSES_URL } from '@/utils/Url';
|
||||
import ClasseDetails from '@/components/ClasseDetails';
|
||||
import StatCard from '@/components/StatCard';
|
||||
import EventCard from '@/components/EventCard';
|
||||
import logger from '@/utils/logger';
|
||||
import {
|
||||
fetchRegisterForms,
|
||||
fetchAbsences,
|
||||
} from '@/app/actions/subscriptionAction';
|
||||
import { fetchUpcomingEvents } from '@/app/actions/planningAction';
|
||||
import { useEstablishment } from '@/context/EstablishmentContext';
|
||||
import Attendance from '@/components/Grades/Attendance';
|
||||
import LineChart from '@/components/Charts/LineChart';
|
||||
import PieChart from '@/components/Charts/PieChart';
|
||||
|
||||
// Composant StatCard pour afficher une statistique
|
||||
const StatCard = ({ title, value, icon, change, color = "blue" }) => (
|
||||
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-100">
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<h3 className="text-gray-500 text-sm font-medium">{title}</h3>
|
||||
<p className="text-2xl font-semibold mt-1">{value}</p>
|
||||
{change && (
|
||||
<p className={`text-sm ${change > 0 ? 'text-green-500' : 'text-red-500'}`}>
|
||||
{change > 0 ? '+' : ''}{change}% depuis le mois dernier
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className={`p-3 rounded-full bg-${color}-100`}>
|
||||
{icon}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
// Composant EventCard pour afficher les événements
|
||||
const EventCard = ({ title, date, description, type }) => (
|
||||
<div className="bg-white p-4 rounded-lg shadow-sm border border-gray-100 mb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<CalendarCheck className="text-blue-500" size={20} />
|
||||
<div>
|
||||
<h4 className="font-medium">{title}</h4>
|
||||
<p className="text-sm text-gray-500">{date}</p>
|
||||
<p className="text-sm mt-1">{description}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
const mockCompletionRate = 72; // en pourcentage
|
||||
|
||||
export default function DashboardPage() {
|
||||
const t = useTranslations('dashboard');
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [stats, setStats] = useState({
|
||||
totalStudents: 0,
|
||||
averageInscriptionTime: 0,
|
||||
reInscriptionRate: 0,
|
||||
structureCapacity: 0,
|
||||
upcomingEvents: [],
|
||||
monthlyStats: {
|
||||
inscriptions: [],
|
||||
completionRate: 0
|
||||
}
|
||||
});
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [currentYearRegistrationCount, setCurrentYearRegistrationCount] =
|
||||
useState(0);
|
||||
const [upcomingEvents, setUpcomingEvents] = useState([]);
|
||||
|
||||
const [classes, setClasses] = useState([]);
|
||||
const [absencesToday, setAbsencesToday] = useState([]);
|
||||
const {
|
||||
selectedEstablishmentId,
|
||||
selectedEstablishmentTotalCapacity,
|
||||
apiDocuseal,
|
||||
} = useEstablishment();
|
||||
|
||||
const fetchClasses = () => {
|
||||
fetch(`${BK_GESTIONENSEIGNANTS_CLASSES_URL}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
setClasses(data);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching classes:', error);
|
||||
});
|
||||
};
|
||||
const [statusDistribution, setStatusDistribution] = useState([
|
||||
{ label: 'Non envoyé', value: 0 },
|
||||
{ label: 'En attente', value: 0 },
|
||||
{ label: 'En validation', value: 0 },
|
||||
{ label: 'Validé', value: 0 },
|
||||
]);
|
||||
const [monthlyRegistrations, setMonthlyRegistrations] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
// Fetch data for classes
|
||||
fetchClasses();
|
||||
if (!selectedEstablishmentId) return;
|
||||
|
||||
// Simulation de chargement des données
|
||||
setTimeout(() => {
|
||||
setStats({
|
||||
totalStudents: 245,
|
||||
averageInscriptionTime: 3.5,
|
||||
reInscriptionRate: 85,
|
||||
structureCapacity: 300,
|
||||
upcomingEvents: [
|
||||
setIsLoading(true); // Début du chargement
|
||||
|
||||
// Fetch des formulaires d'inscription
|
||||
fetchRegisterForms(selectedEstablishmentId)
|
||||
.then((data) => {
|
||||
logger.info('Pending registrations fetched:', data);
|
||||
setCurrentYearRegistrationCount(data.count || 0);
|
||||
const forms = data.registerForms || [];
|
||||
|
||||
// Filtrage des statuts
|
||||
const distribution = [
|
||||
{
|
||||
title: "Réunion de rentrée",
|
||||
date: "2024-09-01",
|
||||
description: "Présentation de l'année scolaire",
|
||||
type: "meeting"
|
||||
label: 'Non envoyé',
|
||||
value: forms.filter((f) => f.status === 1).length,
|
||||
},
|
||||
{
|
||||
title: "Date limite inscriptions",
|
||||
date: "2024-08-15",
|
||||
description: "Clôture des inscriptions",
|
||||
type: "deadline"
|
||||
label: 'En attente',
|
||||
value: forms.filter((f) => f.status === 2 || f.status === 7).length,
|
||||
},
|
||||
{
|
||||
label: 'En validation',
|
||||
value: forms.filter((f) => f.status === 3 || f.status === 8).length,
|
||||
},
|
||||
{
|
||||
label: 'Validé',
|
||||
value: forms.filter((f) => f.status === 5).length,
|
||||
},
|
||||
];
|
||||
setStatusDistribution(distribution);
|
||||
|
||||
// Calcul des inscriptions validées par mois
|
||||
const validForms = forms.filter(
|
||||
(f) => f.status === 5 && f.formatted_last_update
|
||||
);
|
||||
// Format attendu : "29-05-2025 09:23"
|
||||
const monthLabels = [
|
||||
'Janv',
|
||||
'Fév',
|
||||
'Mars',
|
||||
'Avr',
|
||||
'Mai',
|
||||
'Juin',
|
||||
'Juil',
|
||||
'Août',
|
||||
'Sept',
|
||||
'Oct',
|
||||
'Nov',
|
||||
'Déc',
|
||||
];
|
||||
const monthlyCount = Array(12).fill(0);
|
||||
|
||||
validForms.forEach((f) => {
|
||||
const [day, month, yearAndTime] = f.formatted_last_update.split('-');
|
||||
const monthIdx = parseInt(month, 10) - 1;
|
||||
if (monthIdx >= 0 && monthIdx < 12) {
|
||||
monthlyCount[monthIdx]++;
|
||||
}
|
||||
],
|
||||
monthlyStats: {
|
||||
inscriptions: [150, 180, 210, 245],
|
||||
completionRate: 78
|
||||
}
|
||||
});
|
||||
|
||||
const monthlyData = monthLabels.map((label, idx) => ({
|
||||
month: label,
|
||||
value: monthlyCount[idx],
|
||||
}));
|
||||
|
||||
setMonthlyRegistrations(monthlyData);
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error('Erreur lors du fetch des inscriptions :', error);
|
||||
});
|
||||
setIsLoading(false);
|
||||
}, 1000);
|
||||
}, []);
|
||||
|
||||
// Fetch des événements à venir
|
||||
fetchUpcomingEvents(selectedEstablishmentId)
|
||||
.then((data) => {
|
||||
setUpcomingEvents(data);
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error('Error fetching upcoming events:', error);
|
||||
});
|
||||
|
||||
// Fetch absences/retards du jour
|
||||
fetchAbsences(selectedEstablishmentId)
|
||||
.then((data) => {
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
const absToday = (data || []).filter((a) => a.day === today);
|
||||
// Mapping pour Attendance
|
||||
setAbsencesToday(
|
||||
absToday.map((a) => ({
|
||||
id: a.id,
|
||||
date: a.day,
|
||||
type: [1, 2].includes(a.reason) ? 'Absence' : 'Retard',
|
||||
justified: [1, 3].includes(a.reason),
|
||||
commentaire: a.commentaire,
|
||||
student_name: a.student_name,
|
||||
}))
|
||||
);
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error('Erreur lors du fetch des absences du jour:', error);
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false); // Fin du chargement
|
||||
});
|
||||
}, [selectedEstablishmentId]);
|
||||
|
||||
// Calculs à partir de statusDistribution
|
||||
const totalStudents =
|
||||
statusDistribution.find((s) => s.label === 'Validé')?.value || 0;
|
||||
const pendingRegistrationCount = statusDistribution
|
||||
.filter((s) => s.label !== 'Validé')
|
||||
.reduce((acc, s) => acc + s.value, 0);
|
||||
|
||||
if (isLoading) return <Loader />;
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<h1 className="text-2xl font-bold mb-6">{t('dashboard')}</h1>
|
||||
<div key={selectedEstablishmentId} className="p-6">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<span
|
||||
className={`inline-flex items-center px-3 py-1 rounded-full text-sm font-semibold ${
|
||||
apiDocuseal
|
||||
? 'bg-green-100 text-green-700 border border-green-300'
|
||||
: 'bg-red-100 text-red-700 border border-red-300'
|
||||
}`}
|
||||
>
|
||||
{apiDocuseal ? (
|
||||
<CheckCircle2 className="w-4 h-4 mr-2 text-green-500" />
|
||||
) : (
|
||||
<AlertTriangle className="w-4 h-4 mr-2 text-red-500" />
|
||||
)}
|
||||
{apiDocuseal
|
||||
? 'Clé API Docuseal renseignée'
|
||||
: 'Clé API Docuseal manquante'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Statistiques principales */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
|
||||
<StatCard
|
||||
title={t('totalStudents')}
|
||||
value={stats.totalStudents}
|
||||
value={totalStudents}
|
||||
icon={<Users className="text-blue-500" size={24} />}
|
||||
change={12}
|
||||
/>
|
||||
<StatCard
|
||||
title={t('averageInscriptionTime')}
|
||||
value={`${stats.averageInscriptionTime} jours`}
|
||||
title={t('pendingRegistrations')}
|
||||
value={pendingRegistrationCount}
|
||||
icon={<Clock className="text-green-500" size={24} />}
|
||||
color="green"
|
||||
/>
|
||||
<StatCard
|
||||
title={t('reInscriptionRate')}
|
||||
value={`${stats.reInscriptionRate}%`}
|
||||
icon={<UserCheck className="text-purple-500" size={24} />}
|
||||
color="purple"
|
||||
title={t('structureCapacity')}
|
||||
value={selectedEstablishmentTotalCapacity}
|
||||
icon={<School className="text-green-500" size={24} />}
|
||||
color="emerald"
|
||||
/>
|
||||
<StatCard
|
||||
title={t('structureCapacity')}
|
||||
value={`${(stats.totalStudents/stats.structureCapacity * 100).toFixed(1)}%`}
|
||||
title={t('capacityRate')}
|
||||
value={
|
||||
selectedEstablishmentTotalCapacity > 0
|
||||
? `${((totalStudents / selectedEstablishmentTotalCapacity) * 100).toFixed(2)}%`
|
||||
: 0
|
||||
}
|
||||
icon={<School className="text-orange-500" size={24} />}
|
||||
color="orange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Événements et KPIs */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-6">
|
||||
{/* Graphique des inscriptions */}
|
||||
<div className="lg:col-span-2 bg-white p-6 rounded-lg shadow-sm border border-gray-100">
|
||||
<h2 className="text-lg font-semibold mb-4">{t('inscriptionTrends')}</h2>
|
||||
{/* Insérer ici un composant de graphique */}
|
||||
<div className="h-64 bg-gray-50 rounded flex items-center justify-center">
|
||||
<TrendingUp size={48} className="text-gray-300" />
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
||||
{/* Colonne de gauche : Graphique des inscriptions + Présence */}
|
||||
<div className="flex flex-col gap-6">
|
||||
{/* Graphique des inscriptions */}
|
||||
<div className="bg-stone-50 p-6 rounded-lg shadow-sm border border-gray-100 flex-1">
|
||||
<h2 className="text-lg font-semibold mb-6">
|
||||
{t('inscriptionTrends')}
|
||||
</h2>
|
||||
<div className="flex flex-row gap-4">
|
||||
<div className="flex-1 p-6">
|
||||
<LineChart data={monthlyRegistrations} />
|
||||
</div>
|
||||
<div className="flex-1 flex items-center justify-center">
|
||||
<PieChart data={statusDistribution} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* Présence et assiduité */}
|
||||
<div className="bg-stone-50 p-6 rounded-lg shadow-sm border border-gray-100 flex-1">
|
||||
<Attendance absences={absencesToday} readOnly={true} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Événements à venir */}
|
||||
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-100">
|
||||
{/* Colonne de droite : Événements à venir */}
|
||||
<div className="bg-stone-50 p-6 rounded-lg shadow-sm border border-gray-100 flex-1 h-full">
|
||||
<h2 className="text-lg font-semibold mb-4">{t('upcomingEvents')}</h2>
|
||||
{stats.upcomingEvents.map((event, index) => (
|
||||
{upcomingEvents.map((event, index) => (
|
||||
<EventCard key={index} {...event} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap">
|
||||
{classes.map((classe) => (
|
||||
<div className="lg:col-span-2 bg-white p-6 rounded-lg shadow-sm border border-gray-100 mr-4">
|
||||
<ClasseDetails key={classe.id} classe={classe} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,9 +1,14 @@
|
||||
'use client'
|
||||
import { PlanningProvider } from '@/context/PlanningContext';
|
||||
import Calendar from '@/components/Calendar';
|
||||
import EventModal from '@/components/EventModal';
|
||||
import ScheduleNavigation from '@/components/ScheduleNavigation';
|
||||
'use client';
|
||||
import {
|
||||
PlanningModes,
|
||||
PlanningProvider,
|
||||
RecurrenceType,
|
||||
} from '@/context/PlanningContext';
|
||||
import Calendar from '@/components/Calendar/Calendar';
|
||||
import EventModal from '@/components/Calendar/EventModal';
|
||||
import ScheduleNavigation from '@/components/Calendar/ScheduleNavigation';
|
||||
import { useState } from 'react';
|
||||
import { useEstablishment } from '@/context/EstablishmentContext';
|
||||
|
||||
export default function Page() {
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
@ -13,14 +18,15 @@ export default function Page() {
|
||||
start: '',
|
||||
end: '',
|
||||
location: '',
|
||||
scheduleId: '', // Enlever la valeur par défaut ici
|
||||
recurrence: 'none',
|
||||
planning: '', // Enlever la valeur par défaut ici
|
||||
recursionType: RecurrenceType.NONE,
|
||||
selectedDays: [],
|
||||
recurrenceEnd: '',
|
||||
recursionEnd: '',
|
||||
customInterval: 1,
|
||||
customUnit: 'days',
|
||||
viewType: 'week' // Ajouter la vue semaine par défaut
|
||||
viewType: 'week', // Ajouter la vue semaine par défaut
|
||||
});
|
||||
const { selectedEstablishmentId } = useEstablishment();
|
||||
|
||||
const initializeNewEvent = (date = new Date()) => {
|
||||
// S'assurer que date est un objet Date valide
|
||||
@ -32,18 +38,23 @@ export default function Page() {
|
||||
start: eventDate.toISOString(),
|
||||
end: new Date(eventDate.getTime() + 2 * 60 * 60 * 1000).toISOString(),
|
||||
location: '',
|
||||
scheduleId: '', // Ne pas définir de valeur par défaut ici non plus
|
||||
recurrence: 'none',
|
||||
planning: '', // Ne pas définir de valeur par défaut ici non plus
|
||||
recursionType: RecurrenceType.NONE,
|
||||
selectedDays: [],
|
||||
recurrenceEnd: '',
|
||||
recursionEnd: new Date(
|
||||
eventDate.getTime() + 2 * 60 * 60 * 1000
|
||||
).toISOString(),
|
||||
customInterval: 1,
|
||||
customUnit: 'days'
|
||||
customUnit: 'days',
|
||||
});
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<PlanningProvider>
|
||||
<PlanningProvider
|
||||
establishmentId={selectedEstablishmentId}
|
||||
modeSet={PlanningModes.PLANNING}
|
||||
>
|
||||
<div className="flex h-full overflow-hidden">
|
||||
<ScheduleNavigation />
|
||||
<Calendar
|
||||
@ -62,4 +73,4 @@ export default function Page() {
|
||||
</div>
|
||||
</PlanningProvider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,12 +1,22 @@
|
||||
'use client'
|
||||
import React, { useState } from 'react';
|
||||
'use client';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import Tab from '@/components/Tab';
|
||||
import TabContent from '@/components/TabContent';
|
||||
import Button from '@/components/Button';
|
||||
import InputText from '@/components/InputText';
|
||||
import CheckBox from '@/components/CheckBox'; // Import du composant CheckBox
|
||||
import logger from '@/utils/logger';
|
||||
import {
|
||||
fetchSmtpSettings,
|
||||
editSmtpSettings,
|
||||
} from '@/app/actions/settingsAction';
|
||||
import { useEstablishment } from '@/context/EstablishmentContext';
|
||||
import { useCsrfToken } from '@/context/CsrfContext'; // Import du hook pour récupérer le csrfToken
|
||||
import { useNotification } from '@/context/NotificationContext';
|
||||
import { useSearchParams } from 'next/navigation'; // Ajoute cet import
|
||||
|
||||
export default function SettingsPage() {
|
||||
const [activeTab, setActiveTab] = useState('structure');
|
||||
const [activeTab, setActiveTab] = useState('smtp');
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [confirmPassword, setConfirmPassword] = useState('');
|
||||
@ -14,22 +24,58 @@ export default function SettingsPage() {
|
||||
const [smtpPort, setSmtpPort] = useState('');
|
||||
const [smtpUser, setSmtpUser] = useState('');
|
||||
const [smtpPassword, setSmtpPassword] = useState('');
|
||||
const [useTls, setUseTls] = useState(true);
|
||||
const [useSsl, setUseSsl] = useState(false);
|
||||
const { selectedEstablishmentId } = useEstablishment();
|
||||
const csrfToken = useCsrfToken(); // Récupération du csrfToken
|
||||
const { showNotification } = useNotification();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const handleTabClick = (tab) => {
|
||||
setActiveTab(tab);
|
||||
};
|
||||
|
||||
const handleEmailChange = (e) => {
|
||||
setEmail(e.target.value);
|
||||
};
|
||||
// Ajout : sélection automatique de l'onglet via l'ancre ou le paramètre de recherche
|
||||
useEffect(() => {
|
||||
const tabParam = searchParams.get('tab');
|
||||
if (tabParam === 'smtp') {
|
||||
setActiveTab('smtp');
|
||||
}
|
||||
}, [searchParams]);
|
||||
|
||||
const handlePasswordChange = (e) => {
|
||||
setPassword(e.target.value);
|
||||
};
|
||||
|
||||
const handleConfirmPasswordChange = (e) => {
|
||||
setConfirmPassword(e.target.value);
|
||||
};
|
||||
// Charger les paramètres SMTP existants
|
||||
useEffect(() => {
|
||||
if (activeTab === 'smtp') {
|
||||
fetchSmtpSettings(csrfToken, selectedEstablishmentId) // Passer le csrfToken ici
|
||||
.then((data) => {
|
||||
setSmtpServer(data.smtp_server || '');
|
||||
setSmtpPort(data.smtp_port || '');
|
||||
setSmtpUser(data.smtp_user || '');
|
||||
setSmtpPassword(data.smtp_password || '');
|
||||
setUseTls(data.use_tls || false);
|
||||
setUseSsl(data.use_ssl || false);
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error.response && error.response.status === 404) {
|
||||
showNotification(
|
||||
"Les données SMTP n'ont pas été trouvées.",
|
||||
'warning',
|
||||
'Attention'
|
||||
);
|
||||
} else {
|
||||
logger.error(
|
||||
'Erreur lors du chargement des paramètres SMTP:',
|
||||
error
|
||||
);
|
||||
showNotification(
|
||||
'Erreur lors du chargement des paramètres SMTP.',
|
||||
'error',
|
||||
'Erreur'
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [activeTab, csrfToken]); // Ajouter csrfToken comme dépendance
|
||||
|
||||
const handleSmtpServerChange = (e) => {
|
||||
setSmtpServer(e.target.value);
|
||||
@ -47,34 +93,43 @@ export default function SettingsPage() {
|
||||
setSmtpPassword(e.target.value);
|
||||
};
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
if (password !== confirmPassword) {
|
||||
alert('Les mots de passe ne correspondent pas');
|
||||
return;
|
||||
}
|
||||
// Logique pour mettre à jour l'email et le mot de passe
|
||||
console.log('Email:', email);
|
||||
console.log('Password:', password);
|
||||
};
|
||||
|
||||
const handleSmtpSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
// Logique pour mettre à jour les paramètres SMTP
|
||||
console.log('SMTP Server:', smtpServer);
|
||||
console.log('SMTP Port:', smtpPort);
|
||||
console.log('SMTP User:', smtpUser);
|
||||
console.log('SMTP Password:', smtpPassword);
|
||||
const smtpData = {
|
||||
establishment: selectedEstablishmentId,
|
||||
smtp_server: smtpServer,
|
||||
smtp_port: smtpPort,
|
||||
smtp_user: smtpUser,
|
||||
smtp_password: smtpPassword,
|
||||
use_tls: useTls,
|
||||
use_ssl: useSsl,
|
||||
};
|
||||
|
||||
editSmtpSettings(smtpData, csrfToken) // Passer le csrfToken ici
|
||||
.then(() => {
|
||||
showNotification(
|
||||
'Paramètres SMTP mis à jour avec succès.',
|
||||
'success',
|
||||
'Succès'
|
||||
);
|
||||
logger.debug('SMTP Settings Updated:', smtpData);
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error(
|
||||
'Erreur lors de la mise à jour des paramètres SMTP:',
|
||||
error
|
||||
);
|
||||
showNotification(
|
||||
'Erreur lors de la mise à jour des paramètres SMTP.',
|
||||
'error',
|
||||
'Erreur'
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-8">
|
||||
<div className="flex space-x-4 mb-4">
|
||||
<Tab
|
||||
text="Informations de la structure"
|
||||
active={activeTab === 'structure'}
|
||||
onClick={() => handleTabClick('structure')}
|
||||
/>
|
||||
<Tab
|
||||
text="Paramètres SMTP"
|
||||
active={activeTab === 'smtp'}
|
||||
@ -82,21 +137,55 @@ export default function SettingsPage() {
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<TabContent isActive={activeTab === 'structure'}>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<InputText label="Email" value={email} onChange={handleEmailChange} />
|
||||
<InputText label="Mot de passe" type="password" value={password} onChange={handlePasswordChange} />
|
||||
<InputText label="Confirmer le mot de passe" type="password" value={confirmPassword} onChange={handleConfirmPasswordChange} />
|
||||
<Button type="submit" primary text="Mettre à jour"></Button>
|
||||
</form>
|
||||
</TabContent>
|
||||
<TabContent isActive={activeTab === 'smtp'}>
|
||||
<form onSubmit={handleSmtpSubmit}>
|
||||
<InputText label="Serveur SMTP" value={smtpServer} onChange={handleSmtpServerChange} />
|
||||
<InputText label="Port SMTP" value={smtpPort} onChange={handleSmtpPortChange} />
|
||||
<InputText label="Utilisateur SMTP" value={smtpUser} onChange={handleSmtpUserChange} />
|
||||
<InputText label="Mot de passe SMTP" type="password" value={smtpPassword} onChange={handleSmtpPasswordChange} />
|
||||
<Button type="submit" primary text="Mettre à jour"></Button>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<InputText
|
||||
label="Serveur SMTP"
|
||||
value={smtpServer}
|
||||
onChange={handleSmtpServerChange}
|
||||
/>
|
||||
<InputText
|
||||
label="Port SMTP"
|
||||
value={smtpPort}
|
||||
onChange={handleSmtpPortChange}
|
||||
/>
|
||||
<InputText
|
||||
label="Utilisateur SMTP"
|
||||
value={smtpUser}
|
||||
onChange={handleSmtpUserChange}
|
||||
/>
|
||||
<InputText
|
||||
label="Mot de passe SMTP"
|
||||
type="password"
|
||||
value={smtpPassword}
|
||||
onChange={handleSmtpPasswordChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-6 border-t pt-4">
|
||||
<div className="flex items-center space-x-4">
|
||||
<CheckBox
|
||||
item={{ id: 'useTls' }}
|
||||
formData={{ useTls }}
|
||||
handleChange={() => setUseTls((prev) => !prev)} // Inverser la valeur booléenne
|
||||
fieldName="useTls"
|
||||
itemLabelFunc={() => 'Utiliser TLS'}
|
||||
/>
|
||||
<CheckBox
|
||||
item={{ id: 'useSsl' }}
|
||||
formData={{ useSsl }}
|
||||
handleChange={() => setUseSsl((prev) => !prev)} // Inverser la valeur booléenne
|
||||
fieldName="useSsl"
|
||||
itemLabelFunc={() => 'Utiliser SSL'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
type="submit"
|
||||
primary
|
||||
text="Mettre à jour"
|
||||
className="mt-6"
|
||||
></Button>
|
||||
</form>
|
||||
</TabContent>
|
||||
</div>
|
||||
|
||||
@ -0,0 +1,742 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Users, Layers, CheckCircle, Clock, XCircle } from 'lucide-react';
|
||||
import Table from '@/components/Table';
|
||||
import Popup from '@/components/Popup';
|
||||
import { fetchClasse } from '@/app/actions/schoolAction';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import logger from '@/utils/logger';
|
||||
import { useClasses } from '@/context/ClassesContext';
|
||||
import Button from '@/components/Button';
|
||||
import SelectChoice from '@/components/SelectChoice';
|
||||
import CheckBox from '@/components/CheckBox';
|
||||
import {
|
||||
fetchAbsences,
|
||||
createAbsences,
|
||||
editAbsences,
|
||||
deleteAbsences,
|
||||
} from '@/app/actions/subscriptionAction';
|
||||
|
||||
import { useCsrfToken } from '@/context/CsrfContext';
|
||||
import { useEstablishment } from '@/context/EstablishmentContext';
|
||||
import { useNotification } from '@/context/NotificationContext';
|
||||
|
||||
export default function Page() {
|
||||
const searchParams = useSearchParams();
|
||||
const { showNotification } = useNotification();
|
||||
const schoolClassId = searchParams.get('schoolClassId');
|
||||
const [classe, setClasse] = useState([]);
|
||||
const { getNiveauxLabels, getNiveauLabel } = useClasses();
|
||||
|
||||
const [popupVisible, setPopupVisible] = useState(false);
|
||||
const [popupMessage, setPopupMessage] = useState('');
|
||||
const [selectedLevels, setSelectedLevels] = useState([]); // Par défaut, tous les niveaux sont sélectionnés
|
||||
const [filteredStudents, setFilteredStudents] = useState([]);
|
||||
const [isEditingAttendance, setIsEditingAttendance] = useState(false); // État pour le mode édition
|
||||
const [attendance, setAttendance] = useState({}); // État pour les cases cochées
|
||||
const [fetchedAbsences, setFetchedAbsences] = useState({}); // Absences récupérées depuis le backend
|
||||
const [formAbsences, setFormAbsences] = useState({}); // Absences modifiées localement
|
||||
|
||||
const csrfToken = useCsrfToken();
|
||||
const { selectedEstablishmentId } = useEstablishment();
|
||||
|
||||
// AbsenceMoment constants
|
||||
const AbsenceMoment = {
|
||||
MORNING: { value: 1, label: 'Matinée' },
|
||||
AFTERNOON: { value: 2, label: 'Après-midi' },
|
||||
TOTAL: { value: 3, label: 'Journée' },
|
||||
};
|
||||
|
||||
// AbsenceReason constants
|
||||
const AbsenceReason = {
|
||||
JUSTIFIED_ABSENCE: { value: 1, label: 'Absence justifiée' },
|
||||
UNJUSTIFIED_ABSENCE: { value: 2, label: 'Absence non justifiée' },
|
||||
JUSTIFIED_LATE: { value: 3, label: 'Retard justifié' },
|
||||
UNJUSTIFIED_LATE: { value: 4, label: 'Retard non justifié' },
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// Récupérer les données de la classe et initialiser les élèves filtrés
|
||||
if (schoolClassId) {
|
||||
fetchClasse(schoolClassId)
|
||||
.then((classeData) => {
|
||||
logger.debug('Classes récupérées :', classeData);
|
||||
setClasse(classeData);
|
||||
setFilteredStudents(classeData.students); // Initialiser les élèves filtrés
|
||||
setSelectedLevels(getNiveauxLabels(classeData.levels)); // Initialiser les niveaux sélectionnés
|
||||
})
|
||||
.catch(requestErrorHandler);
|
||||
}
|
||||
}, [schoolClassId]);
|
||||
|
||||
useEffect(() => {
|
||||
// Récupérer les absences pour l'établissement sélectionné
|
||||
if (selectedEstablishmentId) {
|
||||
fetchAbsences(selectedEstablishmentId)
|
||||
.then((data) => {
|
||||
const absencesById = data.reduce((acc, absence) => {
|
||||
acc[absence.student] = absence;
|
||||
return acc;
|
||||
}, {});
|
||||
setFetchedAbsences(absencesById);
|
||||
})
|
||||
.catch((error) =>
|
||||
logger.error('Erreur lors de la récupération des absences :', error)
|
||||
);
|
||||
}
|
||||
}, [selectedEstablishmentId]);
|
||||
|
||||
useEffect(() => {
|
||||
// Filtrer les élèves en fonction des niveaux sélectionnés
|
||||
if (classe && selectedLevels.length > 0) {
|
||||
const filtered = classe.students.filter((student) =>
|
||||
selectedLevels.includes(getNiveauLabel(student.level))
|
||||
);
|
||||
setFilteredStudents(filtered);
|
||||
} else {
|
||||
setFilteredStudents([]); // Aucun élève si aucun niveau n'est sélectionné
|
||||
}
|
||||
}, [selectedLevels, classe]);
|
||||
|
||||
useEffect(() => {
|
||||
// Initialiser `attendance` et `formAbsences` en fonction des élèves filtrés et des absences
|
||||
if (filteredStudents.length > 0 && fetchedAbsences) {
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
|
||||
const initialAttendance = {};
|
||||
const initialFormAbsences = {};
|
||||
|
||||
filteredStudents.forEach((student) => {
|
||||
const existingAbsence =
|
||||
fetchedAbsences[student.id] &&
|
||||
fetchedAbsences[student.id].day === today
|
||||
? fetchedAbsences[student.id]
|
||||
: null;
|
||||
|
||||
if (existingAbsence) {
|
||||
// Si une absence existe pour aujourd'hui, décocher la case et pré-remplir les champs
|
||||
// Conversion reason -> type/justified
|
||||
let type = '';
|
||||
let justified = false;
|
||||
switch (existingAbsence.reason) {
|
||||
case AbsenceReason.JUSTIFIED_ABSENCE.value:
|
||||
type = 'absence';
|
||||
justified = true;
|
||||
break;
|
||||
case AbsenceReason.UNJUSTIFIED_ABSENCE.value:
|
||||
type = 'absence';
|
||||
justified = false;
|
||||
break;
|
||||
case AbsenceReason.JUSTIFIED_LATE.value:
|
||||
type = 'retard';
|
||||
justified = true;
|
||||
break;
|
||||
case AbsenceReason.UNJUSTIFIED_LATE.value:
|
||||
type = 'retard';
|
||||
justified = false;
|
||||
break;
|
||||
default:
|
||||
type = '';
|
||||
justified = false;
|
||||
}
|
||||
|
||||
initialAttendance[student.id] = false;
|
||||
initialFormAbsences[student.id] = {
|
||||
...existingAbsence,
|
||||
type,
|
||||
justified,
|
||||
};
|
||||
} else {
|
||||
// Sinon, cocher la case par défaut
|
||||
initialAttendance[student.id] = true;
|
||||
}
|
||||
});
|
||||
|
||||
setAttendance(initialAttendance);
|
||||
setFormAbsences(initialFormAbsences);
|
||||
}
|
||||
}, [filteredStudents, fetchedAbsences]);
|
||||
|
||||
const handleLevelClick = (label) => {
|
||||
setSelectedLevels(
|
||||
(prev) =>
|
||||
prev.includes(label)
|
||||
? prev.filter((level) => level !== label) // Retirer le niveau si déjà sélectionné
|
||||
: [...prev, label] // Ajouter le niveau si non sélectionné
|
||||
);
|
||||
};
|
||||
|
||||
const handleToggleAttendanceMode = () => {
|
||||
setIsEditingAttendance((prev) => !prev); // Basculer entre mode édition et visualisation
|
||||
};
|
||||
|
||||
const handleValidateAttendance = () => {
|
||||
let hasError = false;
|
||||
|
||||
// Pour chaque élève filtré (présents et absents)
|
||||
filteredStudents.forEach((student) => {
|
||||
const studentId = student.id;
|
||||
const isPresent = attendance[studentId];
|
||||
const existingAbsence = fetchedAbsences[studentId];
|
||||
|
||||
if (isPresent) {
|
||||
// Si l'élève est présent et qu'une absence existe, la supprimer
|
||||
if (existingAbsence) {
|
||||
deleteAbsences(existingAbsence.id, csrfToken)
|
||||
.then(() => {
|
||||
logger.debug(
|
||||
`Absence pour l'élève ${studentId} supprimée (présent).`
|
||||
);
|
||||
setFetchedAbsences((prev) => {
|
||||
const updatedAbsences = { ...prev };
|
||||
delete updatedAbsences[studentId];
|
||||
return updatedAbsences;
|
||||
});
|
||||
setFormAbsences((prev) => {
|
||||
const updatedAbsences = { ...prev };
|
||||
delete updatedAbsences[studentId];
|
||||
return updatedAbsences;
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error(
|
||||
`Erreur lors de la suppression de l'absence pour l'élève ${studentId}:`,
|
||||
error
|
||||
);
|
||||
showNotification(
|
||||
`Erreur lors de la suppression de l'absence pour l'élève ${studentId}`,
|
||||
'error',
|
||||
'Erreur'
|
||||
);
|
||||
});
|
||||
}
|
||||
// Si tu veux garder une trace de la présence, tu peux ici appeler une API ou enregistrer un "Présent"
|
||||
} else {
|
||||
// Si l'élève est absent, créer ou modifier l'absence
|
||||
const absenceData = formAbsences[studentId];
|
||||
if (!absenceData || !absenceData.type || !absenceData.moment) {
|
||||
logger.error(
|
||||
`Tous les champs requis doivent être fournis pour l'élève ${studentId}.`
|
||||
);
|
||||
showNotification(
|
||||
`Tous les champs requis doivent être fournis pour l'élève ${studentId}.`,
|
||||
'error',
|
||||
'Erreur'
|
||||
);
|
||||
hasError = true;
|
||||
// On ne fait pas de return ici, on continue la boucle pour les autres élèves
|
||||
} else {
|
||||
saveAbsence(studentId, absenceData);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// On ne quitte le mode édition que s'il n'y a pas d'erreur
|
||||
if (!hasError) {
|
||||
setIsEditingAttendance(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAttendanceChange = (studentId) => {
|
||||
const today = new Date().toISOString().split('T')[0]; // Obtenir la date actuelle au format YYYY-MM-DD
|
||||
|
||||
setAttendance((prev) => {
|
||||
const updatedAttendance = {
|
||||
...prev,
|
||||
[studentId]: !prev[studentId], // Inverser l'état de présence
|
||||
};
|
||||
|
||||
// Si l'élève est décoché (absent)
|
||||
if (!updatedAttendance[studentId]) {
|
||||
// Vérifier s'il existe une absence pour le jour actuel
|
||||
const existingAbsence = Object.values(fetchedAbsences).find(
|
||||
(absence) => absence.student === studentId && absence.day === today
|
||||
);
|
||||
|
||||
if (existingAbsence) {
|
||||
// Afficher l'absence existante pour le jour actuel
|
||||
setFormAbsences((prev) => ({
|
||||
...prev,
|
||||
[studentId]: {
|
||||
...existingAbsence,
|
||||
},
|
||||
}));
|
||||
} else {
|
||||
// Initialiser des champs vides pour créer une nouvelle absence
|
||||
setFormAbsences((prev) => ({
|
||||
...prev,
|
||||
[studentId]: {
|
||||
day: today,
|
||||
reason: null,
|
||||
moment: null,
|
||||
},
|
||||
}));
|
||||
}
|
||||
} else {
|
||||
// Si l'élève est recoché (présent), supprimer l'absence existante
|
||||
const existingAbsence = Object.values(fetchedAbsences).find(
|
||||
(absence) => absence.student === studentId && absence.day === today
|
||||
);
|
||||
|
||||
if (existingAbsence) {
|
||||
// Appeler la fonction pour supprimer l'absence
|
||||
deleteAbsences(existingAbsence.id, csrfToken)
|
||||
.then(() => {
|
||||
logger.debug(
|
||||
`Absence pour l'élève ${studentId} supprimée avec succès.`
|
||||
);
|
||||
// Mettre à jour les absences récupérées
|
||||
setFetchedAbsences((prev) => {
|
||||
const updatedAbsences = { ...prev };
|
||||
delete updatedAbsences[studentId];
|
||||
return updatedAbsences;
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error(
|
||||
`Erreur lors de la suppression de l'absence pour l'élève ${studentId}:`,
|
||||
error
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Supprimer les données d'absence dans `formAbsences`
|
||||
setFormAbsences((prev) => {
|
||||
const updatedAbsences = { ...prev };
|
||||
delete updatedAbsences[studentId];
|
||||
return updatedAbsences;
|
||||
});
|
||||
}
|
||||
|
||||
return updatedAttendance;
|
||||
});
|
||||
};
|
||||
|
||||
const getAbsenceReason = (type, justified) => {
|
||||
if (type === 'absence') {
|
||||
return justified
|
||||
? AbsenceReason.JUSTIFIED_ABSENCE.value
|
||||
: AbsenceReason.UNJUSTIFIED_ABSENCE.value;
|
||||
} else if (type === 'retard') {
|
||||
return justified
|
||||
? AbsenceReason.JUSTIFIED_LATE.value
|
||||
: AbsenceReason.UNJUSTIFIED_LATE.value;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const saveAbsence = (studentId, absenceData) => {
|
||||
if (!absenceData.type || !studentId || !absenceData.moment) {
|
||||
logger.error('Tous les champs requis doivent être fournis.');
|
||||
showNotification(
|
||||
'Tous les champs requis doivent être fournis.',
|
||||
'error',
|
||||
'Erreur'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const reason = getAbsenceReason(absenceData.type, absenceData.justified);
|
||||
|
||||
const payload = {
|
||||
student: studentId,
|
||||
day: absenceData.day,
|
||||
reason: reason,
|
||||
moment: absenceData.moment,
|
||||
establishment: selectedEstablishmentId,
|
||||
commentaire: absenceData.commentaire,
|
||||
};
|
||||
|
||||
if (absenceData.id) {
|
||||
// Modifier une absence existante
|
||||
editAbsences(absenceData.id, payload, csrfToken)
|
||||
.then(() => {
|
||||
logger.debug(
|
||||
`Absence pour l'élève ${studentId} modifiée avec succès.`
|
||||
);
|
||||
showNotification(
|
||||
'Opération effectuée avec succès.',
|
||||
'success',
|
||||
'Succès'
|
||||
);
|
||||
// Mettre à jour fetchedAbsences et formAbsences localement
|
||||
setFetchedAbsences((prev) => ({
|
||||
...prev,
|
||||
[studentId]: { ...prev[studentId], ...payload },
|
||||
}));
|
||||
setFormAbsences((prev) => ({
|
||||
...prev,
|
||||
[studentId]: { ...prev[studentId], ...payload },
|
||||
}));
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error(
|
||||
`Erreur lors de la modification de l'absence pour l'élève ${studentId}:`,
|
||||
error
|
||||
);
|
||||
});
|
||||
} else {
|
||||
// Créer une nouvelle absence
|
||||
createAbsences(payload, csrfToken)
|
||||
.then((response) => {
|
||||
logger.debug(`Absence pour l'élève ${studentId} créée avec succès.`);
|
||||
showNotification(
|
||||
'Opération effectuée avec succès.',
|
||||
'success',
|
||||
'Succès'
|
||||
);
|
||||
// Mettre à jour fetchedAbsences et formAbsences localement
|
||||
setFetchedAbsences((prev) => ({
|
||||
...prev,
|
||||
[studentId]: { id: response.id, ...payload },
|
||||
}));
|
||||
setFormAbsences((prev) => ({
|
||||
...prev,
|
||||
[studentId]: { id: response.id, ...payload },
|
||||
}));
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error(
|
||||
`Erreur lors de la création de l'absence pour l'élève ${studentId}:`,
|
||||
error
|
||||
);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const requestErrorHandler = (err) => {
|
||||
logger.error('Error fetching data:', err);
|
||||
};
|
||||
|
||||
const today = new Date().toISOString().split('T')[0]; // Obtenez la date actuelle au format YYYY-MM-DD
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
<h1 className="text-2xl font-bold">{classe?.atmosphere_name}</h1>
|
||||
|
||||
{/* Section Niveaux et Enseignants */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{/* Section Niveaux */}
|
||||
<div className="bg-white p-4 rounded-lg shadow-md">
|
||||
<h2 className="text-xl font-semibold mb-4 flex items-center">
|
||||
<Layers className="w-6 h-6 mr-2" />
|
||||
Niveaux
|
||||
</h2>
|
||||
<p className="text-sm text-gray-500 mb-4">
|
||||
Filtrer les élèves par niveau
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{classe?.levels?.length > 0 ? (
|
||||
getNiveauxLabels(classe.levels).map((label, index) => (
|
||||
<span
|
||||
key={index}
|
||||
onClick={() => handleLevelClick(label)} // Gérer le clic sur un niveau
|
||||
className={`px-4 py-2 rounded-full cursor-pointer border transition-all duration-200 ${
|
||||
selectedLevels.includes(label)
|
||||
? 'bg-emerald-200 text-emerald-800 border-emerald-300 shadow-md'
|
||||
: 'bg-gray-200 text-gray-800 border-gray-300 hover:bg-gray-300'
|
||||
}`}
|
||||
>
|
||||
{selectedLevels.includes(label) ? (
|
||||
<span className="flex items-center gap-2">
|
||||
<CheckCircle className="w-4 h-4 text-emerald-600" />
|
||||
{label}
|
||||
</span>
|
||||
) : (
|
||||
label
|
||||
)}
|
||||
</span>
|
||||
))
|
||||
) : (
|
||||
<span className="text-gray-500">Aucun niveau associé</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section Enseignants */}
|
||||
<div className="bg-white p-4 rounded-lg shadow-md">
|
||||
<h2 className="text-xl font-semibold mb-4 flex items-center">
|
||||
<Users className="w-6 h-6 mr-2" />
|
||||
Enseignants
|
||||
</h2>
|
||||
<p className="text-sm text-gray-500 mb-4">Liste des enseignants</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{classe?.teachers_details?.map((teacher) => (
|
||||
<span
|
||||
key={teacher.id}
|
||||
className="px-3 py-1 bg-emerald-200 rounded-full text-emerald-800"
|
||||
>
|
||||
{teacher.last_name} {teacher.first_name}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Affichage de la date du jour */}
|
||||
<div className="flex justify-between items-center mb-4 bg-white p-4 rounded-lg shadow-md">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="flex items-center justify-center w-10 h-10 bg-emerald-100 text-emerald-600 rounded-full">
|
||||
<Clock className="w-6 h-6" />
|
||||
</div>
|
||||
<h2 className="text-lg font-semibold text-gray-800">
|
||||
Appel du jour :{' '}
|
||||
<span className="ml-2 text-emerald-600">{today}</span>
|
||||
</h2>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
{!isEditingAttendance ? (
|
||||
<Button
|
||||
text="Faire l'appel"
|
||||
onClick={handleToggleAttendanceMode}
|
||||
primary
|
||||
className="px-4 py-2 bg-emerald-500 text-white rounded-lg shadow hover:bg-emerald-600 transition-all"
|
||||
/>
|
||||
) : (
|
||||
<Button
|
||||
text="Valider l'appel"
|
||||
onClick={handleValidateAttendance}
|
||||
primary
|
||||
className="px-4 py-2 bg-emerald-500 text-white rounded-lg shadow hover:bg-emerald-600 transition-all"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Table
|
||||
columns={[
|
||||
{
|
||||
name: 'Nom',
|
||||
transform: (row) => (
|
||||
<div className="text-center">{row.last_name}</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'Prénom',
|
||||
transform: (row) => (
|
||||
<div className="text-center">{row.first_name}</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'Niveau',
|
||||
transform: (row) => (
|
||||
<div className="text-center">{getNiveauLabel(row.level)}</div>
|
||||
),
|
||||
},
|
||||
...(isEditingAttendance
|
||||
? [
|
||||
{
|
||||
name: "Gestion de l'appel",
|
||||
transform: (row) => (
|
||||
<div className="flex flex-col gap-2 items-center">
|
||||
{/* Présence */}
|
||||
<div className="flex items-center gap-2">
|
||||
{attendance[row.id] ? (
|
||||
<>
|
||||
<CheckBox
|
||||
item={{ id: row.id }}
|
||||
formData={{
|
||||
attendance: attendance[row.id] ? [row.id] : [],
|
||||
}}
|
||||
handleChange={() =>
|
||||
handleAttendanceChange(row.id)
|
||||
}
|
||||
fieldName="attendance"
|
||||
/>
|
||||
<span className="text-sm font-medium text-gray-700">
|
||||
Présent
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{/* Icône croix pour remettre l'élève en présent */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleAttendanceChange(row.id)}
|
||||
className="text-red-500 hover:text-red-700 transition"
|
||||
title="Annuler l'absence"
|
||||
>
|
||||
<XCircle className="w-6 h-6" />
|
||||
</button>
|
||||
<span className="text-sm font-medium text-red-600">
|
||||
Effacer l'absence
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Détails absence/retard */}
|
||||
{!attendance[row.id] && (
|
||||
<div className="w-full bg-emerald-50 border border-emerald-100 rounded-lg p-3 mt-2 shadow-sm">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Clock className="w-4 h-4 text-emerald-500" />
|
||||
<span className="font-semibold text-emerald-700 text-sm">
|
||||
Motif d'absence
|
||||
</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-2 items-center">
|
||||
{/* Select Absence/Retard */}
|
||||
<SelectChoice
|
||||
name={`type-${row.id}`}
|
||||
label=""
|
||||
placeHolder="Type"
|
||||
selected={formAbsences[row.id]?.type || ''}
|
||||
callback={(e) =>
|
||||
setFormAbsences((prev) => ({
|
||||
...prev,
|
||||
[row.id]: {
|
||||
...prev[row.id],
|
||||
type: e.target.value,
|
||||
},
|
||||
}))
|
||||
}
|
||||
choices={[
|
||||
{ value: 'absence', label: 'Absence' },
|
||||
{ value: 'retard', label: 'Retard' },
|
||||
]}
|
||||
/>
|
||||
|
||||
{/* Select Moment */}
|
||||
<SelectChoice
|
||||
name={`moment-${row.id}`}
|
||||
label=""
|
||||
placeHolder="Durée"
|
||||
selected={formAbsences[row.id]?.moment || ''}
|
||||
callback={(e) =>
|
||||
setFormAbsences((prev) => ({
|
||||
...prev,
|
||||
[row.id]: {
|
||||
...prev[row.id],
|
||||
moment: parseInt(e.target.value, 10),
|
||||
},
|
||||
}))
|
||||
}
|
||||
choices={Object.values(AbsenceMoment).map(
|
||||
(moment) => ({
|
||||
value: moment.value,
|
||||
label: moment.label,
|
||||
})
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Nouveau champ commentaire */}
|
||||
<input
|
||||
type="text"
|
||||
className="border rounded px-2 py-1 text-sm w-full"
|
||||
placeholder="Commentaire"
|
||||
value={formAbsences[row.id]?.commentaire || ''}
|
||||
onChange={(e) =>
|
||||
setFormAbsences((prev) => ({
|
||||
...prev,
|
||||
[row.id]: {
|
||||
...prev[row.id],
|
||||
commentaire: e.target.value,
|
||||
},
|
||||
}))
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Checkbox Justifié */}
|
||||
<div className="flex items-center gap-2 justify-center">
|
||||
<CheckBox
|
||||
item={{ id: `justified-${row.id}` }}
|
||||
formData={{
|
||||
justified: !!formAbsences[row.id]?.justified,
|
||||
}}
|
||||
handleChange={() =>
|
||||
setFormAbsences((prev) => ({
|
||||
...prev,
|
||||
[row.id]: {
|
||||
...prev[row.id],
|
||||
justified: !prev[row.id]?.justified,
|
||||
},
|
||||
}))
|
||||
}
|
||||
fieldName="justified"
|
||||
itemLabelFunc={() => 'Justifié'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
name: 'Statut',
|
||||
transform: (row) => {
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
const absence =
|
||||
formAbsences[row.id] ||
|
||||
Object.values(fetchedAbsences).find(
|
||||
(absence) =>
|
||||
absence.student === row.id && absence.day === today
|
||||
);
|
||||
|
||||
if (!absence) {
|
||||
return (
|
||||
<div className="text-center text-green-500 flex justify-center items-center gap-2">
|
||||
<CheckCircle className="w-5 h-5" />
|
||||
Présent
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
switch (absence.reason) {
|
||||
case AbsenceReason.JUSTIFIED_LATE.value:
|
||||
return (
|
||||
<div className="text-center text-yellow-500 flex justify-center items-center gap-2">
|
||||
<Clock className="w-5 h-5" />
|
||||
Retard justifié
|
||||
</div>
|
||||
);
|
||||
case AbsenceReason.UNJUSTIFIED_LATE.value:
|
||||
return (
|
||||
<div className="text-center text-red-500 flex justify-center items-center gap-2">
|
||||
<Clock className="w-5 h-5" />
|
||||
Retard non justifié
|
||||
</div>
|
||||
);
|
||||
case AbsenceReason.JUSTIFIED_ABSENCE.value:
|
||||
return (
|
||||
<div className="text-center text-blue-500 flex justify-center items-center gap-2">
|
||||
<CheckCircle className="w-5 h-5" />
|
||||
Absence justifiée
|
||||
</div>
|
||||
);
|
||||
case AbsenceReason.UNJUSTIFIED_ABSENCE.value:
|
||||
return (
|
||||
<div className="text-center text-red-500 flex justify-center items-center gap-2">
|
||||
<CheckCircle className="w-5 h-5" />
|
||||
Absence non justifiée
|
||||
</div>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<div className="text-center text-gray-500 flex justify-center items-center gap-2">
|
||||
<CheckCircle className="w-5 h-5" />
|
||||
Statut inconnu
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
]),
|
||||
]}
|
||||
data={filteredStudents} // Utiliser les élèves filtrés
|
||||
/>
|
||||
|
||||
{/* Popup */}
|
||||
<Popup
|
||||
isOpen={popupVisible}
|
||||
message={popupMessage}
|
||||
onConfirm={() => setPopupVisible(false)}
|
||||
onCancel={() => setPopupVisible(false)}
|
||||
uniqueConfirmButton={true}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,125 +1,262 @@
|
||||
'use client'
|
||||
'use client';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { School, Calendar } from 'lucide-react';
|
||||
import TabsStructure from '@/components/Structure/Configuration/TabsStructure';
|
||||
import ScheduleManagement from '@/components/Structure/Planning/ScheduleManagement'
|
||||
import StructureManagement from '@/components/Structure/Configuration/StructureManagement'
|
||||
import { BK_GESTIONENSEIGNANTS_SPECIALITES_URL,
|
||||
BK_GESTIONENSEIGNANTS_CLASSES_URL,
|
||||
BK_GESTIONENSEIGNANTS_TEACHERS_URL,
|
||||
BK_GESTIONENSEIGNANTS_PLANNINGS_URL } from '@/utils/Url';
|
||||
import DjangoCSRFToken from '@/components/DjangoCSRFToken'
|
||||
import useCsrfToken from '@/hooks/useCsrfToken';
|
||||
|
||||
import StructureManagement from '@/components/Structure/Configuration/StructureManagement';
|
||||
import ScheduleManagement from '@/components/Structure/Planning/ScheduleManagement';
|
||||
import FeesManagement from '@/components/Structure/Tarification/FeesManagement';
|
||||
import DjangoCSRFToken from '@/components/DjangoCSRFToken';
|
||||
import { useCsrfToken } from '@/context/CsrfContext';
|
||||
import { ClassesProvider } from '@/context/ClassesContext';
|
||||
import {
|
||||
createDatas,
|
||||
updateDatas,
|
||||
removeDatas,
|
||||
fetchSpecialities,
|
||||
fetchTeachers,
|
||||
fetchClasses,
|
||||
fetchRegistrationDiscounts,
|
||||
fetchTuitionDiscounts,
|
||||
fetchRegistrationFees,
|
||||
fetchTuitionFees,
|
||||
fetchRegistrationPaymentPlans,
|
||||
fetchTuitionPaymentPlans,
|
||||
fetchRegistrationPaymentModes,
|
||||
fetchTuitionPaymentModes,
|
||||
fetchEstablishmentCompetencies,
|
||||
} from '@/app/actions/schoolAction';
|
||||
import { fetchProfiles } from '@/app/actions/authAction';
|
||||
import SidebarTabs from '@/components/SidebarTabs';
|
||||
import FilesGroupsManagement from '@/components/Structure/Files/FilesGroupsManagement';
|
||||
import { fetchRegistrationSchoolFileMasters } from '@/app/actions/registerFileGroupAction';
|
||||
import logger from '@/utils/logger';
|
||||
import { useEstablishment } from '@/context/EstablishmentContext';
|
||||
import { PlanningProvider, PlanningModes } from '@/context/PlanningContext';
|
||||
import CompetenciesList from '@/components/Structure/Competencies/CompetenciesList';
|
||||
|
||||
export default function Page() {
|
||||
const [specialities, setSpecialities] = useState([]);
|
||||
const [classes, setClasses] = useState([]);
|
||||
const [teachers, setTeachers] = useState([]);
|
||||
const [schedules, setSchedules] = useState([]);
|
||||
const [activeTab, setActiveTab] = useState('Configuration');
|
||||
const tabs = [
|
||||
{ id: 'Configuration', title: "Configuration de l'école", icon: School },
|
||||
{ id: 'Schedule', title: "Gestion de l'emploi du temps", icon: Calendar },
|
||||
];
|
||||
const [registrationDiscounts, setRegistrationDiscounts] = useState([]);
|
||||
const [tuitionDiscounts, setTuitionDiscounts] = useState([]);
|
||||
const [registrationFees, setRegistrationFees] = useState([]);
|
||||
const [tuitionFees, setTuitionFees] = useState([]);
|
||||
const [fichiers, setFichiers] = useState([]);
|
||||
const [registrationPaymentPlans, setRegistrationPaymentPlans] = useState([]);
|
||||
const [tuitionPaymentPlans, setTuitionPaymentPlans] = useState([]);
|
||||
const [registrationPaymentModes, setRegistrationPaymentModes] = useState([]);
|
||||
const [tuitionPaymentModes, setTuitionPaymentModes] = useState([]);
|
||||
const [profiles, setProfiles] = useState([]);
|
||||
const [establishmentCompetencies, setEstablishmentCompetencies] = useState(
|
||||
[]
|
||||
);
|
||||
|
||||
const csrfToken = useCsrfToken();
|
||||
const { selectedEstablishmentId, apiDocuseal } = useEstablishment();
|
||||
|
||||
useEffect(() => {
|
||||
// Fetch data for specialities
|
||||
fetchSpecialities();
|
||||
if (selectedEstablishmentId) {
|
||||
// Fetch data for specialities
|
||||
handleSpecialities();
|
||||
|
||||
// Fetch data for teachers
|
||||
fetchTeachers();
|
||||
// Fetch data for teachers
|
||||
handleTeachers();
|
||||
|
||||
// Fetch data for classes
|
||||
fetchClasses();
|
||||
|
||||
// Fetch data for schedules
|
||||
fetchSchedules();
|
||||
}, []);
|
||||
// Fetch data for classes
|
||||
handleClasses();
|
||||
|
||||
const fetchSpecialities = () => {
|
||||
fetch(`${BK_GESTIONENSEIGNANTS_SPECIALITES_URL}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
// Fetch data for registration discounts
|
||||
handleRegistrationDiscounts();
|
||||
|
||||
// Fetch data for tuition discounts
|
||||
handleTuitionDiscounts();
|
||||
|
||||
// Fetch data for registration fees
|
||||
handleRegistrationFees();
|
||||
|
||||
// Fetch data for tuition fees
|
||||
handleTuitionFees();
|
||||
|
||||
// Fetch data for registration file schoolFileTemplates
|
||||
fetchRegistrationSchoolFileMasters(selectedEstablishmentId)
|
||||
.then((data) => {
|
||||
setFichiers(data);
|
||||
})
|
||||
.catch((error) => logger.error('Error fetching files:', error));
|
||||
|
||||
// Fetch data for registration payment plans
|
||||
handleRegistrationPaymentPlans();
|
||||
|
||||
// Fetch data for tuition payment plans
|
||||
handleTuitionPaymentPlans();
|
||||
|
||||
// Fetch data for registration payment modes
|
||||
handleRegistrationPaymentModes();
|
||||
|
||||
// Fetch data for tuition payment modes
|
||||
handleTuitionPaymentModes();
|
||||
|
||||
fetchProfiles()
|
||||
.then((data) => {
|
||||
setProfiles(data);
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error('Error fetching profileRoles:', error);
|
||||
});
|
||||
|
||||
// Fetch data for establishment competencies
|
||||
handleEstablishmentCompetencies();
|
||||
}
|
||||
}, [selectedEstablishmentId]);
|
||||
|
||||
const handleEstablishmentCompetencies = (cycle = 1) => {
|
||||
fetchEstablishmentCompetencies(selectedEstablishmentId, cycle)
|
||||
.then((data) => {
|
||||
setEstablishmentCompetencies(data);
|
||||
})
|
||||
.catch((error) =>
|
||||
logger.error('Error fetching setEstablishmentCompetencies:', error)
|
||||
);
|
||||
};
|
||||
|
||||
const handleSpecialities = () => {
|
||||
fetchSpecialities(selectedEstablishmentId)
|
||||
.then((data) => {
|
||||
setSpecialities(data);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching specialities:', error);
|
||||
});
|
||||
.catch((error) => logger.error('Error fetching specialities:', error));
|
||||
};
|
||||
|
||||
const fetchTeachers = () => {
|
||||
fetch(`${BK_GESTIONENSEIGNANTS_TEACHERS_URL}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const handleTeachers = () => {
|
||||
fetchTeachers(selectedEstablishmentId)
|
||||
.then((data) => {
|
||||
setTeachers(data);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching teachers:', error);
|
||||
});
|
||||
.catch((error) => logger.error('Error fetching teachers:', error));
|
||||
};
|
||||
|
||||
const fetchClasses = () => {
|
||||
fetch(`${BK_GESTIONENSEIGNANTS_CLASSES_URL}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const handleClasses = () => {
|
||||
fetchClasses(selectedEstablishmentId)
|
||||
.then((data) => {
|
||||
setClasses(data);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching classes:', error);
|
||||
});
|
||||
.catch((error) => logger.error('Error fetching classes:', error));
|
||||
};
|
||||
|
||||
const fetchSchedules = () => {
|
||||
fetch(`${BK_GESTIONENSEIGNANTS_PLANNINGS_URL}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
setSchedules(data);
|
||||
const handleRegistrationDiscounts = () => {
|
||||
fetchRegistrationDiscounts(selectedEstablishmentId)
|
||||
.then((data) => {
|
||||
setRegistrationDiscounts(data);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching classes:', error);
|
||||
});
|
||||
.catch((error) =>
|
||||
logger.error('Error fetching registration discounts:', error)
|
||||
);
|
||||
};
|
||||
|
||||
const handleTuitionDiscounts = () => {
|
||||
fetchTuitionDiscounts(selectedEstablishmentId)
|
||||
.then((data) => {
|
||||
setTuitionDiscounts(data);
|
||||
})
|
||||
.catch((error) =>
|
||||
logger.error('Error fetching tuition discounts:', error)
|
||||
);
|
||||
};
|
||||
|
||||
const handleRegistrationFees = () => {
|
||||
fetchRegistrationFees(selectedEstablishmentId)
|
||||
.then((data) => {
|
||||
setRegistrationFees(data);
|
||||
})
|
||||
.catch((error) =>
|
||||
logger.error('Error fetching registration fees:', error)
|
||||
);
|
||||
};
|
||||
|
||||
const handleTuitionFees = () => {
|
||||
fetchTuitionFees(selectedEstablishmentId)
|
||||
.then((data) => {
|
||||
setTuitionFees(data);
|
||||
})
|
||||
.catch((error) => logger.error('Error fetching tuition fees', error));
|
||||
};
|
||||
|
||||
const handleRegistrationPaymentPlans = () => {
|
||||
fetchRegistrationPaymentPlans(selectedEstablishmentId)
|
||||
.then((data) => {
|
||||
setRegistrationPaymentPlans(data);
|
||||
})
|
||||
.catch((error) =>
|
||||
logger.error('Error fetching registration payment plans:', error)
|
||||
);
|
||||
};
|
||||
|
||||
const handleTuitionPaymentPlans = () => {
|
||||
fetchTuitionPaymentPlans(selectedEstablishmentId)
|
||||
.then((data) => {
|
||||
setTuitionPaymentPlans(data);
|
||||
})
|
||||
.catch((error) =>
|
||||
logger.error('Error fetching tuition payment plans:', error)
|
||||
);
|
||||
};
|
||||
|
||||
const handleRegistrationPaymentModes = () => {
|
||||
fetchRegistrationPaymentModes(selectedEstablishmentId)
|
||||
.then((data) => {
|
||||
setRegistrationPaymentModes(data);
|
||||
})
|
||||
.catch((error) =>
|
||||
logger.error('Error fetching registration payment modes:', error)
|
||||
);
|
||||
};
|
||||
|
||||
const handleTuitionPaymentModes = () => {
|
||||
fetchTuitionPaymentModes(selectedEstablishmentId)
|
||||
.then((data) => {
|
||||
setTuitionPaymentModes(data);
|
||||
})
|
||||
.catch((error) =>
|
||||
logger.error('Error fetching tuition payment modes:', error)
|
||||
);
|
||||
};
|
||||
|
||||
const handleCreate = (url, newData, setDatas) => {
|
||||
fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken
|
||||
},
|
||||
body: JSON.stringify(newData),
|
||||
credentials: 'include'
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
console.log('Succes :', data);
|
||||
setDatas(prevState => [...prevState, data]);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Erreur :', error);
|
||||
});
|
||||
return createDatas(url, newData, csrfToken)
|
||||
.then((data) => {
|
||||
setDatas((prevState) => [...prevState, data]);
|
||||
return data;
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error('Error creating data:', error);
|
||||
throw error;
|
||||
});
|
||||
};
|
||||
|
||||
const handleEdit = (url, id, updatedData, setDatas) => {
|
||||
fetch(`${url}/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken
|
||||
},
|
||||
body: JSON.stringify(updatedData),
|
||||
credentials: 'include'
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
setDatas(prevState => prevState.map(item => item.id === id ? data : item));
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Erreur :', error);
|
||||
});
|
||||
return updateDatas(url, id, updatedData, csrfToken)
|
||||
.then((data) => {
|
||||
setDatas((prevState) =>
|
||||
prevState.map((item) => (item.id === id ? data : item))
|
||||
);
|
||||
return data;
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error('Error editing data:', error);
|
||||
throw error;
|
||||
});
|
||||
};
|
||||
|
||||
const handleDelete = (url, id, setDatas) => {
|
||||
return removeDatas(url, id, csrfToken)
|
||||
.then((data) => {
|
||||
setDatas((prevState) => prevState.filter((item) => item.id !== id));
|
||||
return data;
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error('Error deleting data:', error);
|
||||
throw error;
|
||||
});
|
||||
};
|
||||
|
||||
const handleUpdatePlanning = (url, planningId, updatedData) => {
|
||||
@ -127,72 +264,123 @@ export default function Page() {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken
|
||||
'X-CSRFToken': csrfToken,
|
||||
},
|
||||
body: JSON.stringify(updatedData),
|
||||
credentials: 'include'
|
||||
credentials: 'include',
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
console.log('Planning mis à jour avec succès :', data);
|
||||
//setDatas(data);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Erreur :', error);
|
||||
});
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
logger.debug('Planning mis à jour avec succès :', data);
|
||||
//setDatas(data);
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error('Erreur :', error);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
const handleDelete = (url, id, setDatas) => {
|
||||
fetch(`${url}/${id}`, {
|
||||
method:'DELETE',
|
||||
headers: {
|
||||
'Content-Type':'application/json',
|
||||
'X-CSRFToken': csrfToken
|
||||
},
|
||||
credentials: 'include'
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
console.log('Success:', data);
|
||||
setDatas(prevState => prevState.filter(item => item.id !== id));
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching data:', error);
|
||||
error = error.errorMessage;
|
||||
console.log(error);
|
||||
});
|
||||
};
|
||||
const tabs = [
|
||||
{
|
||||
id: 'Configuration',
|
||||
label: 'Classes',
|
||||
content: (
|
||||
<div className="h-full overflow-y-auto p-4">
|
||||
<StructureManagement
|
||||
specialities={specialities}
|
||||
setSpecialities={setSpecialities}
|
||||
teachers={teachers}
|
||||
setTeachers={setTeachers}
|
||||
classes={classes}
|
||||
setClasses={setClasses}
|
||||
profiles={profiles}
|
||||
handleCreate={handleCreate}
|
||||
handleEdit={handleEdit}
|
||||
handleDelete={handleDelete}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'Schedule',
|
||||
label: 'Emploi du temps',
|
||||
content: (
|
||||
<div className="h-full overflow-y-auto p-4">
|
||||
<ClassesProvider>
|
||||
<ScheduleManagement
|
||||
handleUpdatePlanning={handleUpdatePlanning}
|
||||
classes={classes}
|
||||
specialities={specialities}
|
||||
teachers={teachers}
|
||||
/>
|
||||
</ClassesProvider>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'Fees',
|
||||
label: 'Tarifs',
|
||||
content: (
|
||||
<div className="h-full overflow-y-auto p-4">
|
||||
<FeesManagement
|
||||
registrationDiscounts={registrationDiscounts}
|
||||
setRegistrationDiscounts={setRegistrationDiscounts}
|
||||
tuitionDiscounts={tuitionDiscounts}
|
||||
setTuitionDiscounts={setTuitionDiscounts}
|
||||
registrationFees={registrationFees}
|
||||
setRegistrationFees={setRegistrationFees}
|
||||
tuitionFees={tuitionFees}
|
||||
setTuitionFees={setTuitionFees}
|
||||
registrationPaymentPlans={registrationPaymentPlans}
|
||||
setRegistrationPaymentPlans={setRegistrationPaymentPlans}
|
||||
tuitionPaymentPlans={tuitionPaymentPlans}
|
||||
setTuitionPaymentPlans={setTuitionPaymentPlans}
|
||||
registrationPaymentModes={registrationPaymentModes}
|
||||
setRegistrationPaymentModes={setRegistrationPaymentModes}
|
||||
tuitionPaymentModes={tuitionPaymentModes}
|
||||
setTuitionPaymentModes={setTuitionPaymentModes}
|
||||
handleCreate={handleCreate}
|
||||
handleEdit={handleEdit}
|
||||
handleDelete={handleDelete}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'Files',
|
||||
label: 'Documents',
|
||||
content: (
|
||||
<div className="h-full overflow-y-auto p-4">
|
||||
<FilesGroupsManagement
|
||||
csrfToken={csrfToken}
|
||||
selectedEstablishmentId={selectedEstablishmentId}
|
||||
apiDocuseal={apiDocuseal}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'Competencies',
|
||||
label: 'Compétences',
|
||||
content: (
|
||||
<div className="h-full overflow-y-auto p-4">
|
||||
<CompetenciesList
|
||||
establishmentCompetencies={establishmentCompetencies}
|
||||
onChangeCycle={handleEstablishmentCompetencies}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className='p-8'>
|
||||
<DjangoCSRFToken csrfToken={csrfToken} />
|
||||
|
||||
<TabsStructure activeTab={activeTab} setActiveTab={setActiveTab} tabs={tabs} />
|
||||
|
||||
{activeTab === 'Configuration' && (
|
||||
<>
|
||||
<StructureManagement
|
||||
specialities={specialities}
|
||||
setSpecialities={setSpecialities}
|
||||
teachers={teachers}
|
||||
setTeachers={setTeachers}
|
||||
classes={classes}
|
||||
setClasses={setClasses}
|
||||
handleCreate={handleCreate}
|
||||
handleEdit={handleEdit}
|
||||
handleDelete={handleDelete} />
|
||||
</>
|
||||
)}
|
||||
|
||||
{activeTab === 'Schedule' && (
|
||||
<ClassesProvider>
|
||||
<ScheduleManagement
|
||||
handleUpdatePlanning={handleUpdatePlanning}
|
||||
classes={classes}
|
||||
/>
|
||||
</ClassesProvider>
|
||||
)}
|
||||
</div>
|
||||
<>
|
||||
<PlanningProvider
|
||||
establishmentId={selectedEstablishmentId}
|
||||
modeSet={PlanningModes.CLASS_SCHEDULE}
|
||||
>
|
||||
<DjangoCSRFToken csrfToken={csrfToken} />
|
||||
<SidebarTabs tabs={tabs} />
|
||||
</PlanningProvider>
|
||||
</>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@ -1,77 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Upload } from 'lucide-react';
|
||||
|
||||
export default function FileUpload({ onFileUpload }) {
|
||||
const [dragActive, setDragActive] = useState(false);
|
||||
const [fileName, setFileName] = useState('');
|
||||
const [file, setFile] = useState(null);
|
||||
|
||||
const handleDragOver = (event) => {
|
||||
event.preventDefault();
|
||||
setDragActive(true);
|
||||
};
|
||||
|
||||
const handleDragLeave = () => {
|
||||
setDragActive(false);
|
||||
};
|
||||
|
||||
const handleDrop = (event) => {
|
||||
event.preventDefault();
|
||||
setDragActive(false);
|
||||
const droppedFile = event.dataTransfer.files[0];
|
||||
setFile(droppedFile);
|
||||
setFileName(droppedFile.name.replace(/\.[^/.]+$/, ""));
|
||||
};
|
||||
|
||||
const handleFileChange = (event) => {
|
||||
const selectedFile = event.target.files[0];
|
||||
setFile(selectedFile);
|
||||
setFileName(selectedFile.name.replace(/\.[^/.]+$/, ""));
|
||||
};
|
||||
|
||||
const handleFileNameChange = (event) => {
|
||||
setFileName(event.target.value);
|
||||
};
|
||||
|
||||
const handleUpload = () => {
|
||||
if (file) {
|
||||
onFileUpload(file, fileName);
|
||||
setFile(null);
|
||||
setFileName('');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
className={`border-2 border-dashed p-8 rounded-md ${dragActive ? 'border-blue-500' : 'border-gray-300'} flex flex-col items-center justify-center`}
|
||||
style={{ height: '200px' }}
|
||||
>
|
||||
<input type="file" onChange={handleFileChange} className="hidden" id="fileInput" />
|
||||
<label htmlFor="fileInput" className="cursor-pointer flex flex-col items-center">
|
||||
<Upload size={48} className="text-gray-400 mb-2" />
|
||||
<p className="text-center">{fileName || 'Glissez et déposez un fichier ici ou cliquez ici pour sélectionner un fichier'}</p>
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex mt-2">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Nom du fichier"
|
||||
value={fileName}
|
||||
onChange={handleFileNameChange}
|
||||
className="flex-grow p-2 border border-gray-200 rounded-md"
|
||||
/>
|
||||
<button
|
||||
onClick={handleUpload}
|
||||
className={`p-2 rounded-md shadow transition duration-200 ml-2 ${file ? 'bg-emerald-600 text-white hover:bg-emerald-900' : 'bg-gray-300 text-gray-500 cursor-not-allowed'}`}
|
||||
disabled={!file}
|
||||
>
|
||||
Ajouter
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,91 +0,0 @@
|
||||
'use client'
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import InscriptionFormShared from '@/components/Inscription/InscriptionFormShared';
|
||||
import { FR_ADMIN_SUBSCRIPTIONS_URL,
|
||||
BK_GESTIONINSCRIPTION_ELEVE_URL,
|
||||
BK_GESTIONINSCRIPTION_FICHEINSCRIPTION_URL } from '@/utils/Url';
|
||||
import useCsrfToken from '@/hooks/useCsrfToken';
|
||||
import { mockStudent } from '@/data/mockStudent';
|
||||
|
||||
const useFakeData = process.env.NEXT_PUBLIC_USE_FAKE_DATA === 'true';
|
||||
|
||||
export default function Page() {
|
||||
const searchParams = useSearchParams();
|
||||
const idProfil = searchParams.get('id');
|
||||
const idEleve = searchParams.get('idEleve'); // Changé de codeDI à idEleve
|
||||
|
||||
const [initialData, setInitialData] = useState(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const csrfToken = useCsrfToken();
|
||||
|
||||
useEffect(() => {
|
||||
if (useFakeData) {
|
||||
setInitialData(mockStudent);
|
||||
setIsLoading(false);
|
||||
} else {
|
||||
fetch(`${BK_GESTIONINSCRIPTION_ELEVE_URL}/${idEleve}`) // Utilisation de idEleve au lieu de codeDI
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
console.log('Fetched data:', data); // Pour le débogage
|
||||
const formattedData = {
|
||||
id: data.id,
|
||||
nom: data.nom,
|
||||
prenom: data.prenom,
|
||||
adresse: data.adresse,
|
||||
dateNaissance: data.dateNaissance,
|
||||
lieuNaissance: data.lieuNaissance,
|
||||
codePostalNaissance: data.codePostalNaissance,
|
||||
nationalite: data.nationalite,
|
||||
medecinTraitant: data.medecinTraitant,
|
||||
niveau: data.niveau,
|
||||
responsables: data.responsables || []
|
||||
};
|
||||
setInitialData(formattedData);
|
||||
setIsLoading(false);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching student data:', error);
|
||||
setIsLoading(false);
|
||||
});
|
||||
}
|
||||
}, [idEleve]); // Dépendance changée à idEleve
|
||||
|
||||
const handleSubmit = async (data) => {
|
||||
if (useFakeData) {
|
||||
console.log('Fake submit:', data);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${BK_GESTIONINSCRIPTION_FICHEINSCRIPTION_URL}/${idEleve}`, { // Utilisation de idEleve
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken
|
||||
},
|
||||
credentials: 'include',
|
||||
body: JSON.stringify(data),
|
||||
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
console.log('Success:', result);
|
||||
// Redirection après succès
|
||||
window.location.href = FR_ADMIN_SUBSCRIPTIONS_URL;
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
alert('Une erreur est survenue lors de la mise à jour des données');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<InscriptionFormShared
|
||||
initialData={initialData}
|
||||
csrfToken={csrfToken}
|
||||
onSubmit={handleSubmit}
|
||||
cancelUrl={FR_ADMIN_SUBSCRIPTIONS_URL}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,69 @@
|
||||
'use client';
|
||||
import React, { useState } from 'react';
|
||||
import { useSearchParams, useRouter } from 'next/navigation';
|
||||
import InscriptionFormShared from '@/components/Inscription/InscriptionFormShared';
|
||||
import { FE_ADMIN_SUBSCRIPTIONS_URL } from '@/utils/Url';
|
||||
import { useCsrfToken } from '@/context/CsrfContext';
|
||||
import { useEstablishment } from '@/context/EstablishmentContext';
|
||||
import { editRegisterForm } from '@/app/actions/subscriptionAction';
|
||||
import logger from '@/utils/logger';
|
||||
import Loader from '@/components/Loader';
|
||||
import { useNotification } from '@/context/NotificationContext';
|
||||
|
||||
export default function Page() {
|
||||
const router = useRouter();
|
||||
const { showNotification } = useNotification();
|
||||
const searchParams = useSearchParams();
|
||||
const studentId = searchParams.get('studentId');
|
||||
const enable = searchParams.get('enabled') === 'true';
|
||||
|
||||
const [formErrors, setFormErrors] = useState({});
|
||||
const csrfToken = useCsrfToken();
|
||||
const { selectedEstablishmentId, apiDocuseal } = useEstablishment();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const handleSubmit = (data) => {
|
||||
setIsLoading(true);
|
||||
editRegisterForm(studentId, data, csrfToken)
|
||||
.then((result) => {
|
||||
setIsLoading(false);
|
||||
logger.debug('Success:', result);
|
||||
showNotification(
|
||||
"Dossier d'inscription soumis avec succès",
|
||||
'success',
|
||||
'Succès'
|
||||
);
|
||||
router.push(FE_ADMIN_SUBSCRIPTIONS_URL);
|
||||
})
|
||||
.catch((error) => {
|
||||
setIsLoading(false);
|
||||
logger.error('Error:', error.message);
|
||||
showNotification(
|
||||
"Erreur lors de la soumission du dossier d'inscription",
|
||||
'error',
|
||||
'Erreur'
|
||||
);
|
||||
if (error.details) {
|
||||
logger.error('Form errors:', error.details);
|
||||
setFormErrors(error.details);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return <Loader />;
|
||||
}
|
||||
|
||||
return (
|
||||
<InscriptionFormShared
|
||||
studentId={studentId}
|
||||
csrfToken={csrfToken}
|
||||
selectedEstablishmentId={selectedEstablishmentId}
|
||||
apiDocuseal = {apiDocuseal}
|
||||
onSubmit={handleSubmit}
|
||||
cancelUrl={FE_ADMIN_SUBSCRIPTIONS_URL}
|
||||
errors={formErrors}
|
||||
enable={enable}
|
||||
/>
|
||||
);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,102 @@
|
||||
'use client';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useSearchParams, useRouter } from 'next/navigation';
|
||||
import ValidateSubscription from '@/components/Inscription/ValidateSubscription';
|
||||
import { editRegisterForm } from '@/app/actions/subscriptionAction';
|
||||
import { fetchClasses } from '@/app/actions/schoolAction';
|
||||
import { useCsrfToken } from '@/context/CsrfContext';
|
||||
import logger from '@/utils/logger';
|
||||
import Loader from '@/components/Loader';
|
||||
import { useEstablishment } from '@/context/EstablishmentContext';
|
||||
import { FE_ADMIN_SUBSCRIPTIONS_URL } from '@/utils/Url';
|
||||
import { useNotification } from '@/context/NotificationContext';
|
||||
|
||||
export default function Page() {
|
||||
const searchParams = useSearchParams();
|
||||
const router = useRouter();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
// Récupérer les paramètres de la requête
|
||||
const studentId = searchParams.get('studentId');
|
||||
const firstName = searchParams.get('firstName');
|
||||
const lastName = searchParams.get('lastName');
|
||||
const level = searchParams.get('level');
|
||||
const sepa_file =
|
||||
searchParams.get('sepa_file') === 'null'
|
||||
? null
|
||||
: searchParams.get('sepa_file');
|
||||
const student_file = searchParams.get('student_file');
|
||||
|
||||
const [classes, setClasses] = useState([]);
|
||||
|
||||
const csrfToken = useCsrfToken();
|
||||
const { selectedEstablishmentId } = useEstablishment();
|
||||
const { showNotification } = useNotification();
|
||||
|
||||
const requestErrorHandler = (err) => {
|
||||
logger.error('Error fetching data:', err);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedEstablishmentId) {
|
||||
setIsLoading(true);
|
||||
fetchClasses(selectedEstablishmentId)
|
||||
.then((classesData) => {
|
||||
logger.debug('Classes récupérées :', classesData);
|
||||
|
||||
// Filtrer les classes par niveau
|
||||
const filteredClasses = classesData.filter(
|
||||
(classe) => classe.levels.includes(parseInt(level, 10)) // Vérifier si le niveau de l'étudiant est dans les niveaux de la classe
|
||||
);
|
||||
|
||||
setClasses(filteredClasses); // Mettre à jour les classes filtrées
|
||||
setIsLoading(false);
|
||||
})
|
||||
.catch(requestErrorHandler);
|
||||
}
|
||||
}, [selectedEstablishmentId]);
|
||||
|
||||
const handleAcceptRF = (data) => {
|
||||
const formData = new FormData();
|
||||
formData.append('data', JSON.stringify(data));
|
||||
|
||||
setIsLoading(true);
|
||||
// Appeler l'API pour mettre à jour le RF
|
||||
editRegisterForm(studentId, formData, csrfToken)
|
||||
.then((response) => {
|
||||
logger.debug('RF mis à jour avec succès:', response);
|
||||
showNotification(
|
||||
'Le dossier d\'inscription a été validé avec succès',
|
||||
'success',
|
||||
'Succès'
|
||||
);
|
||||
router.push(FE_ADMIN_SUBSCRIPTIONS_URL);
|
||||
setIsLoading(false);
|
||||
})
|
||||
.catch((error) => {
|
||||
showNotification(
|
||||
"Erreur lors de la validation du dossier d'inscription",
|
||||
'error',
|
||||
'Erreur'
|
||||
);
|
||||
setIsLoading(false);
|
||||
logger.error('Erreur lors de la mise à jour du RF:', error);
|
||||
});
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return <Loader />;
|
||||
}
|
||||
|
||||
return (
|
||||
<ValidateSubscription
|
||||
studentId={studentId}
|
||||
firstName={firstName}
|
||||
lastName={lastName}
|
||||
sepa_file={sepa_file}
|
||||
student_file={student_file}
|
||||
onAccept={handleAcceptRF}
|
||||
classes={classes}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -1,23 +0,0 @@
|
||||
'use client'
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import Button from '@/components/Button';
|
||||
import { MoreVertical, Send, Edit, Trash2, FileText, ChevronUp, UserPlus } from 'lucide-react';
|
||||
import Modal from '@/components/Modal';
|
||||
|
||||
export default function Page() {
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const openModal = () => {
|
||||
setIsOpen(true);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<div className='p-8'>
|
||||
|
||||
<Button text={"addTeacher"} primary onClick={openModal} icon={<UserPlus size={20} />} />
|
||||
<Modal isOpen={isOpen} setIsOpen={setIsOpen} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
'use client'
|
||||
import {useTranslations} from 'next-intl';
|
||||
'use client';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import React from 'react';
|
||||
import Button from '@/components/Button';
|
||||
import Logo from '@/components/Logo'; // Import du composant Logo
|
||||
|
||||
@ -1,113 +0,0 @@
|
||||
'use client'
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import InscriptionFormShared from '@/components/Inscription/InscriptionFormShared';
|
||||
import { useSearchParams, redirect, useRouter } from 'next/navigation';
|
||||
import useCsrfToken from '@/hooks/useCsrfToken';
|
||||
import { FR_PARENTS_HOME_URL,
|
||||
BK_GESTIONINSCRIPTION_ELEVE_URL,
|
||||
BK_GESTIONINSCRIPTION_FICHEINSCRIPTION_URL,
|
||||
BK_GESTIONINSCRIPTION_RECUPEREDERNIER_RESPONSABLE_URL } from '@/utils/Url';
|
||||
import { mockStudent } from '@/data/mockStudent';
|
||||
|
||||
const useFakeData = process.env.NEXT_PUBLIC_USE_FAKE_DATA === 'true';
|
||||
|
||||
export default function Page() {
|
||||
const searchParams = useSearchParams();
|
||||
const idProfil = searchParams.get('id');
|
||||
const idEleve = searchParams.get('idEleve');
|
||||
const router = useRouter();
|
||||
|
||||
const [initialData, setInitialData] = useState(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const csrfToken = useCsrfToken();
|
||||
const [currentProfil, setCurrentProfil] = useState("");
|
||||
const [lastIdResponsable, setLastIdResponsable] = useState(1);
|
||||
|
||||
useEffect(() => {
|
||||
if (!idEleve || !idProfil) {
|
||||
console.error('Missing idEleve or idProfil');
|
||||
return;
|
||||
}
|
||||
|
||||
if (useFakeData) {
|
||||
setInitialData(mockStudent);
|
||||
setLastIdResponsable(999);
|
||||
setIsLoading(false);
|
||||
} else {
|
||||
Promise.all([
|
||||
// Fetch eleve data
|
||||
fetch(`${BK_GESTIONINSCRIPTION_ELEVE_URL}/${idEleve}`),
|
||||
// Fetch last responsable ID
|
||||
fetch(BK_GESTIONINSCRIPTION_RECUPEREDERNIER_RESPONSABLE_URL)
|
||||
])
|
||||
.then(async ([eleveResponse, responsableResponse]) => {
|
||||
const eleveData = await eleveResponse.json();
|
||||
const responsableData = await responsableResponse.json();
|
||||
|
||||
const formattedData = {
|
||||
id: eleveData.id,
|
||||
nom: eleveData.nom,
|
||||
prenom: eleveData.prenom,
|
||||
adresse: eleveData.adresse,
|
||||
dateNaissance: eleveData.dateNaissance,
|
||||
lieuNaissance: eleveData.lieuNaissance,
|
||||
codePostalNaissance: eleveData.codePostalNaissance,
|
||||
nationalite: eleveData.nationalite,
|
||||
medecinTraitant: eleveData.medecinTraitant,
|
||||
niveau: eleveData.niveau,
|
||||
responsables: eleveData.responsables || []
|
||||
};
|
||||
|
||||
setInitialData(formattedData);
|
||||
setLastIdResponsable(responsableData.lastid);
|
||||
|
||||
let profils = eleveData.profils;
|
||||
const currentProf = profils.find(profil => profil.id === idProfil);
|
||||
if (currentProf) {
|
||||
setCurrentProfil(currentProf);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching data:', error);
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false);
|
||||
});
|
||||
}
|
||||
}, [idEleve, idProfil]);
|
||||
|
||||
const handleSubmit = async (data) => {
|
||||
if (useFakeData) {
|
||||
console.log('Fake submit:', data);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${BK_GESTIONINSCRIPTION_FICHEINSCRIPTION_URL}/${idEleve}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken
|
||||
},
|
||||
credentials: 'include',
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
console.log('Success:', result);
|
||||
router.push(FR_PARENTS_HOME_URL);
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<InscriptionFormShared
|
||||
initialData={initialData}
|
||||
csrfToken={csrfToken}
|
||||
onSubmit={handleSubmit}
|
||||
cancelUrl={FR_PARENTS_HOME_URL}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
);
|
||||
}
|
||||
62
Front-End/src/app/[locale]/parents/editSubscription/page.js
Normal file
62
Front-End/src/app/[locale]/parents/editSubscription/page.js
Normal file
@ -0,0 +1,62 @@
|
||||
'use client';
|
||||
import React, { useState } from 'react';
|
||||
import InscriptionFormShared from '@/components/Inscription/InscriptionFormShared';
|
||||
import { useSearchParams, useRouter } from 'next/navigation';
|
||||
import { useCsrfToken } from '@/context/CsrfContext';
|
||||
import { useEstablishment } from '@/context/EstablishmentContext';
|
||||
import { FE_PARENTS_HOME_URL } from '@/utils/Url';
|
||||
import { editRegisterForm } from '@/app/actions/subscriptionAction';
|
||||
import logger from '@/utils/logger';
|
||||
import Loader from '@/components/Loader';
|
||||
import { useNotification } from '@/context/NotificationContext';
|
||||
|
||||
export default function Page() {
|
||||
const searchParams = useSearchParams();
|
||||
const { showNotification } = useNotification();
|
||||
const studentId = searchParams.get('studentId');
|
||||
const enable = searchParams.get('enabled') === 'true';
|
||||
const router = useRouter();
|
||||
const csrfToken = useCsrfToken();
|
||||
const { selectedEstablishmentId, apiDocuseal } = useEstablishment();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const handleSubmit = (data) => {
|
||||
setIsLoading(true);
|
||||
editRegisterForm(studentId, data, csrfToken)
|
||||
.then((result) => {
|
||||
setIsLoading(false);
|
||||
logger.debug('Success:', result);
|
||||
showNotification(
|
||||
"Dossier d'inscription soumis avec succès",
|
||||
'success',
|
||||
'Succès'
|
||||
);
|
||||
router.push(FE_PARENTS_HOME_URL);
|
||||
})
|
||||
.catch((error) => {
|
||||
setIsLoading(false);
|
||||
showNotification(
|
||||
"Erreur lors de la soumission du dossier d'inscription",
|
||||
'error',
|
||||
'Erreur'
|
||||
);
|
||||
logger.error('Error:', error);
|
||||
});
|
||||
};
|
||||
|
||||
if (isLoading === true) {
|
||||
return <Loader />; // Affichez le composant Loader
|
||||
}
|
||||
|
||||
return (
|
||||
<InscriptionFormShared
|
||||
studentId={studentId}
|
||||
csrfToken={csrfToken}
|
||||
selectedEstablishmentId={selectedEstablishmentId}
|
||||
apiDocuseal = {apiDocuseal}
|
||||
onSubmit={handleSubmit}
|
||||
cancelUrl={FE_PARENTS_HOME_URL}
|
||||
enable={enable}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -1,92 +1,123 @@
|
||||
'use client'
|
||||
'use client';
|
||||
// src/components/Layout.js
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import DropdownMenu from '@/components/DropdownMenu';
|
||||
import { useRouter } from 'next/navigation'; // Ajout de l'importation
|
||||
import { Bell, User, MessageSquare, LogOut, Settings, Home } from 'lucide-react'; // Ajout de l'importation de l'icône Home
|
||||
import Logo from '@/components/Logo'; // Ajout de l'importation du composant Logo
|
||||
import { FR_PARENTS_HOME_URL,FR_PARENTS_MESSAGERIE_URL,FR_PARENTS_SETTINGS_URL, BK_GESTIONINSCRIPTION_MESSAGES_URL } from '@/utils/Url'; // Ajout de l'importation de l'URL de la page d'accueil parent
|
||||
import useLocalStorage from '@/hooks/useLocalStorage';
|
||||
import Sidebar from '@/components/Sidebar';
|
||||
import { useRouter, usePathname } from 'next/navigation';
|
||||
import { MessageSquare, Settings, Home, Menu } from 'lucide-react';
|
||||
import {
|
||||
FE_PARENTS_HOME_URL,
|
||||
FE_PARENTS_MESSAGERIE_URL
|
||||
} from '@/utils/Url';
|
||||
import ProtectedRoute from '@/components/ProtectedRoute';
|
||||
import { disconnect } from '@/app/actions/authAction';
|
||||
import Popup from '@/components/Popup';
|
||||
import { RIGHTS } from '@/utils/rights';
|
||||
import { useEstablishment } from '@/context/EstablishmentContext';
|
||||
import Footer from '@/components/Footer';
|
||||
|
||||
export default function Layout({
|
||||
children,
|
||||
}) {
|
||||
export default function Layout({ children }) {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const [isPopupVisible, setIsPopupVisible] = useState(false);
|
||||
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
||||
const { clearContext } = useEstablishment();
|
||||
const softwareName = 'N3WT School';
|
||||
const softwareVersion = `${process.env.NEXT_PUBLIC_APP_VERSION}`;
|
||||
|
||||
const router = useRouter(); // Définition de router
|
||||
const [messages, setMessages] = useState([]);
|
||||
const [userId, setUserId] = useLocalStorage("userId", '') ;
|
||||
// Vérifier si on est sur la page messagerie
|
||||
const isMessagingPage = pathname?.includes('/messagerie');
|
||||
|
||||
// Configuration des éléments de la sidebar pour les parents
|
||||
const sidebarItems = [
|
||||
{
|
||||
id: 'home',
|
||||
name: 'Accueil',
|
||||
url: FE_PARENTS_HOME_URL,
|
||||
icon: Home,
|
||||
},
|
||||
{
|
||||
id: 'messagerie',
|
||||
name: 'Messagerie',
|
||||
url: FE_PARENTS_MESSAGERIE_URL,
|
||||
icon: MessageSquare,
|
||||
}
|
||||
];
|
||||
|
||||
// Déterminer la page actuelle pour la sidebar
|
||||
const getCurrentPage = () => {
|
||||
if (pathname?.includes('/messagerie')) return 'messagerie';
|
||||
if (pathname?.includes('/settings')) return 'settings';
|
||||
return 'home';
|
||||
};
|
||||
|
||||
const currentPage = getCurrentPage();
|
||||
|
||||
const handleDisconnect = () => {
|
||||
setIsPopupVisible(true);
|
||||
};
|
||||
|
||||
const confirmDisconnect = () => {
|
||||
setIsPopupVisible(false);
|
||||
disconnect();
|
||||
clearContext();
|
||||
};
|
||||
|
||||
const toggleSidebar = () => {
|
||||
setIsSidebarOpen(!isSidebarOpen);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setUserId(userId);
|
||||
fetch(`${BK_GESTIONINSCRIPTION_MESSAGES_URL}/${userId}`, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}).then(response => response.json())
|
||||
.then(data => {
|
||||
if (data) {
|
||||
setMessages(data);
|
||||
}
|
||||
console.log('Success :', data);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching data:', error);
|
||||
});
|
||||
|
||||
}, []);
|
||||
// Fermer la sidebar quand on change de page sur mobile
|
||||
setIsSidebarOpen(false);
|
||||
}, [pathname]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col min-h-screen bg-gray-50">
|
||||
{/* Entête */}
|
||||
<header className="bg-white border-b border-gray-200 px-8 py-4 flex items-center justify-between fixed top-0 left-0 right-0 z-10">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Logo className="h-8 w-8" /> {/* Utilisation du composant Logo */}
|
||||
<ProtectedRoute requiredRight={RIGHTS.PARENT}>
|
||||
{/* Bouton hamburger pour mobile */}
|
||||
<button
|
||||
onClick={toggleSidebar}
|
||||
className="fixed top-4 left-4 z-40 p-2 rounded-md bg-white shadow-lg border border-gray-200 md:hidden"
|
||||
>
|
||||
<Menu size={20} />
|
||||
</button>
|
||||
|
||||
<div className="text-xl font-semibold">Accueil</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-4">
|
||||
<button
|
||||
className="p-2 rounded-full hover:bg-gray-200"
|
||||
onClick={() => { router.push(FR_PARENTS_HOME_URL); }} // Utilisation de router pour revenir à l'accueil parent
|
||||
>
|
||||
<Home />
|
||||
</button>
|
||||
|
||||
|
||||
<div className="relative">
|
||||
<button
|
||||
className="p-2 rounded-full hover:bg-gray-200"
|
||||
onClick={() => { router.push(FR_PARENTS_MESSAGERIE_URL); }} // Utilisation de router
|
||||
>
|
||||
<MessageSquare />
|
||||
|
||||
</button>
|
||||
{messages.length > 0 && (
|
||||
<span className="absolute top-0 right-0 block h-2 w-2 rounded-full bg-emerald-600"></span>
|
||||
)}
|
||||
</div>
|
||||
<DropdownMenu
|
||||
buttonContent={<User />}
|
||||
items={[
|
||||
{ label: 'Se déconnecter', icon: LogOut, onClick: () => {} },
|
||||
{ label: 'Settings', icon: Settings , onClick: () => { router.push(FR_PARENTS_SETTINGS_URL); } }
|
||||
]}
|
||||
buttonClassName="p-2 rounded-full hover:bg-gray-200"
|
||||
menuClassName="absolute right-0 mt-2 w-48 bg-white border border-gray-200 rounded-md shadow-lg"
|
||||
|
||||
/>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Content */}
|
||||
<div className="pt-20 p-8 flex-1"> {/* Ajout de flex-1 pour utiliser toute la hauteur disponible */}
|
||||
{children}
|
||||
</div>
|
||||
{/* Sidebar */}
|
||||
<div
|
||||
className={`absolute top-0 bottom-0 left-0 z-30 w-64 bg-white border-r border-gray-200 box-border ${
|
||||
isSidebarOpen ? 'block' : 'hidden md:block'
|
||||
}`}
|
||||
>
|
||||
<Sidebar
|
||||
currentPage={currentPage}
|
||||
items={sidebarItems}
|
||||
onCloseMobile={toggleSidebar}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
|
||||
{/* Overlay for mobile */}
|
||||
{isSidebarOpen && (
|
||||
<div
|
||||
className="fixed inset-0 bg-black bg-opacity-50 z-20 md:hidden"
|
||||
onClick={toggleSidebar}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Main container */}
|
||||
<div
|
||||
className={`absolute overflow-auto bg-gradient-to-br from-emerald-50 via-sky-50 to-emerald-100 top-0 bottom-16 left-0 md:left-64 right-0 ${!isMessagingPage ? 'p-4 md:p-8' : ''}`}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<Footer softwareName={softwareName} softwareVersion={softwareVersion} />
|
||||
|
||||
<Popup
|
||||
isOpen={isPopupVisible}
|
||||
message="Êtes-vous sûr(e) de vouloir vous déconnecter ?"
|
||||
onConfirm={confirmDisconnect}
|
||||
onCancel={() => setIsPopupVisible(false)}
|
||||
/>
|
||||
</ProtectedRoute>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -1,106 +1,28 @@
|
||||
'use client'
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { SendHorizontal } from 'lucide-react';
|
||||
|
||||
const contacts = [
|
||||
{ id: 1, name: 'Facturation', profilePic: 'https://i.pravatar.cc/32' },
|
||||
{ id: 2, name: 'Enseignant 1', profilePic: 'https://i.pravatar.cc/32' },
|
||||
{ id: 3, name: 'Contact', profilePic: 'https://i.pravatar.cc/32' },
|
||||
];
|
||||
'use client';
|
||||
import React from 'react';
|
||||
import InstantChat from '@/components/Chat/InstantChat';
|
||||
import { useEstablishment } from '@/context/EstablishmentContext';
|
||||
|
||||
export default function MessageriePage() {
|
||||
const [selectedContact, setSelectedContact] = useState(null);
|
||||
const [messages, setMessages] = useState({});
|
||||
const [newMessage, setNewMessage] = useState('');
|
||||
const messagesEndRef = useRef(null);
|
||||
const { user, selectedEstablishmentId } = useEstablishment();
|
||||
|
||||
const scrollToBottom = () => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
scrollToBottom();
|
||||
}, [messages]);
|
||||
|
||||
const handleSendMessage = () => {
|
||||
if (newMessage.trim() && selectedContact) {
|
||||
const contactMessages = messages[selectedContact.id] || [];
|
||||
setMessages({
|
||||
...messages,
|
||||
[selectedContact.id]: [...contactMessages, { id: contactMessages.length + 1, text: newMessage, date: new Date() }],
|
||||
});
|
||||
setNewMessage('');
|
||||
simulateContactResponse(selectedContact.id);
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyPress = (event) => {
|
||||
if (event.key === 'Enter') {
|
||||
handleSendMessage();
|
||||
}
|
||||
};
|
||||
|
||||
const simulateContactResponse = (contactId) => {
|
||||
setTimeout(() => {
|
||||
setMessages((prevMessages) => {
|
||||
const contactMessages = prevMessages[contactId] || [];
|
||||
return {
|
||||
...prevMessages,
|
||||
[contactId]: [...contactMessages, { id: contactMessages.length + 2, text: 'Réponse automatique', isResponse: true, date: new Date() }],
|
||||
};
|
||||
});
|
||||
}, 2000);
|
||||
};
|
||||
if (!user?.user_id || !selectedEstablishmentId) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-full w-full">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900 mx-auto mb-4"></div>
|
||||
<p className="text-gray-600">Chargement de la messagerie...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex" style={{ height: 'calc(100vh - 128px )' }}> {/* Utilisation de calc pour soustraire la hauteur de l'entête */}
|
||||
<div className="w-1/4 border-r border-gray-200 p-4 overflow-y-auto h-full ">
|
||||
{contacts.map((contact) => (
|
||||
<div
|
||||
key={contact.id}
|
||||
className={`p-2 cursor-pointer ${selectedContact?.id === contact.id ? 'bg-gray-200' : ''}`}
|
||||
onClick={() => setSelectedContact(contact)}
|
||||
>
|
||||
<img src={contact.profilePic} alt={`${contact.name}'s profile`} className="w-8 h-8 rounded-full inline-block mr-2" />
|
||||
{contact.name}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex-1 flex flex-col h-full">
|
||||
<div className="flex-1 overflow-y-auto p-4 h-full">
|
||||
{selectedContact && (messages[selectedContact.id] || []).map((message) => (
|
||||
<div
|
||||
key={message.id}
|
||||
className={`mb-2 p-2 rounded max-w-xs ${message.isResponse ? 'bg-gray-200 justify-self-end' : 'bg-emerald-200 justify-self-start'}`}
|
||||
style={{ borderRadius: message.isResponse ? '20px 20px 0 20px' : '20px 20px 20px 0', minWidth: '25%' }}
|
||||
>
|
||||
<div className="flex items-center mb-1">
|
||||
<img src={selectedContact.profilePic} alt={`${selectedContact.name}'s profile`} className="w-8 h-8 rounded-full inline-block mr-2" />
|
||||
<span className="text-xs text-gray-600">{selectedContact.name}</span>
|
||||
<span className="text-xs text-gray-400 ml-2">{new Date(message.date).toLocaleTimeString()}</span>
|
||||
</div>
|
||||
{message.text}
|
||||
</div>
|
||||
))}
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
<div className="p-4 border-t border-gray-200 flex">
|
||||
<input
|
||||
type="text"
|
||||
value={newMessage}
|
||||
onChange={(e) => setNewMessage(e.target.value)}
|
||||
className="w-full p-2 border border-gray-300 rounded"
|
||||
placeholder="Écrire un message..."
|
||||
onKeyDown={handleKeyPress}
|
||||
/>
|
||||
<button
|
||||
onClick={handleSendMessage}
|
||||
className="p-2 bg-emerald-500 text-white rounded mr-2"
|
||||
>
|
||||
<SendHorizontal />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-full flex flex-col">
|
||||
<InstantChat
|
||||
userProfileId={user.user_id}
|
||||
establishmentId={selectedEstablishmentId}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,90 +1,356 @@
|
||||
'use client'
|
||||
'use client';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import Table from '@/components/Table';
|
||||
import { Edit } from 'lucide-react';
|
||||
import StatusLabel from '@/components/StatusLabel';
|
||||
import useLocalStorage from '@/hooks/useLocalStorage';
|
||||
import { BK_GESTIONINSCRIPTION_ENFANTS_URL , FR_PARENTS_EDIT_INSCRIPTION_URL } from '@/utils/Url';
|
||||
import {
|
||||
Edit3,
|
||||
Users,
|
||||
Download,
|
||||
Eye,
|
||||
Upload,
|
||||
CalendarDays,
|
||||
} from 'lucide-react';
|
||||
import StatusLabel from '@/components/StatusLabel';
|
||||
import FileUpload from '@/components/FileUpload';
|
||||
import { FE_PARENTS_EDIT_SUBSCRIPTION_URL } from '@/utils/Url';
|
||||
import {
|
||||
fetchChildren,
|
||||
editRegisterForm,
|
||||
} from '@/app/actions/subscriptionAction';
|
||||
import { fetchUpcomingEvents } from '@/app/actions/planningAction';
|
||||
import logger from '@/utils/logger';
|
||||
import { BASE_URL } from '@/utils/Url';
|
||||
import { useEstablishment } from '@/context/EstablishmentContext';
|
||||
import { useCsrfToken } from '@/context/CsrfContext';
|
||||
import { useClasses } from '@/context/ClassesContext';
|
||||
import { PlanningProvider, PlanningModes } from '@/context/PlanningContext';
|
||||
import SectionHeader from '@/components/SectionHeader';
|
||||
import ParentPlanningSection from '@/components/ParentPlanningSection';
|
||||
import EventCard from '@/components/EventCard';
|
||||
|
||||
export default function ParentHomePage() {
|
||||
const [actions, setActions] = useState([]);
|
||||
const [children, setChildren] = useState([]);
|
||||
const [userId, setUserId] = useLocalStorage("userId", '') ;
|
||||
|
||||
const { user, selectedEstablishmentId } = useEstablishment();
|
||||
const [uploadingStudentId, setUploadingStudentId] = useState(null); // ID de l'étudiant pour l'upload
|
||||
const [uploadedFile, setUploadedFile] = useState(null); // Fichier uploadé
|
||||
const [uploadState, setUploadState] = useState('off'); // État "on" ou "off" pour l'affichage du composant
|
||||
const [showPlanning, setShowPlanning] = useState(false);
|
||||
const [planningClassName, setPlanningClassName] = useState(null);
|
||||
const [upcomingEvents, setUpcomingEvents] = useState([]);
|
||||
const router = useRouter();
|
||||
const csrfToken = useCsrfToken();
|
||||
const [reloadFetch, setReloadFetch] = useState(false);
|
||||
const { getNiveauLabel } = useClasses();
|
||||
|
||||
useEffect(() => {
|
||||
if (!userId) return;
|
||||
if (user !== null) {
|
||||
const userIdFromSession = user.user_id;
|
||||
fetchChildren(userIdFromSession, selectedEstablishmentId).then((data) => {
|
||||
setChildren(data);
|
||||
});
|
||||
setReloadFetch(false);
|
||||
}
|
||||
}, [selectedEstablishmentId, reloadFetch, user]);
|
||||
|
||||
const fetchActions = async () => {
|
||||
const response = await fetch('/api/actions');
|
||||
const data = await response.json();
|
||||
setActions(data);
|
||||
};
|
||||
useEffect(() => {
|
||||
if (selectedEstablishmentId) {
|
||||
// Fetch des événements à venir
|
||||
fetchUpcomingEvents(selectedEstablishmentId)
|
||||
.then((data) => {
|
||||
setUpcomingEvents(data);
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error('Error fetching upcoming events:', error);
|
||||
});
|
||||
}
|
||||
}, [selectedEstablishmentId]);
|
||||
|
||||
const fetchEleves = async () => {
|
||||
const response = await fetch(`${BK_GESTIONINSCRIPTION_ENFANTS_URL}/${userId}`);
|
||||
const data = await response.json();
|
||||
console.log(data);
|
||||
|
||||
setChildren(data);
|
||||
};
|
||||
|
||||
fetchEleves();
|
||||
}, [userId]);
|
||||
function handleView(eleveId) {
|
||||
logger.debug(`View dossier for student id: ${eleveId}`);
|
||||
router.push(
|
||||
`${FE_PARENTS_EDIT_SUBSCRIPTION_URL}?studentId=${eleveId}&enabled=false`
|
||||
);
|
||||
}
|
||||
|
||||
function handleEdit(eleveId) {
|
||||
// Logique pour éditer le dossier de l'élève
|
||||
console.log(`Edit dossier for eleve id: ${eleveId}`);
|
||||
router.push(`${FR_PARENTS_EDIT_INSCRIPTION_URL}?id=${userId}&idEleve=${eleveId}`);
|
||||
logger.debug(`Edit dossier for student id: ${eleveId}`);
|
||||
router.push(
|
||||
`${FE_PARENTS_EDIT_SUBSCRIPTION_URL}?studentId=${eleveId}&enabled=true`
|
||||
);
|
||||
}
|
||||
const actionColumns = [
|
||||
{ name: 'Action', transform: (row) => row.action },
|
||||
];
|
||||
|
||||
const getShadowColor = (etat) => {
|
||||
switch (etat) {
|
||||
case 1:
|
||||
return 'shadow-blue-500'; // Couleur d'ombre plus visible
|
||||
case 2:
|
||||
return 'shadow-orange-500'; // Couleur d'ombre plus visible
|
||||
case 3:
|
||||
return 'shadow-purple-500'; // Couleur d'ombre plus visible
|
||||
case 4:
|
||||
return 'shadow-red-500'; // Couleur d'ombre plus visible
|
||||
case 5:
|
||||
return 'shadow-green-500'; // Couleur d'ombre plus visible
|
||||
case 6:
|
||||
return 'shadow-red-500'; // Couleur d'ombre plus visible
|
||||
default:
|
||||
return 'shadow-green-500'; // Couleur d'ombre plus visible
|
||||
const handleFileUpload = (file) => {
|
||||
if (!file) {
|
||||
logger.error("Aucun fichier sélectionné pour l'upload.");
|
||||
return;
|
||||
}
|
||||
setUploadedFile(file); // Conserve le fichier en mémoire
|
||||
logger.debug('Fichier sélectionné :', file.name);
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!uploadedFile || !uploadingStudentId) {
|
||||
logger.error('Aucun fichier ou étudiant sélectionné.');
|
||||
return;
|
||||
}
|
||||
|
||||
const jsonData = {
|
||||
status: 3,
|
||||
};
|
||||
const formData = new FormData();
|
||||
formData.append('data', JSON.stringify(jsonData));
|
||||
formData.append('sepa_file', uploadedFile); // Ajoute le fichier SEPA
|
||||
|
||||
editRegisterForm(uploadingStudentId, formData, csrfToken)
|
||||
.then((response) => {
|
||||
logger.debug('RF mis à jour avec succès:', response);
|
||||
setReloadFetch(true);
|
||||
setUploadState('off');
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error('Erreur lors de la mise à jour du RF:', error);
|
||||
});
|
||||
};
|
||||
|
||||
const toggleUpload = (studentId) => {
|
||||
if (uploadingStudentId === studentId && uploadState === 'on') {
|
||||
// Si le composant est déjà affiché pour cet étudiant, on le masque
|
||||
setUploadState('off');
|
||||
setUploadingStudentId(null);
|
||||
setUploadedFile(null); // Réinitialise le fichier
|
||||
} else {
|
||||
// Sinon, on l'affiche pour cet étudiant
|
||||
setUploadState('on');
|
||||
setUploadingStudentId(studentId);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold mb-4">Dernières actions à effectuer</h2>
|
||||
<Table data={actions} columns={actionColumns} itemsPerPage={5} />
|
||||
</div>
|
||||
const showClassPlanning = (student) => {
|
||||
setPlanningClassName(
|
||||
`${student.associated_class_name} - ${getNiveauLabel(student.level)}`
|
||||
);
|
||||
setShowPlanning(true);
|
||||
};
|
||||
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold mb-4">Enfants</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{children.map((child) => (
|
||||
<div key={child.eleve.id} className={`border p-4 rounded shadow ${getShadowColor(child.etat)}`}>
|
||||
<div className="flex justify-between items-center">
|
||||
<h3 className="text-lg font-semibold">{child.eleve.nom} {child.eleve.prenom}</h3>
|
||||
<Edit className="cursor-pointer" onClick={() => handleEdit(child.eleve.id)} />
|
||||
</div>
|
||||
<StatusLabel etat={child.etat } showDropdown={false}/>
|
||||
const childrenColumns = [
|
||||
{
|
||||
name: 'photo',
|
||||
transform: (row) => (
|
||||
<div className="flex justify-center items-center">
|
||||
{row.student.photo ? (
|
||||
<a
|
||||
href={`${BASE_URL}${row.student.photo}`} // Lien vers la photo
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<img
|
||||
src={`${BASE_URL}${row.student.photo}`}
|
||||
alt={`${row.student.first_name} ${row.student.last_name}`}
|
||||
className="w-10 h-10 object-cover transition-transform duration-200 hover:scale-125 cursor-pointer rounded-full"
|
||||
/>
|
||||
</a>
|
||||
) : (
|
||||
<div className="w-10 h-10 flex items-center justify-center bg-gray-200 rounded-full">
|
||||
<span className="text-gray-500 text-sm font-semibold">
|
||||
{row.student.first_name[0]}
|
||||
{row.student.last_name[0]}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{ name: 'Nom', transform: (row) => `${row.student.last_name}` },
|
||||
{ name: 'Prénom', transform: (row) => `${row.student.first_name}` },
|
||||
{
|
||||
name: 'Classe',
|
||||
transform: (row) => (
|
||||
<div className="text-center">{row.student.associated_class_name}</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'Niveau',
|
||||
transform: (row) => (
|
||||
<div className="text-center">{getNiveauLabel(row.student.level)}</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'Statut',
|
||||
transform: (row) => (
|
||||
<div className="flex justify-center items-center">
|
||||
<StatusLabel status={row.status} showDropdown={false} parent />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'Actions',
|
||||
transform: (row) => (
|
||||
<div className="flex justify-center items-center gap-2">
|
||||
{row.status === 2 && (
|
||||
<button
|
||||
className="text-blue-500 hover:text-blue-700"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleEdit(row.student.id);
|
||||
}}
|
||||
aria-label="Remplir le dossier"
|
||||
>
|
||||
<Edit3 className="h-5 w-5" />
|
||||
</button>
|
||||
)}
|
||||
|
||||
{(row.status === 3 || row.status === 8) && (
|
||||
<button
|
||||
className="text-purple-500 hover:text-purple-700"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleView(row.student.id);
|
||||
}}
|
||||
aria-label="Visualiser le dossier"
|
||||
>
|
||||
<Eye className="h-5 w-5" />
|
||||
</button>
|
||||
)}
|
||||
|
||||
{row.status === 7 && (
|
||||
<>
|
||||
<button
|
||||
className="flex items-center justify-center w-8 h-8 rounded-full text-purple-500 hover:text-purple-700"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleView(row.student.id);
|
||||
}}
|
||||
aria-label="Visualiser le dossier"
|
||||
>
|
||||
<Eye className="h-5 w-5" />
|
||||
</button>
|
||||
<a
|
||||
href={`${BASE_URL}${row.sepa_file}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-center w-8 h-8 rounded-full text-green-500 hover:text-green-700"
|
||||
aria-label="Télécharger le mandat SEPA"
|
||||
>
|
||||
<Download className="h-5 w-5" />
|
||||
</a>
|
||||
<button
|
||||
className={`flex items-center justify-center w-8 h-8 rounded-full ${
|
||||
uploadingStudentId === row.student.id && uploadState === 'on'
|
||||
? 'bg-blue-100 text-blue-600 ring-3 ring-blue-500'
|
||||
: 'text-blue-500 hover:text-blue-700'
|
||||
}`}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
toggleUpload(row.student.id);
|
||||
}}
|
||||
aria-label="Uploader un fichier"
|
||||
>
|
||||
<Upload className="h-5 w-5" />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{row.status === 5 && (
|
||||
<>
|
||||
<button
|
||||
className="text-purple-500 hover:text-purple-700"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleView(row.student.id);
|
||||
}}
|
||||
aria-label="Visualiser le dossier"
|
||||
>
|
||||
<Eye className="h-5 w-5" />
|
||||
</button>
|
||||
<button
|
||||
className="text-emerald-500 hover:text-emerald-700 ml-1"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
showClassPlanning(row.student);
|
||||
}}
|
||||
aria-label="Voir le planning de la classe"
|
||||
>
|
||||
<CalendarDays className="h-5 w-5" />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="w-full h-full">
|
||||
{showPlanning && planningClassName ? (
|
||||
// Affichage grand format mais respectant la sidebar
|
||||
<>
|
||||
<div className="p-4 flex items-center border-b">
|
||||
<button
|
||||
className="text-emerald-600 hover:text-emerald-800 font-semibold flex items-center"
|
||||
onClick={() => setShowPlanning(false)}
|
||||
>
|
||||
← Retour
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex-1 flex overflow-hidden">
|
||||
<PlanningProvider
|
||||
establishmentId={selectedEstablishmentId}
|
||||
modeSet={PlanningModes.CLASS_SCHEDULE}
|
||||
readOnly={true}
|
||||
>
|
||||
<ParentPlanningSection planningClassName={planningClassName} />
|
||||
</PlanningProvider>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
// Affichage classique avec le tableau des enfants
|
||||
<div>
|
||||
{/* Section des événements à venir */}
|
||||
{upcomingEvents.length > 0 && (
|
||||
<div className="mb-6">
|
||||
<SectionHeader
|
||||
icon={CalendarDays}
|
||||
title="Événements à venir"
|
||||
description="Prochains événements de l'établissement"
|
||||
/>
|
||||
<div className="bg-stone-50 p-4 rounded-lg shadow-sm border border-gray-100">
|
||||
{upcomingEvents.slice(0, 3).map((event, index) => (
|
||||
<EventCard key={index} {...event} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<SectionHeader
|
||||
icon={Users}
|
||||
title="Vos enfants"
|
||||
description="Suivez le parcours de vos enfants"
|
||||
/>
|
||||
<div className="overflow-x-auto">
|
||||
<Table data={children} columns={childrenColumns} />
|
||||
</div>
|
||||
{/* Composant FileUpload et bouton Valider en dessous du tableau */}
|
||||
{uploadState === 'on' && uploadingStudentId && (
|
||||
<div className="mt-4">
|
||||
<FileUpload
|
||||
selectionMessage="Sélectionnez un fichier à uploader"
|
||||
onFileSelect={handleFileUpload}
|
||||
/>
|
||||
<button
|
||||
className={`mt-4 px-6 py-2 rounded-md ${
|
||||
uploadedFile
|
||||
? 'bg-emerald-500 text-white hover:bg-emerald-600'
|
||||
: 'bg-gray-300 text-gray-700 cursor-not-allowed'
|
||||
}`}
|
||||
onClick={handleSubmit}
|
||||
disabled={!uploadedFile}
|
||||
>
|
||||
Valider
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
}
|
||||
|
||||
@ -1,12 +1,15 @@
|
||||
'use client'
|
||||
'use client';
|
||||
import React, { useState } from 'react';
|
||||
import Button from '@/components/Button';
|
||||
import InputText from '@/components/InputText';
|
||||
import logger from '@/utils/logger';
|
||||
import { useNotification } from '@/context/NotificationContext';
|
||||
|
||||
export default function SettingsPage() {
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [confirmPassword, setConfirmPassword] = useState('');
|
||||
const { showNotification } = useNotification();
|
||||
|
||||
const handleEmailChange = (e) => {
|
||||
setEmail(e.target.value);
|
||||
@ -23,12 +26,16 @@ export default function SettingsPage() {
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
if (password !== confirmPassword) {
|
||||
alert('Les mots de passe ne correspondent pas');
|
||||
showNotification(
|
||||
'Les mots de passe ne correspondent pas',
|
||||
'error',
|
||||
'Erreur'
|
||||
);
|
||||
return;
|
||||
}
|
||||
// Logique pour mettre à jour l'email et le mot de passe
|
||||
console.log('Email:', email);
|
||||
console.log('Password:', password);
|
||||
logger.debug('Email:', email);
|
||||
logger.debug('Password:', password);
|
||||
};
|
||||
|
||||
return (
|
||||
@ -60,13 +67,7 @@ export default function SettingsPage() {
|
||||
required
|
||||
/>
|
||||
<div className="flex items-center justify-between">
|
||||
<Button
|
||||
type="submit"
|
||||
primary
|
||||
text={" Mettre à jour"}
|
||||
/>
|
||||
|
||||
|
||||
<Button type="submit" primary text={' Mettre à jour'} />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
5
Front-End/src/app/[locale]/users/login/error.js
Normal file
5
Front-End/src/app/[locale]/users/login/error.js
Normal file
@ -0,0 +1,5 @@
|
||||
'use client';
|
||||
|
||||
function ErrorBoundary({ error }) {
|
||||
return <>{error.message}</>;
|
||||
}
|
||||
@ -1,135 +1,146 @@
|
||||
'use client'
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import DjangoCSRFToken from '@/components/DjangoCSRFToken'
|
||||
|
||||
'use client';
|
||||
import React, { useState } from 'react';
|
||||
import DjangoCSRFToken from '@/components/DjangoCSRFToken';
|
||||
import Logo from '@/components/Logo';
|
||||
import { useSearchParams, useRouter } from 'next/navigation'
|
||||
import { useRouter } from 'next/navigation';
|
||||
import InputTextIcon from '@/components/InputTextIcon';
|
||||
import Loader from '@/components/Loader'; // Importez le composant Loader
|
||||
import Button from '@/components/Button'; // Importez le composant Button
|
||||
import { User, KeySquare } from 'lucide-react'; // Importez directement les icônes nécessaires
|
||||
import { BK_LOGIN_URL, FR_ADMIN_SUBSCRIPTIONS_EDIT_URL, FR_ADMIN_SUBSCRIPTIONS_URL, FR_PARENTS_HOME_URL, FR_USERS_NEW_PASSWORD_URL, FR_USERS_SUBSCRIBE_URL } from '@/utils/Url';
|
||||
import useLocalStorage from '@/hooks/useLocalStorage';
|
||||
import useCsrfToken from '@/hooks/useCsrfToken';
|
||||
const useFakeData = process.env.NEXT_PUBLIC_USE_FAKE_DATA === 'true';
|
||||
import { FE_USERS_NEW_PASSWORD_URL, getRedirectUrlFromRole } from '@/utils/Url';
|
||||
import { login } from '@/app/actions/authAction';
|
||||
import { getSession } from 'next-auth/react';
|
||||
import { useCsrfToken } from '@/context/CsrfContext'; // Importez le hook useCsrfToken
|
||||
import logger from '@/utils/logger';
|
||||
|
||||
import { useEstablishment } from '@/context/EstablishmentContext';
|
||||
import { useNotification } from '@/context/NotificationContext';
|
||||
|
||||
export default function Page() {
|
||||
const searchParams = useSearchParams();
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
const [userFieldError,setUserFieldError] = useState("")
|
||||
const [passwordFieldError,setPasswordFieldError] = useState("")
|
||||
const { initializeContextWithSession } = useEstablishment();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const router = useRouter();
|
||||
const csrfToken = useCsrfToken(); // Utilisez le hook useCsrfToken
|
||||
const { showNotification } = useNotification();
|
||||
|
||||
const [userId, setUserId] = useLocalStorage("userId", '') ;
|
||||
function handleFormLogin(formData) {
|
||||
setIsLoading(true);
|
||||
|
||||
const router = useRouter();
|
||||
const csrfToken = useCsrfToken();
|
||||
login({
|
||||
email: formData.get('login'),
|
||||
password: formData.get('password'),
|
||||
})
|
||||
.then((result) => {
|
||||
logger.debug('Sign In Result', result);
|
||||
|
||||
|
||||
function isOK(data) {
|
||||
return data.errorMessage === ""
|
||||
}
|
||||
|
||||
function handleFormLogin(formData) {
|
||||
if (useFakeData) {
|
||||
// Simuler une réponse réussie
|
||||
const data = {
|
||||
errorFields: {},
|
||||
errorMessage: "",
|
||||
profil: "fakeProfileId"
|
||||
};
|
||||
setUserFieldError("")
|
||||
setPasswordFieldError("")
|
||||
setErrorMessage("")
|
||||
if(isOK(data)){
|
||||
localStorage.setItem('userId', data.profil); // Stocker l'identifiant de l'utilisateur
|
||||
router.push(`${FR_ADMIN_SUBSCRIPTIONS_EDIT_URL}?id=${data.profil}`);
|
||||
} else {
|
||||
if(data.errorFields){
|
||||
setUserFieldError(data.errorFields.email)
|
||||
setPasswordFieldError(data.errorFields.password);
|
||||
}
|
||||
if(data.errorMessage){
|
||||
setErrorMessage(data.errorMessage)
|
||||
}
|
||||
}
|
||||
if (result.error) {
|
||||
showNotification(
|
||||
result.error,
|
||||
'error',
|
||||
'Erreur'
|
||||
);
|
||||
setIsLoading(false);
|
||||
} else {
|
||||
const request = new Request(
|
||||
`${BK_LOGIN_URL}`,
|
||||
{
|
||||
method:'POST',
|
||||
headers: {
|
||||
'Content-Type':'application/json',
|
||||
'X-CSRFToken': csrfToken
|
||||
},
|
||||
body: JSON.stringify( {
|
||||
email: formData.get('login'),
|
||||
password: formData.get('password'),
|
||||
}),
|
||||
credentials: 'include',
|
||||
}
|
||||
);
|
||||
fetch(request).then(response => response.json())
|
||||
.then(data => {
|
||||
console.log('Success:', data);
|
||||
setUserFieldError("")
|
||||
setPasswordFieldError("")
|
||||
setErrorMessage("")
|
||||
if(isOK(data)){
|
||||
localStorage.setItem('userId', data.profil); // Stocker l'identifiant de l'utilisateur
|
||||
if (data.droit == 0) {
|
||||
// Vue ECOLE
|
||||
} else if (data.droit == 1) {
|
||||
// Vue ADMIN
|
||||
router.push(`${FR_ADMIN_SUBSCRIPTIONS_URL}`);
|
||||
} else if (data.droit == 2) {
|
||||
// Vue PARENT
|
||||
router.push(`${FR_PARENTS_HOME_URL}`);
|
||||
} else {
|
||||
// Cas anormal
|
||||
}
|
||||
|
||||
// On initialise le contexte establishement avec la session
|
||||
getSession()
|
||||
.then((session) => {
|
||||
initializeContextWithSession(session, (role) => {
|
||||
const url = getRedirectUrlFromRole(role);
|
||||
if (url) {
|
||||
router.push(url);
|
||||
} else {
|
||||
if(data.errorFields){
|
||||
setUserFieldError(data.errorFields.email)
|
||||
setPasswordFieldError(data.errorFields.password);
|
||||
}
|
||||
if(data.errorMessage){
|
||||
setErrorMessage(data.errorMessage)
|
||||
}
|
||||
showNotification(
|
||||
'Type de rôle non géré',
|
||||
'error',
|
||||
'Erreur'
|
||||
);
|
||||
}
|
||||
});
|
||||
setIsLoading(false);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching data:', error);
|
||||
error = error.message;
|
||||
console.log(error);
|
||||
.catch((error) => {
|
||||
logger.error(
|
||||
'Erreur lors de la récupération de la session:',
|
||||
error
|
||||
);
|
||||
setIsLoading(false);
|
||||
showNotification(
|
||||
'Une erreur est survenue lors de la récupération de la session.',
|
||||
'error',
|
||||
'Erreur'
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error('Erreur lors de la connexion:', error);
|
||||
setIsLoading(false);
|
||||
showNotification(
|
||||
'Une erreur est survenue lors de la connexion.',
|
||||
'error',
|
||||
'Erreur'
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (isLoading === true) {
|
||||
return <Loader /> // Affichez le composant Loader
|
||||
} else {
|
||||
return <>
|
||||
<div className="container max mx-auto p-4">
|
||||
<div className="flex justify-center mb-4">
|
||||
<Logo className="h-150 w-150" />
|
||||
</div>
|
||||
<h1 className="text-2xl text-emerald-900 font-bold text-center mb-4">Authentification</h1>
|
||||
<form className="max-w-md mx-auto" onSubmit={(e) => { e.preventDefault(); handleFormLogin(new FormData(e.target)); }}>
|
||||
<DjangoCSRFToken csrfToken={csrfToken} />
|
||||
<InputTextIcon name="login" type="text" IconItem={User} label="Identifiant" placeholder="Identifiant" errorMsg={userFieldError} className="w-full" />
|
||||
<InputTextIcon name="password" type="password" IconItem={KeySquare} label="Mot de passe" placeholder="Mot de passe" errorMsg={passwordFieldError} className="w-full" />
|
||||
<div className="input-group mb-4">
|
||||
</div>
|
||||
<label className="text-red-500">{errorMessage}</label>
|
||||
<label><a className="float-right text-emerald-900" href={`${FR_USERS_NEW_PASSWORD_URL}`}>Mot de passe oublié ?</a></label>
|
||||
<div className="form-group-submit mt-4">
|
||||
<Button text="Se Connecter" className="w-full" primary type="submit" name="connect" />
|
||||
</div>
|
||||
</form>
|
||||
if (isLoading === true) {
|
||||
return <Loader />; // Affichez le composant Loader
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
<div className="container max mx-auto p-4">
|
||||
<div className="flex justify-center mb-4">
|
||||
<Logo className="h-150 w-150" />
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold text-center mb-4">
|
||||
Authentification
|
||||
</h1>
|
||||
<form
|
||||
className="max-w-md mx-auto"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
handleFormLogin(new FormData(e.target));
|
||||
}}
|
||||
>
|
||||
<DjangoCSRFToken csrfToken={csrfToken} />
|
||||
<InputTextIcon
|
||||
name="login"
|
||||
type="text"
|
||||
IconItem={User}
|
||||
label="Identifiant"
|
||||
placeholder="Identifiant"
|
||||
className="w-full mb-5"
|
||||
/>
|
||||
<InputTextIcon
|
||||
name="password"
|
||||
type="password"
|
||||
IconItem={KeySquare}
|
||||
label="Mot de passe"
|
||||
placeholder="Mot de passe"
|
||||
className="w-full mb-5"
|
||||
/>
|
||||
<div className="input-group mb-4"></div>
|
||||
<label>
|
||||
<a
|
||||
className="float-right mb-4"
|
||||
href={`${FE_USERS_NEW_PASSWORD_URL}`}
|
||||
>
|
||||
Mot de passe oublié ?
|
||||
</a>
|
||||
</label>
|
||||
<div className="form-group-submit mt-4">
|
||||
<Button
|
||||
text="Se Connecter"
|
||||
className="w-full"
|
||||
primary
|
||||
type="submit"
|
||||
name="connect"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
};
|
||||
</form>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,107 +1,109 @@
|
||||
'use client'
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import DjangoCSRFToken from '@/components/DjangoCSRFToken'
|
||||
import React, { useState } from 'react';
|
||||
import DjangoCSRFToken from '@/components/DjangoCSRFToken';
|
||||
import Logo from '@/components/Logo';
|
||||
import { useSearchParams, useRouter } from 'next/navigation';
|
||||
import InputTextIcon from '@/components/InputTextIcon';
|
||||
import Loader from '@/components/Loader'; // Importez le composant Loader
|
||||
import Button from '@/components/Button'; // Importez le composant Button
|
||||
import Popup from '@/components/Popup'; // Importez le composant Popup
|
||||
import { User } from 'lucide-react'; // Importez directement les icônes nécessaires
|
||||
import { BK_NEW_PASSWORD_URL,FR_USERS_LOGIN_URL } from '@/utils/Url';
|
||||
import useCsrfToken from '@/hooks/useCsrfToken';
|
||||
|
||||
const useFakeData = process.env.NEXT_PUBLIC_USE_FAKE_DATA === 'true';
|
||||
import Loader from '@/components/Loader';
|
||||
import Button from '@/components/Button';
|
||||
import { User } from 'lucide-react';
|
||||
import { FE_USERS_LOGIN_URL } from '@/utils/Url';
|
||||
import { useCsrfToken } from '@/context/CsrfContext';
|
||||
import { sendNewPassword } from '@/app/actions/authAction';
|
||||
import logger from '@/utils/logger';
|
||||
import { useNotification } from '@/context/NotificationContext';
|
||||
|
||||
export default function Page() {
|
||||
const searchParams = useSearchParams();
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
const [userFieldError, setUserFieldError] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [popupVisible, setPopupVisible] = useState(false);
|
||||
const [popupMessage, setPopupMessage] = useState("");
|
||||
const [popupConfirmAction, setPopupConfirmAction] = useState(null);
|
||||
const csrfToken = useCsrfToken();
|
||||
const { showNotification } = useNotification();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const csrfToken = useCsrfToken();
|
||||
|
||||
function validate(formData) {
|
||||
if (useFakeData) {
|
||||
setTimeout(() => {
|
||||
setUserFieldError("");
|
||||
setErrorMessage("");
|
||||
setPopupMessage("Mot de passe réinitialisé avec succès !");
|
||||
setPopupConfirmAction(() => () => setPopupVisible(false));
|
||||
setPopupVisible(true);
|
||||
}, 1000); // Simule un délai de traitement
|
||||
function validate(formData) {
|
||||
const data = { email: formData.get('email') };
|
||||
setIsLoading(true);
|
||||
sendNewPassword(data, csrfToken)
|
||||
.then((data) => {
|
||||
logger.debug('Success:', data);
|
||||
if (data.message !== '') {
|
||||
showNotification(
|
||||
data.message,
|
||||
'success',
|
||||
'Succès'
|
||||
);
|
||||
router.push(`${FE_USERS_LOGIN_URL}`);
|
||||
} else {
|
||||
const request = new Request(
|
||||
`${BK_NEW_PASSWORD_URL}`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken
|
||||
},
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({
|
||||
email: formData.get('email')
|
||||
}),
|
||||
}
|
||||
if (data.errorMessage) {
|
||||
showNotification(
|
||||
data.errorMessage,
|
||||
'error',
|
||||
'Erreur'
|
||||
);
|
||||
fetch(request).then(response => response.json())
|
||||
.then(data => {
|
||||
console.log('Success:', data);
|
||||
setUserFieldError("");
|
||||
setErrorMessage("");
|
||||
if (data.errorMessage === "") {
|
||||
setPopupMessage(data.message);
|
||||
setPopupConfirmAction(() => () => setPopupVisible(false));
|
||||
setPopupVisible(true);
|
||||
} else {
|
||||
if (data.errorFields) {
|
||||
setUserFieldError(data.errorFields.email);
|
||||
}
|
||||
if (data.errorMessage) {
|
||||
setErrorMessage(data.errorMessage);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching data:', error);
|
||||
error = error.errorMessage;
|
||||
console.log(error);
|
||||
});
|
||||
} else if (data.errorFields) {
|
||||
showNotification(
|
||||
data.errorFields.email,
|
||||
'error',
|
||||
'Erreur'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
setIsLoading(false);
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error('Error fetching data:', error);
|
||||
setIsLoading(false);
|
||||
error = error.errorMessage;
|
||||
logger.debug(error);
|
||||
});
|
||||
}
|
||||
|
||||
if (isLoading === true) {
|
||||
return <Loader /> // Affichez le composant Loader
|
||||
} else {
|
||||
return <>
|
||||
<div className="container max mx-auto p-4">
|
||||
<div className="flex justify-center mb-4">
|
||||
<Logo className="h-150 w-150" />
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold text-center mb-4">Nouveau Mot de passe</h1>
|
||||
<form className="max-w-md mx-auto" onSubmit={(e) => { e.preventDefault(); validate(new FormData(e.target)); }}>
|
||||
<DjangoCSRFToken csrfToken={csrfToken} />
|
||||
<InputTextIcon name="email" type="text" IconItem={User} label="Identifiant" placeholder="Identifiant" errorMsg={userFieldError} className="w-full" />
|
||||
<p className="text-red-500">{errorMessage}</p>
|
||||
<div className="form-group-submit mt-4">
|
||||
<Button text="Réinitialiser" className="w-full" primary type="submit" name="validate" />
|
||||
</div>
|
||||
</form>
|
||||
<br />
|
||||
<div className='flex justify-center mt-2 max-w-md mx-auto'>
|
||||
<Button text="Annuler" className="w-full" href={ `${FR_USERS_LOGIN_URL}`} />
|
||||
</div>
|
||||
</div>
|
||||
<Popup
|
||||
visible={popupVisible}
|
||||
message={popupMessage}
|
||||
onConfirm={popupConfirmAction}
|
||||
onCancel={() => setPopupVisible(false)}
|
||||
if (isLoading === true) {
|
||||
return <Loader />;
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
<div className="container max mx-auto p-4">
|
||||
<div className="flex justify-center mb-4">
|
||||
<Logo className="h-150 w-150" />
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold text-center mb-4">
|
||||
Nouveau Mot de passe
|
||||
</h1>
|
||||
<form
|
||||
className="max-w-md mx-auto"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
validate(new FormData(e.target));
|
||||
}}
|
||||
>
|
||||
<DjangoCSRFToken csrfToken={csrfToken} />
|
||||
<InputTextIcon
|
||||
name="email"
|
||||
type="text"
|
||||
IconItem={User}
|
||||
label="Identifiant"
|
||||
placeholder="Identifiant"
|
||||
className="w-full"
|
||||
/>
|
||||
</>
|
||||
}
|
||||
}
|
||||
<div className="form-group-submit mt-4">
|
||||
<Button
|
||||
text="Réinitialiser"
|
||||
className="w-full"
|
||||
primary
|
||||
type="submit"
|
||||
name="validate"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
<br />
|
||||
<div className="flex justify-center mt-2 max-w-md mx-auto">
|
||||
<Button
|
||||
text="Annuler"
|
||||
className="w-full"
|
||||
href={`${FE_USERS_LOGIN_URL}`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,144 +1,127 @@
|
||||
'use client'
|
||||
'use client';
|
||||
// src/app/pages/subscribe.js
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import DjangoCSRFToken from '@/components/DjangoCSRFToken'
|
||||
import DjangoCSRFToken from '@/components/DjangoCSRFToken';
|
||||
import Logo from '@/components/Logo';
|
||||
import { useSearchParams, useRouter } from 'next/navigation'
|
||||
import { useSearchParams, useRouter } from 'next/navigation';
|
||||
import InputTextIcon from '@/components/InputTextIcon';
|
||||
import Loader from '@/components/Loader'; // Importez le composant Loader
|
||||
import Button from '@/components/Button'; // Importez le composant Button
|
||||
import Popup from '@/components/Popup';
|
||||
import { BK_RESET_PASSWORD_URL, FR_USERS_LOGIN_URL } from '@/utils/Url';
|
||||
import { KeySquare } from 'lucide-react'; // Importez directement les icônes nécessaires
|
||||
import useCsrfToken from '@/hooks/useCsrfToken';
|
||||
|
||||
const useFakeData = process.env.NEXT_PUBLIC_USE_FAKE_DATA === 'true';
|
||||
import Loader from '@/components/Loader';
|
||||
import Button from '@/components/Button';
|
||||
import { FE_USERS_LOGIN_URL } from '@/utils/Url';
|
||||
import { KeySquare } from 'lucide-react';
|
||||
import { useCsrfToken } from '@/context/CsrfContext';
|
||||
import { resetPassword } from '@/app/actions/authAction';
|
||||
import logger from '@/utils/logger';
|
||||
import { useNotification } from '@/context/NotificationContext';
|
||||
|
||||
export default function Page() {
|
||||
const searchParams = useSearchParams();
|
||||
const uuid = searchParams.get('uuid');
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
const [password1FieldError,setPassword1FieldError] = useState("")
|
||||
const [password2FieldError,setPassword2FieldError] = useState("")
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [popupVisible, setPopupVisible] = useState(false);
|
||||
const [popupMessage, setPopupMessage] = useState("");
|
||||
const searchParams = useSearchParams();
|
||||
const { showNotification } = useNotification();
|
||||
const uuid = searchParams.get('uuid');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
const csrfToken = useCsrfToken();
|
||||
const router = useRouter();
|
||||
const csrfToken = useCsrfToken();
|
||||
|
||||
useEffect(() => {
|
||||
if (useFakeData) {
|
||||
setTimeout(() => {
|
||||
setIsLoading(false);
|
||||
}, 1000);
|
||||
} else {
|
||||
const url= `${BK_RESET_PASSWORD_URL}/${uuid}`;
|
||||
fetch(url, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}).then(response => response.json())
|
||||
.then(data => {
|
||||
console.log('Success:', data);
|
||||
setIsLoading(true);
|
||||
if(data.errorFields){
|
||||
setPassword1FieldError(data.errorFields.password1)
|
||||
setPassword2FieldError(data.errorFields.password2)
|
||||
}
|
||||
if(data.errorMessage){
|
||||
setErrorMessage(data.errorMessage)
|
||||
}
|
||||
setIsLoading(false);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching data:', error);
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
function validate(formData) {
|
||||
const data = {
|
||||
password1: formData.get('password1'),
|
||||
password2: formData.get('password2'),
|
||||
};
|
||||
setIsLoading(true);
|
||||
resetPassword(uuid, data, csrfToken)
|
||||
.then((data) => {
|
||||
if (data.message !== '') {
|
||||
|
||||
function validate(formData) {
|
||||
if (useFakeData) {
|
||||
setTimeout(() => {
|
||||
setPopupMessage("Mot de passe réinitialisé avec succès");
|
||||
setPopupVisible(true);
|
||||
}, 1000);
|
||||
} else {
|
||||
const request = new Request(
|
||||
`${BK_RESET_PASSWORD_URL}/${uuid}`,
|
||||
{
|
||||
method:'POST',
|
||||
headers: {
|
||||
'Content-Type':'application/json',
|
||||
'X-CSRFToken': csrfToken
|
||||
},
|
||||
credentials: 'include',
|
||||
body: JSON.stringify( {
|
||||
password1: formData.get('password1'),
|
||||
password2: formData.get('password2'),
|
||||
}),
|
||||
}
|
||||
logger.debug('Success:', data);
|
||||
showNotification(
|
||||
data.message,
|
||||
'success',
|
||||
'Succès'
|
||||
);
|
||||
fetch(request).then(response => response.json())
|
||||
.then(data => {
|
||||
console.log('Success:', data);
|
||||
setPassword1FieldError("")
|
||||
setPassword2FieldError("")
|
||||
setErrorMessage("")
|
||||
if(data.errorMessage === ""){
|
||||
setPopupMessage(data.message);
|
||||
setPopupVisible(true);
|
||||
} else {
|
||||
if(data.errorMessage){
|
||||
setErrorMessage(data.errorMessage);
|
||||
}
|
||||
if(data.errorFields){
|
||||
setPassword1FieldError(data.errorFields.password1)
|
||||
setPassword2FieldError(data.errorFields.password2)
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching data:', error);
|
||||
error = error.errorMessage;
|
||||
console.log(error);
|
||||
});
|
||||
router.push(`${FE_USERS_LOGIN_URL}`);
|
||||
} else {
|
||||
if (data.errorMessage) {
|
||||
showNotification(
|
||||
data.errorMessage,
|
||||
'error',
|
||||
'Erreur'
|
||||
);
|
||||
} else if (data.errorFields) {
|
||||
showNotification(
|
||||
data.errorFields.password1 || data.errorFields.password2,
|
||||
'error',
|
||||
'Erreur'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
setIsLoading(false);
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error('Error fetching data:', error);
|
||||
error = error.errorMessage;
|
||||
logger.debug(error);
|
||||
setIsLoading(false);
|
||||
});
|
||||
}
|
||||
|
||||
if (isLoading === true) {
|
||||
return <Loader /> // Affichez le composant Loader
|
||||
} else {
|
||||
return <>
|
||||
<Popup
|
||||
visible={popupVisible}
|
||||
message={popupMessage}
|
||||
onConfirm={() => {
|
||||
setPopupVisible(false);
|
||||
router.push(`${FR_USERS_LOGIN_URL}`);
|
||||
}}
|
||||
onCancel={() => setPopupVisible(false)}
|
||||
if (isLoading === true) {
|
||||
return <Loader />;
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
<div className="container max mx-auto p-4">
|
||||
<div className="flex justify-center mb-4">
|
||||
<Logo className="h-150 w-150" />
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold text-center mb-4">
|
||||
Réinitialisation du mot de passe
|
||||
</h1>
|
||||
<form
|
||||
className="max-w-md mx-auto"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
validate(new FormData(e.target));
|
||||
}}
|
||||
>
|
||||
<DjangoCSRFToken csrfToken={csrfToken} />
|
||||
<InputTextIcon
|
||||
name="password1"
|
||||
type="password"
|
||||
IconItem={KeySquare}
|
||||
label="Mot de passe"
|
||||
placeholder="Mot de passe"
|
||||
className="w-full mb-5"
|
||||
/>
|
||||
<div className="container max mx-auto p-4">
|
||||
<div className="flex justify-center mb-4">
|
||||
<Logo className="h-150 w-150" />
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold text-center mb-4">Réinitialisation du mot de passe</h1>
|
||||
<form className="max-w-md mx-auto" onSubmit={(e) => { e.preventDefault(); validate(new FormData(e.target)); }}>
|
||||
<DjangoCSRFToken csrfToken={csrfToken} />
|
||||
<InputTextIcon name="password1" type="password" IconItem={KeySquare} label="Mot de passe" placeholder="Mot de passe" errorMsg={password1FieldError} className="w-full" />
|
||||
<InputTextIcon name="password2" type="password" IconItem={KeySquare} label="Confirmation mot de passe" placeholder="Confirmation mot de passe" errorMsg={password2FieldError} className="w-full" />
|
||||
<label className="text-red-500">{errorMessage}</label>
|
||||
<div className="form-group-submit mt-4">
|
||||
<Button text="Enregistrer" className="w-full" primary type="submit" name="validate" />
|
||||
</div>
|
||||
</form>
|
||||
<br/>
|
||||
<div className="flex justify-center mt-2 max-w-md mx-auto">
|
||||
<Button text="Annuler" className="w-full" href={`${FR_USERS_LOGIN_URL}`} />
|
||||
</div>
|
||||
<InputTextIcon
|
||||
name="password2"
|
||||
type="password"
|
||||
IconItem={KeySquare}
|
||||
label="Confirmation mot de passe"
|
||||
placeholder="Confirmation mot de passe"
|
||||
className="w-full"
|
||||
/>
|
||||
<div className="form-group-submit mt-4">
|
||||
<Button
|
||||
text="Enregistrer"
|
||||
className="w-full"
|
||||
primary
|
||||
type="submit"
|
||||
name="validate"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
}
|
||||
</form>
|
||||
<br />
|
||||
<div className="flex justify-center mt-2 max-w-md mx-auto">
|
||||
<Button
|
||||
text="Annuler"
|
||||
className="w-full"
|
||||
href={`${FE_USERS_LOGIN_URL}`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,180 +1,138 @@
|
||||
'use client'
|
||||
// src/app/pages/subscribe.js
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import DjangoCSRFToken from '@/components/DjangoCSRFToken'
|
||||
import DjangoCSRFToken from '@/components/DjangoCSRFToken';
|
||||
import Logo from '@/components/Logo';
|
||||
import { useSearchParams, useRouter } from 'next/navigation'
|
||||
import { useSearchParams, useRouter } from 'next/navigation';
|
||||
import InputTextIcon from '@/components/InputTextIcon';
|
||||
import Loader from '@/components/Loader'; // Importez le composant Loader
|
||||
import Button from '@/components/Button'; // Importez le composant Button
|
||||
import Popup from '@/components/Popup'; // Importez le composant Popup
|
||||
import { User, KeySquare } from 'lucide-react'; // Importez directement les icônes nécessaires
|
||||
import { BK_REGISTER_URL, FR_USERS_LOGIN_URL } from '@/utils/Url';
|
||||
import useCsrfToken from '@/hooks/useCsrfToken';
|
||||
const useFakeData = process.env.NEXT_PUBLIC_USE_FAKE_DATA === 'true';
|
||||
import Loader from '@/components/Loader';
|
||||
import Button from '@/components/Button';
|
||||
import { User, KeySquare } from 'lucide-react';
|
||||
import { FE_USERS_LOGIN_URL } from '@/utils/Url';
|
||||
import { useCsrfToken } from '@/context/CsrfContext';
|
||||
import { subscribe } from '@/app/actions/authAction';
|
||||
import logger from '@/utils/logger';
|
||||
import { useNotification } from '@/context/NotificationContext';
|
||||
|
||||
export default function Page() {
|
||||
const searchParams = useSearchParams();
|
||||
const searchParams = useSearchParams();
|
||||
const { showNotification } = useNotification();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
const [userFieldError,setUserFieldError] = useState("")
|
||||
const [password1FieldError,setPassword1FieldError] = useState("")
|
||||
const [password2FieldError,setPassword2FieldError] = useState("")
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [popupVisible, setPopupVisible] = useState(false);
|
||||
const [popupMessage, setPopupMessage] = useState("");
|
||||
const router = useRouter();
|
||||
const csrfToken = useCsrfToken();
|
||||
|
||||
const router = useRouter();
|
||||
const csrfToken = useCsrfToken();
|
||||
const establishment_id = searchParams.get('establishment_id');
|
||||
|
||||
useEffect(() => {
|
||||
if (useFakeData) {
|
||||
// Simuler une réponse réussie
|
||||
const data = {
|
||||
errorFields: {},
|
||||
errorMessage: ""
|
||||
};
|
||||
setUserFieldError("")
|
||||
setPassword1FieldError("")
|
||||
setPassword2FieldError("")
|
||||
setErrorMessage("")
|
||||
setIsLoading(false);
|
||||
function subscribeFormSubmit(formData) {
|
||||
const data = {
|
||||
email: formData.get('login'),
|
||||
password1: formData.get('password1'),
|
||||
password2: formData.get('password2'),
|
||||
establishment_id: establishment_id,
|
||||
};
|
||||
setIsLoading(true);
|
||||
subscribe(data, csrfToken)
|
||||
.then((data) => {
|
||||
logger.debug('Success:', data);
|
||||
if (data.message !== '') {
|
||||
showNotification(
|
||||
data.message,
|
||||
'success',
|
||||
'Succès'
|
||||
);
|
||||
router.push(`${FE_USERS_LOGIN_URL}`);
|
||||
} else {
|
||||
const url= `${BK_REGISTER_URL}`;
|
||||
fetch(url, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}).then(response => response.json())
|
||||
.then(data => {
|
||||
console.log('Success:', data);
|
||||
setUserFieldError("")
|
||||
setPassword1FieldError("")
|
||||
setPassword2FieldError("")
|
||||
setErrorMessage("")
|
||||
setIsLoading(true);
|
||||
if(data.errorFields){
|
||||
setUserFieldError(data.errorFields.email)
|
||||
setPassword1FieldError(data.errorFields.password1)
|
||||
setPassword2FieldError(data.errorFields.password2)
|
||||
}
|
||||
if(data.errorMessage){
|
||||
setErrorMessage(data.errorMessage)
|
||||
}
|
||||
setIsLoading(false);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching data:', error);
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
function isOK(data) {
|
||||
return data.errorMessage === ""
|
||||
}
|
||||
|
||||
function suscribe(formData) {
|
||||
if (useFakeData) {
|
||||
// Simuler une réponse réussie
|
||||
const data = {
|
||||
errorFields: {},
|
||||
errorMessage: ""
|
||||
};
|
||||
setUserFieldError("")
|
||||
setPassword1FieldError("")
|
||||
setPassword2FieldError("")
|
||||
setErrorMessage("")
|
||||
if(isOK(data)){
|
||||
setPopupMessage("Votre compte a été créé avec succès");
|
||||
setPopupVisible(true);
|
||||
} else {
|
||||
if(data.errorMessage){
|
||||
setErrorMessage(data.errorMessage);
|
||||
}
|
||||
if(data.errorFields){
|
||||
setUserFieldError(data.errorFields.email)
|
||||
setPassword1FieldError(data.errorFields.password1)
|
||||
setPassword2FieldError(data.errorFields.password2)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const request = new Request(
|
||||
`${BK_REGISTER_URL}`,
|
||||
{
|
||||
method:'POST',
|
||||
headers: {
|
||||
'Content-Type':'application/json',
|
||||
'X-CSRFToken': csrfToken
|
||||
},
|
||||
credentials: 'include',
|
||||
body: JSON.stringify( {
|
||||
email: formData.get('login'),
|
||||
password1: formData.get('password1'),
|
||||
password2: formData.get('password2'),
|
||||
}),
|
||||
}
|
||||
if (data.errorMessage) {
|
||||
showNotification(
|
||||
data.errorMessage,
|
||||
'error',
|
||||
'Erreur'
|
||||
);
|
||||
fetch(request).then(response => response.json())
|
||||
.then(data => {
|
||||
console.log('Success:', data);
|
||||
setUserFieldError("")
|
||||
setPassword1FieldError("")
|
||||
setPassword2FieldError("")
|
||||
setErrorMessage("")
|
||||
if(isOK(data)){
|
||||
setPopupMessage(data.message);
|
||||
setPopupVisible(true);
|
||||
} else {
|
||||
if(data.errorMessage){
|
||||
setErrorMessage(data.errorMessage);
|
||||
}
|
||||
if(data.errorFields){
|
||||
setUserFieldError(data.errorFields.email)
|
||||
setPassword1FieldError(data.errorFields.password1)
|
||||
setPassword2FieldError(data.errorFields.password2)
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching data:', error);
|
||||
error = error.errorMessage;
|
||||
console.log(error);
|
||||
});
|
||||
} else if (data.errorFields) {
|
||||
showNotification(
|
||||
data.errorFields.email || data.errorFields.password1 || data.errorFields.password2,
|
||||
'error',
|
||||
'Erreur'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
setIsLoading(false);
|
||||
})
|
||||
.catch((error) => {
|
||||
setIsLoading(false);
|
||||
logger.error('Error fetching data:', error);
|
||||
error = error.errorMessage;
|
||||
logger.debug(error);
|
||||
});
|
||||
}
|
||||
|
||||
if (isLoading === true) {
|
||||
return <Loader /> // Affichez le composant Loader
|
||||
} else {
|
||||
return <>
|
||||
<div className="container max mx-auto p-4">
|
||||
<div className="flex justify-center mb-4">
|
||||
<Logo className="h-150 w-150" />
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold text-center mb-4">Nouveau profil</h1>
|
||||
<form className="max-w-md mx-auto" onSubmit={(e) => { e.preventDefault(); suscribe(new FormData(e.target)); }}>
|
||||
<DjangoCSRFToken csrfToken={csrfToken} />
|
||||
<InputTextIcon name="login" type="text" IconItem={User} label="Identifiant" placeholder="Identifiant" errorMsg={userFieldError} className="w-full" />
|
||||
<InputTextIcon name="password1" type="password" IconItem={KeySquare} label="Mot de passe" placeholder="Mot de passe" errorMsg={password1FieldError} className="w-full" />
|
||||
<InputTextIcon name="password2" type="password" IconItem={KeySquare} label="Confirmation mot de passe" placeholder="Confirmation mot de passe" errorMsg={password2FieldError} className="w-full" />
|
||||
<p className="text-red-500">{errorMessage}</p>
|
||||
<div className="form-group-submit mt-4">
|
||||
<Button text="Enregistrer" className="w-full" primary type="submit" name="validate" />
|
||||
</div>
|
||||
</form>
|
||||
<br/>
|
||||
<div className='flex justify-center mt-2 max-w-md mx-auto'><Button text="Annuler" className="w-full" onClick={()=>{router.push(`${FR_USERS_LOGIN_URL}`)}} /></div>
|
||||
</div>
|
||||
<Popup
|
||||
visible={popupVisible}
|
||||
message={popupMessage}
|
||||
onConfirm={() => {
|
||||
setPopupVisible(false);
|
||||
router.push(`${FR_USERS_LOGIN_URL}`);
|
||||
}}
|
||||
onCancel={() => setPopupVisible(false)}
|
||||
if (isLoading === true) {
|
||||
return <Loader />;
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
<div className="container max mx-auto p-4">
|
||||
<div className="flex justify-center mb-4">
|
||||
<Logo className="h-150 w-150" />
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold text-center mb-4">
|
||||
Nouveau profil
|
||||
</h1>
|
||||
<form
|
||||
className="max-w-md mx-auto"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
subscribeFormSubmit(new FormData(e.target));
|
||||
}}
|
||||
>
|
||||
<DjangoCSRFToken csrfToken={csrfToken} />
|
||||
<InputTextIcon
|
||||
name="login"
|
||||
type="text"
|
||||
IconItem={User}
|
||||
label="Identifiant"
|
||||
placeholder="Identifiant"
|
||||
className="w-full mb-5"
|
||||
/>
|
||||
</>
|
||||
}
|
||||
}
|
||||
<InputTextIcon
|
||||
name="password1"
|
||||
type="password"
|
||||
IconItem={KeySquare}
|
||||
label="Mot de passe"
|
||||
placeholder="Mot de passe"
|
||||
className="w-full mb-5"
|
||||
/>
|
||||
<InputTextIcon
|
||||
name="password2"
|
||||
type="password"
|
||||
IconItem={KeySquare}
|
||||
label="Confirmation mot de passe"
|
||||
placeholder="Confirmation mot de passe"
|
||||
className="w-full"
|
||||
/>
|
||||
<div className="form-group-submit mt-4">
|
||||
<Button
|
||||
text="Enregistrer"
|
||||
className="w-full"
|
||||
primary
|
||||
type="submit"
|
||||
name="validate"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
<br />
|
||||
<div className="flex justify-center mt-2 max-w-md mx-auto">
|
||||
<Button
|
||||
text="Annuler"
|
||||
className="w-full"
|
||||
onClick={() => {
|
||||
router.push(`${FE_USERS_LOGIN_URL}`);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
31
Front-End/src/app/actions/actionsHandlers.js
Normal file
31
Front-End/src/app/actions/actionsHandlers.js
Normal file
@ -0,0 +1,31 @@
|
||||
import logger from '@/utils/logger';
|
||||
/**
|
||||
*
|
||||
* @param {*} response
|
||||
* @returns
|
||||
*/
|
||||
export const requestResponseHandler = async (response) => {
|
||||
try {
|
||||
const body = await response?.json();
|
||||
if (response.ok) {
|
||||
return body;
|
||||
}
|
||||
|
||||
// Throw an error with the JSON body containing the form errors
|
||||
const error = new Error(body?.errorMessage || 'Une erreur est survenue');
|
||||
error.details = body;
|
||||
error.response = response;
|
||||
throw error;
|
||||
} catch (error) {
|
||||
logger.error('Une erreur est survenue lors du traitement de la réponse', {
|
||||
error,
|
||||
response,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
export const errorHandler = (error) => {
|
||||
logger.error('Error:', { error });
|
||||
// Handle the error here, e.g., show a notification
|
||||
throw error;
|
||||
};
|
||||
201
Front-End/src/app/actions/authAction.js
Normal file
201
Front-End/src/app/actions/authAction.js
Normal file
@ -0,0 +1,201 @@
|
||||
import { signOut, signIn } from 'next-auth/react';
|
||||
import { errorHandler, requestResponseHandler } from './actionsHandlers';
|
||||
import {
|
||||
BE_AUTH_LOGIN_URL,
|
||||
BE_AUTH_REFRESH_JWT_URL,
|
||||
BE_AUTH_REGISTER_URL,
|
||||
BE_AUTH_PROFILES_URL,
|
||||
BE_AUTH_PROFILES_ROLES_URL,
|
||||
BE_AUTH_RESET_PASSWORD_URL,
|
||||
BE_AUTH_NEW_PASSWORD_URL,
|
||||
FE_USERS_LOGIN_URL,
|
||||
} from '@/utils/Url';
|
||||
import { PARENT_FILTER } from '@/utils/constants';
|
||||
|
||||
/**
|
||||
* Login action
|
||||
*/
|
||||
export const login = (data) => {
|
||||
return signIn('credentials', {
|
||||
redirect: false,
|
||||
email: data.email,
|
||||
password: data.password,
|
||||
role_type: data.role_type,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Login user with API
|
||||
*/
|
||||
export const getJWT = (data) => {
|
||||
const request = new Request(`${BE_AUTH_LOGIN_URL}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
credentials: 'include',
|
||||
});
|
||||
return fetch(request).then(requestResponseHandler).catch(errorHandler);
|
||||
};
|
||||
export const refreshJWT = (data) => {
|
||||
const request = new Request(`${BE_AUTH_REFRESH_JWT_URL}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
credentials: 'include',
|
||||
});
|
||||
return fetch(request).then(requestResponseHandler).catch(errorHandler);
|
||||
};
|
||||
|
||||
/**
|
||||
* Disconnects the user after confirming the action.
|
||||
* If `NEXT_PUBLIC_USE_FAKE_DATA` environment variable is set to 'true', it will log a fake disconnect and redirect to the login URL.
|
||||
* Otherwise, it will call `signOut` from NextAuth.js to handle the logout.
|
||||
*
|
||||
* @function
|
||||
* @name disconnect
|
||||
* @returns {void}
|
||||
*/
|
||||
export const disconnect = () => {
|
||||
signOut({ callbackUrl: FE_USERS_LOGIN_URL });
|
||||
};
|
||||
|
||||
export const fetchProfileRoles = (
|
||||
establishment,
|
||||
filter = PARENT_FILTER,
|
||||
page = '',
|
||||
pageSize = ''
|
||||
) => {
|
||||
let url = `${BE_AUTH_PROFILES_ROLES_URL}?filter=${filter}&establishment_id=${establishment}`;
|
||||
if (page !== '' && pageSize !== '') {
|
||||
url = `${BE_AUTH_PROFILES_ROLES_URL}?filter=${filter}&establishment_id=${establishment}&page=${page}`;
|
||||
}
|
||||
return fetch(url, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
.then(requestResponseHandler)
|
||||
.catch(errorHandler);
|
||||
};
|
||||
|
||||
export const updateProfileRoles = (id, data, csrfToken) => {
|
||||
const request = new Request(`${BE_AUTH_PROFILES_ROLES_URL}/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken,
|
||||
},
|
||||
credentials: 'include',
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
return fetch(request).then(requestResponseHandler).catch(errorHandler);
|
||||
};
|
||||
|
||||
export const deleteProfileRoles = async (id, csrfToken) => {
|
||||
const response = await fetch(`${BE_AUTH_PROFILES_ROLES_URL}/${id}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'X-CSRFToken': csrfToken,
|
||||
},
|
||||
credentials: 'include',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
// Extraire le message d'erreur du backend
|
||||
const errorData = await response.json();
|
||||
const errorMessage =
|
||||
errorData?.error ||
|
||||
'Une erreur est survenue lors de la suppression du profil.';
|
||||
|
||||
// Jeter une erreur avec le message spécifique
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
};
|
||||
|
||||
export const fetchProfiles = () => {
|
||||
return fetch(`${BE_AUTH_PROFILES_URL}`)
|
||||
.then(requestResponseHandler)
|
||||
.catch(errorHandler);
|
||||
};
|
||||
|
||||
export const createProfile = (data, csrfToken) => {
|
||||
const request = new Request(`${BE_AUTH_PROFILES_URL}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken,
|
||||
},
|
||||
credentials: 'include',
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
return fetch(request).then(requestResponseHandler).catch(errorHandler);
|
||||
};
|
||||
|
||||
export const deleteProfile = (id, csrfToken) => {
|
||||
const request = new Request(`${BE_AUTH_PROFILES_URL}/${id}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'X-CSRFToken': csrfToken,
|
||||
},
|
||||
credentials: 'include',
|
||||
});
|
||||
return fetch(request).then(requestResponseHandler).catch(errorHandler);
|
||||
};
|
||||
|
||||
export const updateProfile = (id, data, csrfToken) => {
|
||||
const request = new Request(`${BE_AUTH_PROFILES_URL}/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken,
|
||||
},
|
||||
credentials: 'include',
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
return fetch(request).then(requestResponseHandler).catch(errorHandler);
|
||||
};
|
||||
|
||||
export const sendNewPassword = (data, csrfToken) => {
|
||||
const request = new Request(`${BE_AUTH_NEW_PASSWORD_URL}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken,
|
||||
},
|
||||
credentials: 'include',
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
return fetch(request).then(requestResponseHandler).catch(errorHandler);
|
||||
};
|
||||
|
||||
export const subscribe = (data, csrfToken) => {
|
||||
const request = new Request(`${BE_AUTH_REGISTER_URL}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken,
|
||||
},
|
||||
credentials: 'include',
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
return fetch(request).then(requestResponseHandler).catch(errorHandler);
|
||||
};
|
||||
|
||||
export const resetPassword = (uuid, data, csrfToken) => {
|
||||
const request = new Request(`${BE_AUTH_RESET_PASSWORD_URL}/${uuid}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken,
|
||||
},
|
||||
credentials: 'include',
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
return fetch(request).then(requestResponseHandler).catch(errorHandler);
|
||||
};
|
||||
34
Front-End/src/app/actions/emailAction.js
Normal file
34
Front-End/src/app/actions/emailAction.js
Normal file
@ -0,0 +1,34 @@
|
||||
import {
|
||||
BE_GESTIONEMAIL_SEARCH_RECIPIENTS_URL,
|
||||
BE_GESTIONEMAIL_SEND_EMAIL_URL,
|
||||
} from '@/utils/Url';
|
||||
import { errorHandler, requestResponseHandler } from './actionsHandlers';
|
||||
import { getCsrfToken } from '@/utils/getCsrfToken';
|
||||
|
||||
// Recherche de destinataires pour email
|
||||
export const searchRecipients = (establishmentId, query) => {
|
||||
const url = `${BE_GESTIONEMAIL_SEARCH_RECIPIENTS_URL}/?establishment_id=${establishmentId}&q=${encodeURIComponent(query)}`;
|
||||
return fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
.then(requestResponseHandler)
|
||||
.catch(errorHandler);
|
||||
};
|
||||
// Envoyer un email
|
||||
export const sendEmail = async (messageData) => {
|
||||
const csrfToken = getCsrfToken();
|
||||
return fetch(BE_GESTIONEMAIL_SEND_EMAIL_URL, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken,
|
||||
},
|
||||
credentials: 'include',
|
||||
body: JSON.stringify(messageData),
|
||||
})
|
||||
.then(requestResponseHandler)
|
||||
.catch(errorHandler);
|
||||
};
|
||||
251
Front-End/src/app/actions/messagerieAction.js
Normal file
251
Front-End/src/app/actions/messagerieAction.js
Normal file
@ -0,0 +1,251 @@
|
||||
import {
|
||||
BE_GESTIONMESSAGERIE_CONVERSATIONS_URL,
|
||||
BE_GESTIONMESSAGERIE_SEND_MESSAGE_URL,
|
||||
BE_GESTIONMESSAGERIE_CREATE_CONVERSATION_URL,
|
||||
BE_GESTIONMESSAGERIE_SEARCH_RECIPIENTS_URL,
|
||||
BE_GESTIONMESSAGERIE_MARK_AS_READ_URL,
|
||||
BE_GESTIONMESSAGERIE_UPLOAD_FILE_URL,
|
||||
BE_GESTIONMESSAGERIE_DELETE_CONVERSATION_URL,
|
||||
} from '@/utils/Url';
|
||||
import { errorHandler, requestResponseHandler } from './actionsHandlers';
|
||||
import logger from '@/utils/logger';
|
||||
|
||||
// Helper pour construire les en-têtes avec CSRF
|
||||
const buildHeaders = (csrfToken) => {
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
// Ajouter le token CSRF
|
||||
if (csrfToken) {
|
||||
headers['X-CSRFToken'] = csrfToken;
|
||||
}
|
||||
|
||||
return headers;
|
||||
};
|
||||
|
||||
/**
|
||||
* Récupère les conversations d'un utilisateur
|
||||
*/
|
||||
export const fetchConversations = async (userId, csrfToken) => {
|
||||
try {
|
||||
// Utiliser la nouvelle route avec user_id en paramètre d'URL
|
||||
const url = `${BE_GESTIONMESSAGERIE_CONVERSATIONS_URL}/user/${userId}/`;
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: buildHeaders(csrfToken),
|
||||
credentials: 'include',
|
||||
});
|
||||
return await requestResponseHandler(response);
|
||||
} catch (error) {
|
||||
logger.error('Erreur lors de la récupération des conversations:', error);
|
||||
return errorHandler(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Récupère les messages d'une conversation
|
||||
*/
|
||||
export const fetchMessages = async (
|
||||
conversationId,
|
||||
page = 1,
|
||||
limit = 50,
|
||||
csrfToken,
|
||||
userId = null
|
||||
) => {
|
||||
try {
|
||||
// Utiliser la nouvelle URL avec conversation_id en paramètre d'URL
|
||||
let url = `${BE_GESTIONMESSAGERIE_CONVERSATIONS_URL}/${conversationId}/messages/?page=${page}&limit=${limit}`;
|
||||
|
||||
// Ajouter user_id si fourni pour calculer correctement is_read
|
||||
if (userId) {
|
||||
url += `&user_id=${userId}`;
|
||||
}
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: buildHeaders(csrfToken),
|
||||
credentials: 'include',
|
||||
});
|
||||
return await requestResponseHandler(response);
|
||||
} catch (error) {
|
||||
logger.error('Erreur lors de la récupération des messages:', error);
|
||||
return errorHandler(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Envoie un message dans une conversation
|
||||
*/
|
||||
export const sendMessage = async (messageData, csrfToken) => {
|
||||
try {
|
||||
const response = await fetch(BE_GESTIONMESSAGERIE_SEND_MESSAGE_URL, {
|
||||
method: 'POST',
|
||||
headers: buildHeaders(csrfToken),
|
||||
credentials: 'include',
|
||||
body: JSON.stringify(messageData),
|
||||
});
|
||||
return await requestResponseHandler(response);
|
||||
} catch (error) {
|
||||
logger.error("Erreur lors de l'envoi du message:", error);
|
||||
return errorHandler(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Crée une nouvelle conversation
|
||||
*/
|
||||
export const createConversation = async (participantIds, csrfToken) => {
|
||||
try {
|
||||
const requestBody = {
|
||||
participant_ids: participantIds, // Le backend attend "participant_ids"
|
||||
conversation_type: 'private', // Spécifier le type de conversation
|
||||
name: '', // Le nom sera généré côté backend
|
||||
};
|
||||
|
||||
const response = await fetch(BE_GESTIONMESSAGERIE_CREATE_CONVERSATION_URL, {
|
||||
method: 'POST',
|
||||
headers: buildHeaders(csrfToken),
|
||||
credentials: 'include',
|
||||
body: JSON.stringify(requestBody),
|
||||
});
|
||||
|
||||
return await requestResponseHandler(response);
|
||||
} catch (error) {
|
||||
logger.error('Erreur lors de la création de la conversation:', error);
|
||||
return errorHandler(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Recherche des destinataires pour la messagerie
|
||||
*/
|
||||
export const searchMessagerieRecipients = async (
|
||||
establishmentId,
|
||||
query,
|
||||
csrfToken
|
||||
) => {
|
||||
try {
|
||||
const baseUrl = BE_GESTIONMESSAGERIE_SEARCH_RECIPIENTS_URL.endsWith('/')
|
||||
? BE_GESTIONMESSAGERIE_SEARCH_RECIPIENTS_URL
|
||||
: BE_GESTIONMESSAGERIE_SEARCH_RECIPIENTS_URL + '/';
|
||||
|
||||
const url = `${baseUrl}?establishment_id=${establishmentId}&q=${encodeURIComponent(query)}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: buildHeaders(csrfToken),
|
||||
credentials: 'include',
|
||||
});
|
||||
|
||||
return await requestResponseHandler(response);
|
||||
} catch (error) {
|
||||
logger.error('Erreur lors de la recherche des destinataires:', error);
|
||||
return errorHandler(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Marque des messages comme lus
|
||||
*/
|
||||
export const markAsRead = async (conversationId, userId, csrfToken) => {
|
||||
try {
|
||||
const response = await fetch(BE_GESTIONMESSAGERIE_MARK_AS_READ_URL, {
|
||||
method: 'POST',
|
||||
headers: buildHeaders(csrfToken),
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({
|
||||
conversation_id: conversationId,
|
||||
user_id: userId,
|
||||
}),
|
||||
});
|
||||
return await requestResponseHandler(response);
|
||||
} catch (error) {
|
||||
logger.error('Erreur lors du marquage des messages comme lus:', error);
|
||||
return errorHandler(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Upload un fichier pour la messagerie
|
||||
*/
|
||||
export const uploadFile = async (
|
||||
file,
|
||||
conversationId,
|
||||
senderId,
|
||||
csrfToken,
|
||||
onProgress = null
|
||||
) => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append('conversation_id', conversationId);
|
||||
formData.append('sender_id', senderId);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
|
||||
if (onProgress) {
|
||||
xhr.upload.addEventListener('progress', (event) => {
|
||||
if (event.lengthComputable) {
|
||||
const percentComplete = (event.loaded / event.total) * 100;
|
||||
onProgress(percentComplete);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
xhr.addEventListener('load', () => {
|
||||
if (xhr.status >= 200 && xhr.status < 300) {
|
||||
try {
|
||||
const response = JSON.parse(xhr.responseText);
|
||||
resolve(response);
|
||||
} catch (error) {
|
||||
reject(new Error('Réponse invalide du serveur'));
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
const errorResponse = JSON.parse(xhr.responseText);
|
||||
reject(new Error(errorResponse.message || "Erreur lors de l'upload"));
|
||||
} catch {
|
||||
reject(new Error(`Erreur HTTP: ${xhr.status}`));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
xhr.addEventListener('error', () => {
|
||||
reject(new Error("Erreur réseau lors de l'upload"));
|
||||
});
|
||||
|
||||
xhr.addEventListener('timeout', () => {
|
||||
reject(new Error("Timeout lors de l'upload"));
|
||||
});
|
||||
|
||||
xhr.open('POST', BE_GESTIONMESSAGERIE_UPLOAD_FILE_URL);
|
||||
xhr.withCredentials = true;
|
||||
xhr.timeout = 30000;
|
||||
|
||||
// Ajouter le header CSRF pour XMLHttpRequest
|
||||
if (csrfToken) {
|
||||
xhr.setRequestHeader('X-CSRFToken', csrfToken);
|
||||
}
|
||||
|
||||
xhr.send(formData);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Supprime une conversation
|
||||
*/
|
||||
export const deleteConversation = async (conversationId, csrfToken) => {
|
||||
try {
|
||||
const url = `${BE_GESTIONMESSAGERIE_DELETE_CONVERSATION_URL}/${conversationId}/`;
|
||||
const response = await fetch(url, {
|
||||
method: 'DELETE',
|
||||
headers: buildHeaders(csrfToken),
|
||||
credentials: 'include',
|
||||
});
|
||||
return await requestResponseHandler(response);
|
||||
} catch (error) {
|
||||
logger.error('Erreur lors de la suppression de la conversation:', error);
|
||||
return errorHandler(error);
|
||||
}
|
||||
};
|
||||
113
Front-End/src/app/actions/planningAction.js
Normal file
113
Front-End/src/app/actions/planningAction.js
Normal file
@ -0,0 +1,113 @@
|
||||
import { BE_PLANNING_PLANNINGS_URL, BE_PLANNING_EVENTS_URL } from '@/utils/Url';
|
||||
import { errorHandler, requestResponseHandler } from './actionsHandlers';
|
||||
|
||||
const getData = (url) => {
|
||||
return fetch(`${url}`).then(requestResponseHandler).catch(errorHandler);
|
||||
};
|
||||
|
||||
const createDatas = (url, newData, csrfToken) => {
|
||||
return fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken,
|
||||
},
|
||||
body: JSON.stringify(newData),
|
||||
credentials: 'include',
|
||||
})
|
||||
.then(requestResponseHandler)
|
||||
.catch(errorHandler);
|
||||
};
|
||||
|
||||
const updateDatas = (url, updatedData, csrfToken) => {
|
||||
return fetch(`${url}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken,
|
||||
},
|
||||
body: JSON.stringify(updatedData),
|
||||
credentials: 'include',
|
||||
})
|
||||
.then(requestResponseHandler)
|
||||
.catch(errorHandler);
|
||||
};
|
||||
|
||||
const removeDatas = (url, csrfToken) => {
|
||||
return fetch(`${url}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken,
|
||||
},
|
||||
credentials: 'include',
|
||||
})
|
||||
.then(requestResponseHandler)
|
||||
.catch(errorHandler);
|
||||
};
|
||||
|
||||
export const fetchPlannings = (
|
||||
establishment_id = null,
|
||||
planningMode = null
|
||||
) => {
|
||||
let url = `${BE_PLANNING_PLANNINGS_URL}`;
|
||||
if (establishment_id) {
|
||||
url += `?establishment_id=${establishment_id}`;
|
||||
}
|
||||
if (planningMode) {
|
||||
url += `&planning_mode=${planningMode}`;
|
||||
}
|
||||
return getData(url);
|
||||
};
|
||||
|
||||
export const getPlanning = (id) => {
|
||||
return getData(`${BE_PLANNING_PLANNINGS_URL}/${id}`);
|
||||
};
|
||||
|
||||
export const createPlanning = (newData, csrfToken) => {
|
||||
return createDatas(`${BE_PLANNING_PLANNINGS_URL}`, newData, csrfToken);
|
||||
};
|
||||
|
||||
export const updatePlanning = (id, newData, csrfToken) => {
|
||||
return updateDatas(`${BE_PLANNING_PLANNINGS_URL}/${id}`, newData, csrfToken);
|
||||
};
|
||||
|
||||
export const deletePlanning = (id, csrfToken) => {
|
||||
return removeDatas(`${BE_PLANNING_PLANNINGS_URL}/${id}`, csrfToken);
|
||||
};
|
||||
|
||||
export const fetchEvents = (establishment_id = null, planningMode = null) => {
|
||||
let url = `${BE_PLANNING_EVENTS_URL}`;
|
||||
if (establishment_id) {
|
||||
url += `?establishment_id=${establishment_id}`;
|
||||
}
|
||||
if (planningMode) {
|
||||
url += `&planning_mode=${planningMode}`;
|
||||
}
|
||||
|
||||
return getData(url);
|
||||
};
|
||||
|
||||
export const getEvent = (id) => {
|
||||
return getData(`${BE_PLANNING_EVENTS_URL}/${id}`);
|
||||
};
|
||||
|
||||
export const createEvent = (newData, csrfToken) => {
|
||||
return createDatas(`${BE_PLANNING_EVENTS_URL}`, newData, csrfToken);
|
||||
};
|
||||
|
||||
export const updateEvent = (id, newData, csrfToken) => {
|
||||
return updateDatas(`${BE_PLANNING_EVENTS_URL}/${id}`, newData, csrfToken);
|
||||
};
|
||||
|
||||
export const deleteEvent = (id, csrfToken) => {
|
||||
return removeDatas(`${BE_PLANNING_EVENTS_URL}/${id}`, csrfToken);
|
||||
};
|
||||
|
||||
export const fetchUpcomingEvents = (establishment_id = null) => {
|
||||
let url = `${BE_PLANNING_EVENTS_URL}/upcoming`;
|
||||
if (establishment_id) {
|
||||
url += `?establishment_id=${establishment_id}`;
|
||||
}
|
||||
return getData(`${url}`);
|
||||
};
|
||||
388
Front-End/src/app/actions/registerFileGroupAction.js
Normal file
388
Front-End/src/app/actions/registerFileGroupAction.js
Normal file
@ -0,0 +1,388 @@
|
||||
import {
|
||||
BE_SUBSCRIPTION_REGISTRATIONFILE_GROUPS_URL,
|
||||
BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_TEMPLATES_URL,
|
||||
BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_MASTERS_URL,
|
||||
BE_SUBSCRIPTION_REGISTRATION_PARENT_FILE_MASTERS_URL,
|
||||
BE_SUBSCRIPTION_REGISTRATION_PARENT_FILE_TEMPLATES_URL,
|
||||
FE_API_DOCUSEAL_CLONE_URL,
|
||||
FE_API_DOCUSEAL_DOWNLOAD_URL,
|
||||
FE_API_DOCUSEAL_GENERATE_TOKEN,
|
||||
FE_API_DOCUSEAL_DELETE_URL
|
||||
} from '@/utils/Url';
|
||||
import { errorHandler, requestResponseHandler } from './actionsHandlers';
|
||||
|
||||
// FETCH requests
|
||||
|
||||
export async function fetchRegistrationFileGroups(establishment) {
|
||||
const response = await fetch(
|
||||
`${BE_SUBSCRIPTION_REGISTRATIONFILE_GROUPS_URL}?establishment_id=${establishment}`,
|
||||
{
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch file groups');
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export const fetchRegistrationFileFromGroup = async (groupId) => {
|
||||
const response = await fetch(
|
||||
`${BE_SUBSCRIPTION_REGISTRATIONFILE_GROUPS_URL}/${groupId}/school_file_templates`,
|
||||
{
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
'Erreur lors de la récupération des fichiers associés au groupe'
|
||||
);
|
||||
}
|
||||
return response.json();
|
||||
};
|
||||
|
||||
export const fetchRegistrationSchoolFileMasters = (establishment) => {
|
||||
let url = `${BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_MASTERS_URL}?establishment_id=${establishment}`;
|
||||
const request = new Request(`${url}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
return fetch(request).then(requestResponseHandler).catch(errorHandler);
|
||||
};
|
||||
|
||||
export const fetchRegistrationParentFileMasters = (establishment) => {
|
||||
let url = `${BE_SUBSCRIPTION_REGISTRATION_PARENT_FILE_MASTERS_URL}?establishment_id=${establishment}`;
|
||||
const request = new Request(`${url}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
return fetch(request).then(requestResponseHandler).catch(errorHandler);
|
||||
};
|
||||
|
||||
export const fetchRegistrationSchoolFileTemplates = (establishment) => {
|
||||
let url = `${BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_TEMPLATES_URL}?establishment_id=${establishment}`;
|
||||
const request = new Request(`${url}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
return fetch(request).then(requestResponseHandler).catch(errorHandler);
|
||||
};
|
||||
|
||||
// CREATE requests
|
||||
|
||||
export async function createRegistrationFileGroup(groupData, csrfToken) {
|
||||
const response = await fetch(
|
||||
`${BE_SUBSCRIPTION_REGISTRATIONFILE_GROUPS_URL}`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken,
|
||||
},
|
||||
body: JSON.stringify(groupData),
|
||||
credentials: 'include',
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to create file group');
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export const createRegistrationSchoolFileMaster = (data, csrfToken) => {
|
||||
return fetch(`${BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_MASTERS_URL}`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
headers: {
|
||||
'X-CSRFToken': csrfToken,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
credentials: 'include',
|
||||
})
|
||||
.then(requestResponseHandler)
|
||||
.catch(errorHandler);
|
||||
};
|
||||
|
||||
export const createRegistrationParentFileMaster = (data, csrfToken) => {
|
||||
return fetch(`${BE_SUBSCRIPTION_REGISTRATION_PARENT_FILE_MASTERS_URL}`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
headers: {
|
||||
'X-CSRFToken': csrfToken,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
credentials: 'include',
|
||||
})
|
||||
.then(requestResponseHandler)
|
||||
.catch(errorHandler);
|
||||
};
|
||||
|
||||
export const createRegistrationSchoolFileTemplate = (data, csrfToken) => {
|
||||
return fetch(`${BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_TEMPLATES_URL}`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
headers: {
|
||||
'X-CSRFToken': csrfToken,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
credentials: 'include',
|
||||
})
|
||||
.then(requestResponseHandler)
|
||||
.catch(errorHandler);
|
||||
};
|
||||
|
||||
export const createRegistrationParentFileTemplate = (data, csrfToken) => {
|
||||
return fetch(`${BE_SUBSCRIPTION_REGISTRATION_PARENT_FILE_TEMPLATES_URL}`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
headers: {
|
||||
'X-CSRFToken': csrfToken,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
credentials: 'include',
|
||||
})
|
||||
.then(requestResponseHandler)
|
||||
.catch(errorHandler);
|
||||
};
|
||||
|
||||
// EDIT requests
|
||||
|
||||
export const editRegistrationFileGroup = async (
|
||||
groupId,
|
||||
groupData,
|
||||
csrfToken
|
||||
) => {
|
||||
const response = await fetch(
|
||||
`${BE_SUBSCRIPTION_REGISTRATIONFILE_GROUPS_URL}/${groupId}`,
|
||||
{
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken,
|
||||
},
|
||||
body: JSON.stringify(groupData),
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Erreur lors de la modification du groupe');
|
||||
}
|
||||
|
||||
return response.json();
|
||||
};
|
||||
|
||||
export const editRegistrationSchoolFileMaster = (fileId, data, csrfToken) => {
|
||||
return fetch(
|
||||
`${BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_MASTERS_URL}/${fileId}`,
|
||||
{
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(data),
|
||||
headers: {
|
||||
'X-CSRFToken': csrfToken,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
credentials: 'include',
|
||||
}
|
||||
)
|
||||
.then(requestResponseHandler)
|
||||
.catch(errorHandler);
|
||||
};
|
||||
|
||||
export const editRegistrationParentFileMaster = (id, data, csrfToken) => {
|
||||
return fetch(
|
||||
`${BE_SUBSCRIPTION_REGISTRATION_PARENT_FILE_MASTERS_URL}/${id}`,
|
||||
{
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(data),
|
||||
headers: {
|
||||
'X-CSRFToken': csrfToken,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
credentials: 'include',
|
||||
}
|
||||
)
|
||||
.then(requestResponseHandler)
|
||||
.catch(errorHandler);
|
||||
};
|
||||
|
||||
export const editRegistrationSchoolFileTemplates = (
|
||||
fileId,
|
||||
data,
|
||||
csrfToken
|
||||
) => {
|
||||
return fetch(
|
||||
`${BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_TEMPLATES_URL}/${fileId}`,
|
||||
{
|
||||
method: 'PUT',
|
||||
body: data,
|
||||
headers: {
|
||||
'X-CSRFToken': csrfToken,
|
||||
},
|
||||
credentials: 'include',
|
||||
}
|
||||
)
|
||||
.then(requestResponseHandler)
|
||||
.catch(errorHandler);
|
||||
};
|
||||
|
||||
export const editRegistrationParentFileTemplates = (
|
||||
fileId,
|
||||
data,
|
||||
csrfToken
|
||||
) => {
|
||||
return fetch(
|
||||
`${BE_SUBSCRIPTION_REGISTRATION_PARENT_FILE_TEMPLATES_URL}/${fileId}`,
|
||||
{
|
||||
method: 'PUT',
|
||||
body: data,
|
||||
headers: {
|
||||
'X-CSRFToken': csrfToken,
|
||||
},
|
||||
credentials: 'include',
|
||||
}
|
||||
)
|
||||
.then(requestResponseHandler)
|
||||
.catch(errorHandler);
|
||||
};
|
||||
|
||||
// DELETE requests
|
||||
|
||||
export async function deleteRegistrationFileGroup(groupId, csrfToken) {
|
||||
const response = await fetch(
|
||||
`${BE_SUBSCRIPTION_REGISTRATIONFILE_GROUPS_URL}/${groupId}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'X-CSRFToken': csrfToken,
|
||||
},
|
||||
credentials: 'include',
|
||||
}
|
||||
);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
export const deleteRegistrationSchoolFileMaster = (fileId, csrfToken) => {
|
||||
return fetch(
|
||||
`${BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_MASTERS_URL}/${fileId}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'X-CSRFToken': csrfToken,
|
||||
},
|
||||
credentials: 'include',
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const deleteRegistrationParentFileMaster = (id, csrfToken) => {
|
||||
return fetch(
|
||||
`${BE_SUBSCRIPTION_REGISTRATION_PARENT_FILE_MASTERS_URL}/${id}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'X-CSRFToken': csrfToken,
|
||||
},
|
||||
credentials: 'include',
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const deleteRegistrationSchoolFileTemplates = (fileId, csrfToken) => {
|
||||
return fetch(
|
||||
`${BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_TEMPLATES_URL}/${fileId}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'X-CSRFToken': csrfToken,
|
||||
},
|
||||
credentials: 'include',
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const deleteRegistrationParentFileTemplate = (id, csrfToken) => {
|
||||
return fetch(
|
||||
`${BE_SUBSCRIPTION_REGISTRATION_PARENT_FILE_TEMPLATES_URL}/${id}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'X-CSRFToken': csrfToken,
|
||||
},
|
||||
credentials: 'include',
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
// API requests
|
||||
export const removeTemplate = (templateId, selectedEstablishmentId, apiDocuseal) => {
|
||||
return fetch(`${FE_API_DOCUSEAL_DELETE_URL}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
templateId,
|
||||
establishment_id :selectedEstablishmentId,
|
||||
apiDocuseal
|
||||
}),
|
||||
})
|
||||
.then(requestResponseHandler)
|
||||
.catch(errorHandler);
|
||||
};
|
||||
|
||||
export const cloneTemplate = (templateId, email, is_required, selectedEstablishmentId, apiDocuseal) => {
|
||||
return fetch(`${FE_API_DOCUSEAL_CLONE_URL}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
templateId,
|
||||
email,
|
||||
is_required,
|
||||
establishment_id :selectedEstablishmentId,
|
||||
apiDocuseal
|
||||
}),
|
||||
})
|
||||
.then(requestResponseHandler)
|
||||
.catch(errorHandler);
|
||||
};
|
||||
|
||||
export const downloadTemplate = (slug, selectedEstablishmentId, apiDocuseal) => {
|
||||
const url = `${FE_API_DOCUSEAL_DOWNLOAD_URL}/${slug}?establishment_id=${selectedEstablishmentId}&apiDocuseal=${apiDocuseal}`;
|
||||
return fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
.then(requestResponseHandler)
|
||||
.catch(errorHandler);
|
||||
};
|
||||
|
||||
export const generateToken = (email, id = null, selectedEstablishmentId, apiDocuseal) => {
|
||||
return fetch(`${FE_API_DOCUSEAL_GENERATE_TOKEN}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ user_email: email, id, establishment_id :selectedEstablishmentId, apiDocuseal }),
|
||||
})
|
||||
.then(requestResponseHandler)
|
||||
.catch(errorHandler);
|
||||
};
|
||||
194
Front-End/src/app/actions/schoolAction.js
Normal file
194
Front-End/src/app/actions/schoolAction.js
Normal file
@ -0,0 +1,194 @@
|
||||
import {
|
||||
BE_SCHOOL_SPECIALITIES_URL,
|
||||
BE_SCHOOL_TEACHERS_URL,
|
||||
BE_SCHOOL_SCHOOLCLASSES_URL,
|
||||
BE_SCHOOL_PLANNINGS_URL,
|
||||
BE_SCHOOL_FEES_URL,
|
||||
BE_SCHOOL_DISCOUNTS_URL,
|
||||
BE_SCHOOL_PAYMENT_PLANS_URL,
|
||||
BE_SCHOOL_PAYMENT_MODES_URL,
|
||||
BE_SCHOOL_ESTABLISHMENT_URL,
|
||||
BE_SCHOOL_ESTABLISHMENT_COMPETENCIES_URL,
|
||||
} from '@/utils/Url';
|
||||
import { errorHandler, requestResponseHandler } from './actionsHandlers';
|
||||
|
||||
export const deleteEstablishmentCompetencies = (ids, csrfToken) => {
|
||||
return fetch(BE_SCHOOL_ESTABLISHMENT_COMPETENCIES_URL, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken,
|
||||
},
|
||||
body: JSON.stringify({ ids }),
|
||||
credentials: 'include',
|
||||
})
|
||||
.then(requestResponseHandler)
|
||||
.catch(errorHandler);
|
||||
};
|
||||
|
||||
export const createEstablishmentCompetencies = (newData, csrfToken) => {
|
||||
return fetch(BE_SCHOOL_ESTABLISHMENT_COMPETENCIES_URL, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken,
|
||||
},
|
||||
body: JSON.stringify(newData),
|
||||
credentials: 'include',
|
||||
})
|
||||
.then(requestResponseHandler)
|
||||
.catch(errorHandler);
|
||||
};
|
||||
|
||||
export const fetchEstablishmentCompetencies = (establishment, cycle = 1) => {
|
||||
return fetch(
|
||||
`${BE_SCHOOL_ESTABLISHMENT_COMPETENCIES_URL}?establishment_id=${establishment}&cycle=${cycle}`
|
||||
)
|
||||
.then(requestResponseHandler)
|
||||
.catch(errorHandler);
|
||||
};
|
||||
|
||||
export const fetchSpecialities = (establishment) => {
|
||||
return fetch(
|
||||
`${BE_SCHOOL_SPECIALITIES_URL}?establishment_id=${establishment}`
|
||||
)
|
||||
.then(requestResponseHandler)
|
||||
.catch(errorHandler);
|
||||
};
|
||||
|
||||
export const fetchTeachers = (establishment) => {
|
||||
return fetch(`${BE_SCHOOL_TEACHERS_URL}?establishment_id=${establishment}`)
|
||||
.then(requestResponseHandler)
|
||||
.catch(errorHandler);
|
||||
};
|
||||
|
||||
export const fetchClasses = (establishment) => {
|
||||
return fetch(
|
||||
`${BE_SCHOOL_SCHOOLCLASSES_URL}?establishment_id=${establishment}`
|
||||
)
|
||||
.then(requestResponseHandler)
|
||||
.catch(errorHandler);
|
||||
};
|
||||
|
||||
export const fetchClasse = (id) => {
|
||||
return fetch(`${BE_SCHOOL_SCHOOLCLASSES_URL}/${id}`).then(
|
||||
requestResponseHandler
|
||||
);
|
||||
};
|
||||
|
||||
export const fetchSchedules = () => {
|
||||
return fetch(`${BE_SCHOOL_PLANNINGS_URL}`)
|
||||
.then(requestResponseHandler)
|
||||
.catch(errorHandler);
|
||||
};
|
||||
|
||||
export const fetchRegistrationDiscounts = (establishment) => {
|
||||
return fetch(
|
||||
`${BE_SCHOOL_DISCOUNTS_URL}?filter=registration&establishment_id=${establishment}`
|
||||
)
|
||||
.then(requestResponseHandler)
|
||||
.catch(errorHandler);
|
||||
};
|
||||
|
||||
export const fetchTuitionDiscounts = (establishment) => {
|
||||
return fetch(
|
||||
`${BE_SCHOOL_DISCOUNTS_URL}?filter=tuition&establishment_id=${establishment}`
|
||||
)
|
||||
.then(requestResponseHandler)
|
||||
.catch(errorHandler);
|
||||
};
|
||||
|
||||
export const fetchRegistrationFees = (establishment) => {
|
||||
return fetch(
|
||||
`${BE_SCHOOL_FEES_URL}?filter=registration&establishment_id=${establishment}`
|
||||
)
|
||||
.then(requestResponseHandler)
|
||||
.catch(errorHandler);
|
||||
};
|
||||
|
||||
export const fetchTuitionFees = (establishment) => {
|
||||
return fetch(
|
||||
`${BE_SCHOOL_FEES_URL}?filter=tuition&establishment_id=${establishment}`
|
||||
)
|
||||
.then(requestResponseHandler)
|
||||
.catch(errorHandler);
|
||||
};
|
||||
|
||||
export const fetchRegistrationPaymentPlans = (establishment) => {
|
||||
return fetch(
|
||||
`${BE_SCHOOL_PAYMENT_PLANS_URL}?filter=registration&establishment_id=${establishment}`
|
||||
)
|
||||
.then(requestResponseHandler)
|
||||
.catch(errorHandler);
|
||||
};
|
||||
|
||||
export const fetchTuitionPaymentPlans = (establishment) => {
|
||||
return fetch(
|
||||
`${BE_SCHOOL_PAYMENT_PLANS_URL}?filter=tuition&establishment_id=${establishment}`
|
||||
)
|
||||
.then(requestResponseHandler)
|
||||
.catch(errorHandler);
|
||||
};
|
||||
|
||||
export const fetchRegistrationPaymentModes = (establishment) => {
|
||||
return fetch(
|
||||
`${BE_SCHOOL_PAYMENT_MODES_URL}?filter=registration&establishment_id=${establishment}`
|
||||
)
|
||||
.then(requestResponseHandler)
|
||||
.catch(errorHandler);
|
||||
};
|
||||
|
||||
export const fetchTuitionPaymentModes = (establishment) => {
|
||||
return fetch(
|
||||
`${BE_SCHOOL_PAYMENT_MODES_URL}?filter=tuition&establishment_id=${establishment}`
|
||||
)
|
||||
.then(requestResponseHandler)
|
||||
.catch(errorHandler);
|
||||
};
|
||||
|
||||
export const fetchEstablishment = (establishment) => {
|
||||
return fetch(`${BE_SCHOOL_ESTABLISHMENT_URL}/${establishment}`)
|
||||
.then(requestResponseHandler)
|
||||
.catch(errorHandler);
|
||||
};
|
||||
|
||||
export const createDatas = (url, newData, csrfToken) => {
|
||||
return fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken,
|
||||
},
|
||||
body: JSON.stringify(newData),
|
||||
credentials: 'include',
|
||||
})
|
||||
.then(requestResponseHandler)
|
||||
.catch(errorHandler);
|
||||
};
|
||||
|
||||
export const updateDatas = (url, id, updatedData, csrfToken) => {
|
||||
return fetch(`${url}/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken,
|
||||
},
|
||||
body: JSON.stringify(updatedData),
|
||||
credentials: 'include',
|
||||
})
|
||||
.then(requestResponseHandler)
|
||||
.catch(errorHandler);
|
||||
};
|
||||
|
||||
export const removeDatas = (url, id, csrfToken) => {
|
||||
return fetch(`${url}/${id}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken,
|
||||
},
|
||||
credentials: 'include',
|
||||
})
|
||||
.then(requestResponseHandler)
|
||||
.catch(errorHandler);
|
||||
};
|
||||
35
Front-End/src/app/actions/settingsAction.js
Normal file
35
Front-End/src/app/actions/settingsAction.js
Normal file
@ -0,0 +1,35 @@
|
||||
import { BE_SETTINGS_SMTP_URL } from '@/utils/Url';
|
||||
import { errorHandler, requestResponseHandler } from './actionsHandlers';
|
||||
|
||||
export const PENDING = 'pending';
|
||||
export const SUBSCRIBED = 'subscribed';
|
||||
export const ARCHIVED = 'archived';
|
||||
|
||||
export const fetchSmtpSettings = (csrfToken, establishment_id = null) => {
|
||||
let url = `${BE_SETTINGS_SMTP_URL}/`;
|
||||
if (establishment_id) {
|
||||
url += `?establishment_id=${establishment_id}`;
|
||||
}
|
||||
return fetch(`${url}`, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken,
|
||||
},
|
||||
})
|
||||
.then(requestResponseHandler)
|
||||
.catch(errorHandler);
|
||||
};
|
||||
|
||||
export const editSmtpSettings = (data, csrfToken) => {
|
||||
return fetch(`${BE_SETTINGS_SMTP_URL}/`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken,
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
credentials: 'include',
|
||||
})
|
||||
.then(requestResponseHandler)
|
||||
.catch(errorHandler);
|
||||
};
|
||||
304
Front-End/src/app/actions/subscriptionAction.js
Normal file
304
Front-End/src/app/actions/subscriptionAction.js
Normal file
@ -0,0 +1,304 @@
|
||||
import {
|
||||
BE_SUBSCRIPTION_STUDENTS_URL,
|
||||
BE_SUBSCRIPTION_CHILDRENS_URL,
|
||||
BE_SUBSCRIPTION_REGISTERFORMS_URL,
|
||||
BE_SUBSCRIPTION_LAST_GUARDIAN_ID_URL,
|
||||
BE_SUBSCRIPTION_ABSENCES_URL,
|
||||
BE_SUBSCRIPTION_STUDENT_COMPETENCIES_URL,
|
||||
BE_SUBSCRIPTION_SEARCH_STUDENTS_URL,
|
||||
} from '@/utils/Url';
|
||||
|
||||
import { CURRENT_YEAR_FILTER } from '@/utils/constants';
|
||||
import { errorHandler, requestResponseHandler } from './actionsHandlers';
|
||||
|
||||
export const editStudentCompetencies = (data, csrfToken) => {
|
||||
const request = new Request(`${BE_SUBSCRIPTION_STUDENT_COMPETENCIES_URL}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(data),
|
||||
headers: {
|
||||
'X-CSRFToken': csrfToken,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
credentials: 'include',
|
||||
});
|
||||
return fetch(request).then(requestResponseHandler).catch(errorHandler);
|
||||
};
|
||||
|
||||
export const fetchStudentCompetencies = (id, period) => {
|
||||
// Si period est vide, ne pas l'ajouter à l'URL
|
||||
const url = period
|
||||
? `${BE_SUBSCRIPTION_STUDENT_COMPETENCIES_URL}?student_id=${id}&period=${period}`
|
||||
: `${BE_SUBSCRIPTION_STUDENT_COMPETENCIES_URL}?student_id=${id}`;
|
||||
|
||||
const request = new Request(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
return fetch(request).then(requestResponseHandler).catch(errorHandler);
|
||||
};
|
||||
|
||||
export const fetchRegisterForms = (
|
||||
establishment,
|
||||
filter = CURRENT_YEAR_FILTER,
|
||||
page = '',
|
||||
pageSize = '',
|
||||
search = ''
|
||||
) => {
|
||||
let url = `${BE_SUBSCRIPTION_REGISTERFORMS_URL}?filter=${filter}&establishment_id=${establishment}`;
|
||||
if (page !== '' && pageSize !== '') {
|
||||
url = `${BE_SUBSCRIPTION_REGISTERFORMS_URL}?filter=${filter}&establishment_id=${establishment}&page=${page}&search=${search}`;
|
||||
}
|
||||
return fetch(url, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
.then(requestResponseHandler)
|
||||
.catch(errorHandler);
|
||||
};
|
||||
|
||||
export const fetchRegisterForm = (id) => {
|
||||
return fetch(`${BE_SUBSCRIPTION_REGISTERFORMS_URL}/${id}`) // Utilisation de studentId au lieu de codeDI
|
||||
.then(requestResponseHandler)
|
||||
.catch(errorHandler);
|
||||
};
|
||||
export const fetchLastGuardian = () => {
|
||||
return fetch(`${BE_SUBSCRIPTION_LAST_GUARDIAN_ID_URL}`)
|
||||
.then(requestResponseHandler)
|
||||
.catch(errorHandler);
|
||||
};
|
||||
|
||||
export const editRegisterForm = (id, data, csrfToken) => {
|
||||
return fetch(`${BE_SUBSCRIPTION_REGISTERFORMS_URL}/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'X-CSRFToken': csrfToken,
|
||||
},
|
||||
body: data,
|
||||
credentials: 'include',
|
||||
})
|
||||
.then(requestResponseHandler)
|
||||
.catch(errorHandler);
|
||||
};
|
||||
|
||||
export const createRegisterForm = (data, csrfToken) => {
|
||||
const url = `${BE_SUBSCRIPTION_REGISTERFORMS_URL}`;
|
||||
return fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken,
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
credentials: 'include',
|
||||
})
|
||||
.then(requestResponseHandler)
|
||||
.catch(errorHandler);
|
||||
};
|
||||
|
||||
export const sendRegisterForm = (id) => {
|
||||
const url = `${BE_SUBSCRIPTION_REGISTERFORMS_URL}/${id}/send`;
|
||||
return fetch(url, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
.then(requestResponseHandler)
|
||||
.catch(errorHandler);
|
||||
};
|
||||
|
||||
export const resendRegisterForm = (id) => {
|
||||
const url = `${BE_SUBSCRIPTION_REGISTERFORMS_URL}/${id}/resend`;
|
||||
return fetch(url, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
.then(requestResponseHandler)
|
||||
.catch(errorHandler);
|
||||
};
|
||||
export const archiveRegisterForm = (id) => {
|
||||
const url = `${BE_SUBSCRIPTION_REGISTERFORMS_URL}/${id}/archive`;
|
||||
return fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
.then(requestResponseHandler)
|
||||
.catch(errorHandler);
|
||||
};
|
||||
|
||||
export const searchStudents = (establishmentId, query) => {
|
||||
const url = `${BE_SUBSCRIPTION_SEARCH_STUDENTS_URL}/?establishment_id=${establishmentId}&q=${encodeURIComponent(query)}`;
|
||||
return fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
.then(requestResponseHandler)
|
||||
.catch(errorHandler);
|
||||
};
|
||||
|
||||
export const fetchStudents = (establishment, id = null, status = null) => {
|
||||
let url;
|
||||
if (id) {
|
||||
url = `${BE_SUBSCRIPTION_STUDENTS_URL}/${id}`;
|
||||
} else {
|
||||
url = `${BE_SUBSCRIPTION_STUDENTS_URL}?establishment_id=${establishment}`;
|
||||
if (status) {
|
||||
url += `&status=${status}`;
|
||||
}
|
||||
}
|
||||
const request = new Request(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
return fetch(request).then(requestResponseHandler).catch(errorHandler);
|
||||
};
|
||||
|
||||
export const fetchChildren = (id, establishment) => {
|
||||
const request = new Request(
|
||||
`${BE_SUBSCRIPTION_CHILDRENS_URL}/${id}?establishment_id=${establishment}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
return fetch(request).then(requestResponseHandler).catch(errorHandler);
|
||||
};
|
||||
|
||||
export async function getRegisterFormFileTemplate(fileId) {
|
||||
const response = await fetch(
|
||||
`${BE_SUBSCRIPTION_REGISTERFORM_FILE_TEMPLATE_URL}/${fileId}`,
|
||||
{
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch file template');
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export const fetchSchoolFileTemplatesFromRegistrationFiles = async (id) => {
|
||||
const response = await fetch(
|
||||
`${BE_SUBSCRIPTION_REGISTERFORMS_URL}/${id}/school_file_templates`,
|
||||
{
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
'Erreur lors de la récupération des fichiers associés au groupe'
|
||||
);
|
||||
}
|
||||
return response.json();
|
||||
};
|
||||
|
||||
export const fetchParentFileTemplatesFromRegistrationFiles = async (id) => {
|
||||
const response = await fetch(
|
||||
`${BE_SUBSCRIPTION_REGISTERFORMS_URL}/${id}/parent_file_templates`,
|
||||
{
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
'Erreur lors de la récupération des fichiers associés au groupe'
|
||||
);
|
||||
}
|
||||
return response.json();
|
||||
};
|
||||
|
||||
export const dissociateGuardian = async (studentId, guardianId) => {
|
||||
const response = await fetch(
|
||||
`${BE_SUBSCRIPTION_STUDENTS_URL}/${studentId}/guardians/${guardianId}/dissociate`,
|
||||
{
|
||||
credentials: 'include',
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
// Extraire le message d'erreur du backend
|
||||
const errorData = await response.json();
|
||||
const errorMessage =
|
||||
errorData?.error || 'Une erreur est survenue lors de la dissociation.';
|
||||
|
||||
// Jeter une erreur avec le message spécifique
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
};
|
||||
|
||||
export const fetchAbsences = (establishment) => {
|
||||
return fetch(
|
||||
`${BE_SUBSCRIPTION_ABSENCES_URL}?establishment_id=${establishment}`
|
||||
)
|
||||
.then(requestResponseHandler)
|
||||
.catch(errorHandler);
|
||||
};
|
||||
|
||||
export const createAbsences = (data, csrfToken) => {
|
||||
return fetch(`${BE_SUBSCRIPTION_ABSENCES_URL}`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
headers: {
|
||||
'X-CSRFToken': csrfToken,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
credentials: 'include',
|
||||
})
|
||||
.then(requestResponseHandler)
|
||||
.catch(errorHandler);
|
||||
};
|
||||
|
||||
export const editAbsences = (absenceId, payload, csrfToken) => {
|
||||
return fetch(`${BE_SUBSCRIPTION_ABSENCES_URL}/${absenceId}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken,
|
||||
},
|
||||
body: JSON.stringify(payload), // Sérialisez les données en JSON
|
||||
credentials: 'include',
|
||||
}).then((response) => {
|
||||
if (!response.ok) {
|
||||
return response.json().then((error) => {
|
||||
throw new Error(error);
|
||||
});
|
||||
}
|
||||
return response.json();
|
||||
});
|
||||
};
|
||||
|
||||
export const deleteAbsences = (id, csrfToken) => {
|
||||
return fetch(`${BE_SUBSCRIPTION_ABSENCES_URL}/${id}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'X-CSRFToken': csrfToken,
|
||||
},
|
||||
credentials: 'include',
|
||||
});
|
||||
};
|
||||
@ -1,11 +1,11 @@
|
||||
import React from 'react';
|
||||
import { NextIntlClientProvider } from 'next-intl';
|
||||
|
||||
import {getMessages} from 'next-intl/server';
|
||||
import "@/css/tailwind.css";
|
||||
import { getMessages } from 'next-intl/server';
|
||||
import Providers from '@/components/Providers';
|
||||
import '@/css/tailwind.css';
|
||||
import { headers } from 'next/headers';
|
||||
|
||||
export const metadata = {
|
||||
title: "N3WT-SCHOOL",
|
||||
title: 'N3WT-SCHOOL',
|
||||
description: "Gestion de l'école",
|
||||
icons: {
|
||||
icon: [
|
||||
@ -14,22 +14,24 @@ export const metadata = {
|
||||
type: 'image/svg+xml',
|
||||
},
|
||||
{
|
||||
url: '/favicon.ico', // Fallback pour les anciens navigateurs
|
||||
url: '/favicon.ico', // Fallback pour les anciens navigateurs
|
||||
sizes: 'any',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export default async function RootLayout({ children, params: { locale } }) {
|
||||
const messages = await getMessages();
|
||||
export default async function RootLayout({ children, params }) {
|
||||
const headersList = headers();
|
||||
const locale = headersList.get('x-locale') || 'fr';
|
||||
const messages = await getMessages(locale);
|
||||
|
||||
return (
|
||||
<html lang={locale}>
|
||||
<body>
|
||||
<NextIntlClientProvider messages={messages}>
|
||||
<body className="p-0 m-0">
|
||||
<Providers messages={messages} locale={locale} session={params.session}>
|
||||
{children}
|
||||
</NextIntlClientProvider>
|
||||
</Providers>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
@ -1,39 +0,0 @@
|
||||
|
||||
import {
|
||||
BK_LOGIN_URL,
|
||||
FR_USERS_LOGIN_URL ,
|
||||
FR_ADMIN_HOME_URL,
|
||||
FR_ADMIN_SUBSCRIPTIONS_URL,
|
||||
FR_ADMIN_CLASSES_URL,
|
||||
FR_ADMIN_GRADES_URL,
|
||||
FR_ADMIN_PLANNING_URL,
|
||||
FR_ADMIN_TEACHERS_URL,
|
||||
FR_ADMIN_SETTINGS_URL
|
||||
} from '@/utils/Url';
|
||||
|
||||
import {mockUser} from "@/data/mockUsersData";
|
||||
|
||||
const useFakeData = process.env.NEXT_PUBLIC_USE_FAKE_DATA === 'true';
|
||||
|
||||
|
||||
/**
|
||||
* Disconnects the user after confirming the action.
|
||||
* If `NEXT_PUBLIC_USE_FAKE_DATA` environment variable is set to 'true', it will log a fake disconnect and redirect to the login URL.
|
||||
* Otherwise, it will send a PUT request to the backend to update the user profile and then redirect to the login URL.
|
||||
*
|
||||
* @function
|
||||
* @name disconnect
|
||||
* @returns {void}
|
||||
*/
|
||||
export function disconnect () {
|
||||
if (confirm("\nÊtes-vous sûr(e) de vouloir vous déconnecter ?")) {
|
||||
|
||||
if (useFakeData) {
|
||||
console.log('Fake disconnect:', mockUser);
|
||||
router.push(`${FR_USERS_LOGIN_URL}`);
|
||||
} else {
|
||||
console.log('Fake disconnect:', mockUser);
|
||||
router.push(`${FR_USERS_LOGIN_URL}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -1,15 +1,21 @@
|
||||
import Link from 'next/link'
|
||||
import Logo from '../components/Logo'
|
||||
import Link from 'next/link';
|
||||
import Logo from '../components/Logo';
|
||||
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<div className='flex items-center justify-center min-h-screen bg-emerald-500'>
|
||||
<div className='text-center p-6 '>
|
||||
<div className="flex items-center justify-center min-h-screen bg-emerald-500">
|
||||
<div className="text-center p-6 ">
|
||||
<Logo className="w-32 h-32 mx-auto mb-4" />
|
||||
<h2 className='text-2xl font-bold text-emerald-900 mb-4'>404 | Page non trouvée</h2>
|
||||
<p className='text-emerald-900 mb-4'>La ressource que vous souhaitez consulter n'existe pas ou plus.</p>
|
||||
<Link className="text-gray-900 hover:underline" href="/">Retour Accueil</Link>
|
||||
<h2 className="text-2xl font-bold text-emerald-900 mb-4">
|
||||
404 | Page non trouvée
|
||||
</h2>
|
||||
<p className="text-emerald-900 mb-4">
|
||||
La ressource que vous souhaitez consulter n'existe pas ou plus.
|
||||
</p>
|
||||
<Link className="text-gray-900 hover:underline" href="/">
|
||||
Retour Accueil
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
53
Front-End/src/components/Admin/AnnouncementScheduler.js
Normal file
53
Front-End/src/components/Admin/AnnouncementScheduler.js
Normal file
@ -0,0 +1,53 @@
|
||||
'use client';
|
||||
import logger from '@/utils/logger';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
export default function AnnouncementScheduler({ csrfToken }) {
|
||||
const [title, setTitle] = useState('');
|
||||
const [date, setDate] = useState('');
|
||||
const [message, setMessage] = useState('');
|
||||
|
||||
const handleSchedule = () => {
|
||||
// Logique pour planifier une annonce
|
||||
logger.debug('Annonce planifiée:', { title, date, message });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-4 bg-white rounded shadow">
|
||||
<h2 className="text-xl font-bold mb-4">Planifier une Annonce</h2>
|
||||
<div className="mb-4">
|
||||
<label className="block font-medium">Titre</label>
|
||||
<input
|
||||
type="text"
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
className="w-full p-2 border rounded"
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label className="block font-medium">Date</label>
|
||||
<input
|
||||
type="date"
|
||||
value={date}
|
||||
onChange={(e) => setDate(e.target.value)}
|
||||
className="w-full p-2 border rounded"
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label className="block font-medium">Message</label>
|
||||
<textarea
|
||||
value={message}
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
className="w-full p-2 border rounded"
|
||||
rows="5"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleSchedule}
|
||||
className="bg-blue-500 text-white px-4 py-2 rounded"
|
||||
>
|
||||
Planifier
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
158
Front-End/src/components/Admin/EmailSender.js
Normal file
158
Front-End/src/components/Admin/EmailSender.js
Normal file
@ -0,0 +1,158 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { sendEmail, searchRecipients } from '@/app/actions/emailAction';
|
||||
import { fetchSmtpSettings } from '@/app/actions/settingsAction';
|
||||
import { useNotification } from '@/context/NotificationContext';
|
||||
import { useEstablishment } from '@/context/EstablishmentContext';
|
||||
import AlertMessage from '@/components/AlertMessage';
|
||||
import RecipientInput from '@/components/RecipientInput';
|
||||
import { useRouter } from 'next/navigation'; // Ajoute cette ligne
|
||||
import WisiwigTextArea from '@/components/WisiwigTextArea';
|
||||
import logger from '@/utils/logger';
|
||||
import InputText from '@/components/InputText';
|
||||
import Button from '@/components/Button';
|
||||
|
||||
export default function EmailSender({ csrfToken }) {
|
||||
const [recipients, setRecipients] = useState([]);
|
||||
const [fromEmail, setFromEmail] = useState('');
|
||||
const [cc, setCc] = useState([]);
|
||||
const [bcc, setBcc] = useState([]);
|
||||
const [subject, setSubject] = useState('');
|
||||
const [message, setMessage] = useState('');
|
||||
const [smtpConfigured, setSmtpConfigured] = useState(false); // État pour vérifier si SMTP est configuré
|
||||
const { showNotification } = useNotification();
|
||||
const { selectedEstablishmentId } = useEstablishment(); // Récupérer l'establishment_id depuis le contexte
|
||||
const router = useRouter(); // Ajoute cette ligne
|
||||
|
||||
useEffect(() => {
|
||||
// Vérifier si les paramètres SMTP sont configurés
|
||||
fetchSmtpSettings(csrfToken, selectedEstablishmentId)
|
||||
.then((data) => {
|
||||
if (data.smtp_server && data.smtp_port && data.smtp_user) {
|
||||
setFromEmail(data.smtp_user);
|
||||
setSmtpConfigured(true);
|
||||
} else {
|
||||
setSmtpConfigured(false);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error('Erreur lors de la vérification des paramètres SMTP:', {
|
||||
error,
|
||||
});
|
||||
setSmtpConfigured(false);
|
||||
});
|
||||
}, [csrfToken, selectedEstablishmentId]);
|
||||
|
||||
const handleSendEmail = async () => {
|
||||
const data = {
|
||||
recipients,
|
||||
cc,
|
||||
bcc,
|
||||
subject,
|
||||
message,
|
||||
establishment_id: selectedEstablishmentId, // Ajouter l'establishment_id à la payload
|
||||
};
|
||||
|
||||
try {
|
||||
await sendEmail(data);
|
||||
showNotification('Email envoyé avec succès.', 'success', 'Succès');
|
||||
// Réinitialiser les champs après succès
|
||||
setRecipients([]);
|
||||
setCc([]);
|
||||
setBcc([]);
|
||||
setSubject('');
|
||||
setMessage('');
|
||||
} catch (error) {
|
||||
logger.error("Erreur lors de l'envoi de l'email:", { error });
|
||||
showNotification(
|
||||
"Une erreur est survenue lors de l'envoi de l'email.",
|
||||
'error',
|
||||
'Erreur'
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
if (!smtpConfigured) {
|
||||
return (
|
||||
<AlertMessage
|
||||
type="warning"
|
||||
title="Configuration SMTP requise"
|
||||
message="Les paramètres SMTP de cet établissement ne sont pas configurés. Veuillez les configurer dans la page des paramètres."
|
||||
actionLabel="Aller aux paramètres"
|
||||
onAction={() => router.push('/admin/settings?tab=smtp')} // Utilise next/navigation ici
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-3xl mx-auto bg-white rounded-lg shadow-md">
|
||||
{/* Form */}
|
||||
<div className="p-4 flex flex-col min-h-[600px]">
|
||||
{' '}
|
||||
{/* Ajout flex-col et min-h */}
|
||||
{/* Destinataires */}
|
||||
<RecipientInput
|
||||
label="Destinataires"
|
||||
recipients={recipients}
|
||||
setRecipients={setRecipients}
|
||||
searchRecipients={searchRecipients}
|
||||
establishmentId={selectedEstablishmentId}
|
||||
required
|
||||
/>
|
||||
{/* Cc */}
|
||||
<div className="mt-2">
|
||||
<RecipientInput
|
||||
label="Cc"
|
||||
placeholder="Ajouter Cc"
|
||||
recipients={cc}
|
||||
searchRecipients={searchRecipients}
|
||||
establishmentId={selectedEstablishmentId}
|
||||
setRecipients={setCc}
|
||||
/>
|
||||
</div>
|
||||
{/* Bcc */}
|
||||
<div className="mt-2">
|
||||
<RecipientInput
|
||||
label="Cci"
|
||||
placeholder="Ajouter Bcc"
|
||||
recipients={bcc}
|
||||
searchRecipients={searchRecipients}
|
||||
establishmentId={selectedEstablishmentId}
|
||||
setRecipients={setBcc}
|
||||
/>
|
||||
</div>
|
||||
{/* Subject */}
|
||||
<InputText
|
||||
name="subject"
|
||||
label="Sujet"
|
||||
value={subject}
|
||||
onChange={(e) => setSubject(e.target.value)}
|
||||
placeholder="Saisir le sujet"
|
||||
className="mb-4 mt-2"
|
||||
required
|
||||
/>
|
||||
{/* Email Body */}
|
||||
<div className="mb-4 flex flex-col">
|
||||
<WisiwigTextArea
|
||||
label="Mail"
|
||||
value={message}
|
||||
onChange={setMessage}
|
||||
placeholder="Ecrivez votre mail ici..."
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
{/* Footer */}
|
||||
<div className="flex justify-between items-center mt-10">
|
||||
<Button
|
||||
text="Envoyer"
|
||||
onClick={handleSendEmail}
|
||||
primary
|
||||
className="px-4 py-2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
17
Front-End/src/components/Admin/InstantMessaging.js
Normal file
17
Front-End/src/components/Admin/InstantMessaging.js
Normal file
@ -0,0 +1,17 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import InstantChat from '@/components/Chat/InstantChat';
|
||||
import { useEstablishment } from '@/context/EstablishmentContext';
|
||||
import logger from '@/utils/logger';
|
||||
|
||||
export default function InstantMessaging({ csrfToken }) {
|
||||
const { user, selectedEstablishmentId } = useEstablishment();
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col">
|
||||
<InstantChat
|
||||
userProfileId={user.user_id}
|
||||
establishmentId={selectedEstablishmentId}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,9 +1,8 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
const AffectationClasseForm = ({ eleve, onSubmit, classes }) => {
|
||||
|
||||
const AffectationClasseForm = ({ eleve = {}, onSubmit, classes }) => {
|
||||
const [formData, setFormData] = useState({
|
||||
classeAssocie_id: eleve.classeAssocie_id || null,
|
||||
classeAssocie_id: eleve?.classeAssocie_id || null,
|
||||
});
|
||||
|
||||
const handleChange = (e) => {
|
||||
@ -15,12 +14,13 @@ const AffectationClasseForm = ({ eleve, onSubmit, classes }) => {
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
onSubmit({
|
||||
logger.debug(formData);
|
||||
/*onSubmit({
|
||||
eleve: {
|
||||
...formData
|
||||
...formData,
|
||||
},
|
||||
etat:5
|
||||
});
|
||||
etat: 5,
|
||||
});*/
|
||||
};
|
||||
|
||||
return (
|
||||
@ -30,19 +30,22 @@ const AffectationClasseForm = ({ eleve, onSubmit, classes }) => {
|
||||
Classes
|
||||
</label>
|
||||
<div className="mt-2 grid grid-cols-1 gap-4">
|
||||
{classes.map(classe => (
|
||||
{classes.map((classe) => (
|
||||
<div key={classe.id} className="flex items-center">
|
||||
<input
|
||||
type="radio"
|
||||
id={`classe-${classe.id}`}
|
||||
name="classeAssocie_id"
|
||||
value={classe.id}
|
||||
checked={formData.classeAssocie_id === classe.id}
|
||||
onChange={handleChange}
|
||||
className="form-radio h-3 w-3 text-emerald-600 focus:ring-emerald-500 hover:ring-emerald-400 checked:bg-emerald-600 checked:h-3 checked:w-3"
|
||||
type="radio"
|
||||
id={`classe-${classe.id}`}
|
||||
name="classeAssocie_id"
|
||||
value={classe.id}
|
||||
checked={formData.classeAssocie_id === classe.id}
|
||||
onChange={handleChange}
|
||||
className="form-radio h-3 w-3 text-emerald-600 focus:ring-emerald-500 hover:ring-emerald-400 checked:bg-emerald-600 checked:h-3 checked:w-3"
|
||||
/>
|
||||
<label htmlFor={`classe-${classe.id}`} className="ml-2 block text-sm text-gray-900 flex items-center">
|
||||
{classe.nom_ambiance}
|
||||
<label
|
||||
htmlFor={`classe-${classe.id}`}
|
||||
className="ml-2 block text-sm text-gray-900 flex items-center"
|
||||
>
|
||||
{classe.atmosphere_name}
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
@ -50,15 +53,15 @@ const AffectationClasseForm = ({ eleve, onSubmit, classes }) => {
|
||||
</div>
|
||||
<div className="flex justify-end mt-4 space-x-4">
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
className={`px-4 py-2 rounded-md shadow-sm focus:outline-none ${
|
||||
(!formData.classeAssocie_id )
|
||||
? "bg-gray-300 text-gray-700 cursor-not-allowed"
|
||||
: "bg-emerald-500 text-white hover:bg-emerald-600"
|
||||
}`}
|
||||
disabled={(!formData.classeAssocie_id)}
|
||||
onClick={handleSubmit}
|
||||
className={`px-4 py-2 rounded-md shadow-sm focus:outline-none ${
|
||||
!formData.classeAssocie_id
|
||||
? 'bg-gray-300 text-gray-700 cursor-not-allowed'
|
||||
: 'bg-emerald-500 text-white hover:bg-emerald-600'
|
||||
}`}
|
||||
disabled={!formData.classeAssocie_id}
|
||||
>
|
||||
Associer
|
||||
Associer
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@ -1,15 +1,36 @@
|
||||
import React from 'react';
|
||||
|
||||
const AlertMessage = ({ title, message, buttonText, buttonLink }) => {
|
||||
const AlertMessage = ({
|
||||
type = 'info',
|
||||
title,
|
||||
message,
|
||||
actionLabel,
|
||||
onAction,
|
||||
}) => {
|
||||
// Définir les styles en fonction du type d'alerte
|
||||
const typeStyles = {
|
||||
info: 'bg-blue-100 border-blue-500 text-blue-700',
|
||||
warning: 'bg-yellow-100 border-yellow-500 text-yellow-700',
|
||||
error: 'bg-red-100 border-red-500 text-red-700',
|
||||
success: 'bg-green-100 border-green-500 text-green-700',
|
||||
};
|
||||
|
||||
const alertStyle = typeStyles[type] || typeStyles.info;
|
||||
|
||||
return (
|
||||
<div className="alert centered bg-yellow-100 border-l-4 border-yellow-500 text-yellow-700 p-4" role="alert">
|
||||
<div className={`alert centered border-l-4 p-4 ${alertStyle}`} role="alert">
|
||||
<h3 className="font-bold">{title}</h3>
|
||||
<p className="mt-2">{message}</p>
|
||||
<div className="alert-actions mt-4">
|
||||
<a className="btn primary bg-emerald-500 text-white rounded-md px-4 py-2 hover:bg-emerald-600" href={buttonLink}>
|
||||
{buttonText} <i className="icon profile-add"></i>
|
||||
</a>
|
||||
</div>
|
||||
{actionLabel && onAction && (
|
||||
<div className="alert-actions mt-4">
|
||||
<button
|
||||
className="btn primary bg-emerald-500 text-white rounded-md px-4 py-2 hover:bg-emerald-600"
|
||||
onClick={onAction}
|
||||
>
|
||||
{actionLabel}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -2,28 +2,31 @@ import React, { useState } from 'react';
|
||||
import Modal from '@/components/Modal';
|
||||
import { UserPlus } from 'lucide-react';
|
||||
|
||||
const AlertWithModal = ({ title, message, buttonText}) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const AlertWithModal = ({ title, message, buttonText }) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const openModal = () => {
|
||||
setIsOpen(true);
|
||||
};
|
||||
const openModal = () => {
|
||||
setIsOpen(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="alert centered bg-yellow-100 border-l-4 border-yellow-500 text-yellow-700 p-4" role="alert">
|
||||
<h3 className="font-bold">{title}</h3>
|
||||
<p className="mt-2">{message}</p>
|
||||
<div className="alert-actions mt-4">
|
||||
<button
|
||||
className="btn primary bg-emerald-500 text-white rounded-md px-4 py-2 hover:bg-emerald-600 flex items-center"
|
||||
onClick={openModal}
|
||||
>
|
||||
{buttonText} <UserPlus size={20} className="ml-2" />
|
||||
</button>
|
||||
</div>
|
||||
<Modal isOpen={isOpen} setIsOpen={setIsOpen} />
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div
|
||||
className="alert centered bg-yellow-100 border-l-4 border-yellow-500 text-yellow-700 p-4"
|
||||
role="alert"
|
||||
>
|
||||
<h3 className="font-bold">{title}</h3>
|
||||
<p className="mt-2">{message}</p>
|
||||
<div className="alert-actions mt-4">
|
||||
<button
|
||||
className="btn primary bg-emerald-500 text-white rounded-md px-4 py-2 hover:bg-emerald-600 flex items-center"
|
||||
onClick={openModal}
|
||||
>
|
||||
{buttonText} <UserPlus size={20} className="ml-2" />
|
||||
</button>
|
||||
</div>
|
||||
<Modal isOpen={isOpen} setIsOpen={setIsOpen} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AlertWithModal;
|
||||
export default AlertWithModal;
|
||||
|
||||
@ -1,37 +1,39 @@
|
||||
|
||||
import React, { useState } from 'react';
|
||||
|
||||
|
||||
const AlphabetPaginationNumber = ({ letter, active , onClick}) => (
|
||||
<button className={`w-8 h-8 flex items-center justify-center rounded ${
|
||||
active ? 'bg-emerald-500 text-white' : 'text-gray-600 bg-gray-200 hover:bg-gray-50'
|
||||
}`} onClick={onClick}>
|
||||
const AlphabetPaginationNumber = ({ letter, active, onClick }) => (
|
||||
<button
|
||||
className={`w-8 h-8 flex items-center justify-center rounded ${
|
||||
active
|
||||
? 'bg-emerald-500 text-white'
|
||||
: 'text-gray-600 bg-gray-200 hover:bg-gray-50'
|
||||
}`}
|
||||
onClick={onClick}
|
||||
>
|
||||
{letter}
|
||||
</button>
|
||||
);
|
||||
|
||||
|
||||
const AlphabetLinks = ({filter, onLetterClick }) => {
|
||||
const AlphabetLinks = ({ filter, onLetterClick }) => {
|
||||
const [currentLetter, setCurrentLetter] = useState(filter);
|
||||
const alphabet = "*ABCDEFGHIJKLMNOPQRSTUVWXYZ".split('');
|
||||
|
||||
const alphabet = '*ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('');
|
||||
|
||||
return (
|
||||
<div className="px-6 py-4 border-t border-gray-200 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
{alphabet.map((letter) => (
|
||||
<AlphabetPaginationNumber
|
||||
key={letter}
|
||||
letter={letter}
|
||||
active={currentLetter === letter }
|
||||
onClick={() => {setCurrentLetter(letter);onLetterClick(letter)}}
|
||||
/>
|
||||
|
||||
|
||||
))}
|
||||
{alphabet.map((letter) => (
|
||||
<AlphabetPaginationNumber
|
||||
key={letter}
|
||||
letter={letter}
|
||||
active={currentLetter === letter}
|
||||
onClick={() => {
|
||||
setCurrentLetter(letter);
|
||||
onLetterClick(letter);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AlphabetLinks;
|
||||
export default AlphabetLinks;
|
||||
|
||||
@ -1,9 +1,18 @@
|
||||
import React from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
const Button = ({ text, onClick, href, className, primary, icon, disabled}) => {
|
||||
const Button = ({
|
||||
text,
|
||||
onClick,
|
||||
href,
|
||||
className,
|
||||
primary,
|
||||
icon,
|
||||
disabled,
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const baseClass = 'px-4 py-2 rounded-md text-white h-8 flex items-center justify-center';
|
||||
const baseClass =
|
||||
'px-4 py-2 rounded-md text-white h-8 flex items-center justify-center';
|
||||
const primaryClass = 'bg-emerald-500 hover:bg-emerald-600';
|
||||
const secondaryClass = 'bg-gray-300 hover:bg-gray-400 text-black';
|
||||
const buttonClass = `${baseClass} ${primary && !disabled ? primaryClass : secondaryClass} ${className}`;
|
||||
@ -18,7 +27,8 @@ const Button = ({ text, onClick, href, className, primary, icon, disabled}) => {
|
||||
|
||||
return (
|
||||
<button className={buttonClass} onClick={handleClick} disabled={disabled}>
|
||||
{icon && <span className="mr-2">{icon}</span>}
|
||||
{icon && text && <span className="mr-2">{icon}</span>}
|
||||
{icon && !text && icon}
|
||||
{text}
|
||||
</button>
|
||||
);
|
||||
|
||||
@ -1,212 +0,0 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { usePlanning } from '@/context/PlanningContext';
|
||||
import WeekView from '@/components/Calendar/WeekView';
|
||||
import MonthView from '@/components/Calendar/MonthView';
|
||||
import YearView from '@/components/Calendar/YearView';
|
||||
import PlanningView from '@/components/Calendar/PlanningView';
|
||||
import ToggleView from '@/components/ToggleView';
|
||||
import { ChevronLeft, ChevronRight, Plus, ChevronDown } from 'lucide-react';
|
||||
import { format, addWeeks, addMonths, addYears, subWeeks, subMonths, subYears, getWeek, setMonth, setYear } from 'date-fns';
|
||||
import { fr } from 'date-fns/locale';
|
||||
import { AnimatePresence, motion } from 'framer-motion'; // Ajouter cet import
|
||||
|
||||
const Calendar = ({ onDateClick, onEventClick }) => {
|
||||
const { currentDate, setCurrentDate, viewType, setViewType, events, hiddenSchedules } = usePlanning();
|
||||
const [visibleEvents, setVisibleEvents] = useState([]);
|
||||
const [showDatePicker, setShowDatePicker] = useState(false);
|
||||
|
||||
// Ajouter ces fonctions pour la gestion des mois et années
|
||||
const months = Array.from({ length: 12 }, (_, i) => ({
|
||||
value: i,
|
||||
label: format(new Date(2024, i, 1), 'MMMM', { locale: fr })
|
||||
}));
|
||||
|
||||
const years = Array.from({ length: 10 }, (_, i) => ({
|
||||
value: new Date().getFullYear() - 5 + i,
|
||||
label: new Date().getFullYear() - 5 + i
|
||||
}));
|
||||
|
||||
const handleMonthSelect = (monthIndex) => {
|
||||
setCurrentDate(setMonth(currentDate, monthIndex));
|
||||
setShowDatePicker(false);
|
||||
};
|
||||
|
||||
const handleYearSelect = (year) => {
|
||||
setCurrentDate(setYear(currentDate, year));
|
||||
setShowDatePicker(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// S'assurer que le filtrage est fait au niveau parent
|
||||
const filtered = events.filter(event => !hiddenSchedules.includes(event.scheduleId));
|
||||
setVisibleEvents(filtered);
|
||||
console.log('Events filtrés:', filtered); // Debug
|
||||
}, [events, hiddenSchedules]);
|
||||
|
||||
const navigateDate = (direction) => {
|
||||
const getNewDate = () => {
|
||||
switch (viewType) {
|
||||
case 'week':
|
||||
return direction === 'next'
|
||||
? addWeeks(currentDate, 1)
|
||||
: subWeeks(currentDate, 1);
|
||||
case 'month':
|
||||
return direction === 'next'
|
||||
? addMonths(currentDate, 1)
|
||||
: subMonths(currentDate, 1);
|
||||
case 'year':
|
||||
return direction === 'next'
|
||||
? addYears(currentDate, 1)
|
||||
: subYears(currentDate, 1);
|
||||
default:
|
||||
return currentDate;
|
||||
}
|
||||
};
|
||||
|
||||
setCurrentDate(getNewDate());
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex-1 flex h-full flex-col">
|
||||
<div className="flex items-center justify-between p-4 bg-white sticky top-0 z-30 border-b shadow-sm h-[64px]">
|
||||
{/* Navigation à gauche */}
|
||||
<div className="flex items-center gap-4">
|
||||
<button
|
||||
onClick={() => setCurrentDate(new Date())}
|
||||
className="px-3 py-1.5 text-sm font-medium text-gray-700 hover:text-gray-900 bg-gray-100 hover:bg-gray-200 rounded-md transition-colors"
|
||||
>
|
||||
Aujourd'hui
|
||||
</button>
|
||||
<button onClick={() => navigateDate('prev')} className="p-2 hover:bg-gray-100 rounded-full">
|
||||
<ChevronLeft className="w-5 h-5" />
|
||||
</button>
|
||||
|
||||
{/* Menu déroulant pour le mois/année */}
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={() => setShowDatePicker(!showDatePicker)}
|
||||
className="flex items-center gap-1 px-2 py-1 hover:bg-gray-100 rounded-md"
|
||||
>
|
||||
<h2 className="text-xl font-semibold">
|
||||
{format(currentDate, viewType === 'year' ? 'yyyy' : 'MMMM yyyy', { locale: fr })}
|
||||
</h2>
|
||||
<ChevronDown className="w-4 h-4" />
|
||||
</button>
|
||||
|
||||
{/* Menu de sélection du mois/année */}
|
||||
{showDatePicker && (
|
||||
<div className="absolute top-full left-0 mt-1 bg-white border border-gray-200 rounded-lg shadow-lg z-50 w-64">
|
||||
{viewType !== 'year' && (
|
||||
<div className="p-2 border-b">
|
||||
<div className="grid grid-cols-3 gap-1">
|
||||
{months.map((month) => (
|
||||
<button
|
||||
key={month.value}
|
||||
onClick={() => handleMonthSelect(month.value)}
|
||||
className="p-2 text-sm hover:bg-gray-100 rounded-md"
|
||||
>
|
||||
{month.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="p-2">
|
||||
<div className="grid grid-cols-3 gap-1">
|
||||
{years.map((year) => (
|
||||
<button
|
||||
key={year.value}
|
||||
onClick={() => handleYearSelect(year.value)}
|
||||
className="p-2 text-sm hover:bg-gray-100 rounded-md"
|
||||
>
|
||||
{year.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button onClick={() => navigateDate('next')} className="p-2 hover:bg-gray-100 rounded-full">
|
||||
<ChevronRight className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Numéro de semaine au centre */}
|
||||
{viewType === 'week' && (
|
||||
<div className="flex items-center gap-1 text-sm font-medium text-gray-600">
|
||||
<span>Semaine</span>
|
||||
<span className="px-2 py-1 bg-gray-100 rounded-md">
|
||||
{getWeek(currentDate, { weekStartsOn: 1 })}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Contrôles à droite */}
|
||||
<div className="flex items-center gap-4">
|
||||
<ToggleView viewType={viewType} setViewType={setViewType} />
|
||||
<button
|
||||
onClick={onDateClick}
|
||||
className="w-10 h-10 flex items-center justify-center bg-emerald-600 text-white rounded-full hover:bg-emerald-700 shadow-md transition-colors"
|
||||
>
|
||||
<Plus className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Contenu scrollable */}
|
||||
<div className="flex-1 max-h-[calc(100vh-192px)] overflow-hidden">
|
||||
<AnimatePresence mode="wait">
|
||||
{viewType === 'week' && (
|
||||
<motion.div
|
||||
key="week"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="h-full flex flex-col"
|
||||
>
|
||||
<WeekView onDateClick={onDateClick} onEventClick={onEventClick} events={visibleEvents} />
|
||||
</motion.div>
|
||||
)}
|
||||
{viewType === 'month' && (
|
||||
<motion.div
|
||||
key="month"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<MonthView onDateClick={onDateClick} onEventClick={onEventClick} events={visibleEvents} />
|
||||
</motion.div>
|
||||
)}
|
||||
{viewType === 'year' && (
|
||||
<motion.div
|
||||
key="year"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<YearView onDateClick={onDateClick} events={visibleEvents} />
|
||||
</motion.div>
|
||||
)}
|
||||
{viewType === 'planning' && (
|
||||
<motion.div
|
||||
key="planning"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<PlanningView onEventClick={onEventClick} events={visibleEvents} />
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Calendar;
|
||||
265
Front-End/src/components/Calendar/Calendar.js
Normal file
265
Front-End/src/components/Calendar/Calendar.js
Normal file
@ -0,0 +1,265 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { usePlanning, PlanningModes } from '@/context/PlanningContext';
|
||||
import WeekView from '@/components/Calendar/WeekView';
|
||||
import MonthView from '@/components/Calendar/MonthView';
|
||||
import YearView from '@/components/Calendar/YearView';
|
||||
import PlanningView from '@/components/Calendar/PlanningView';
|
||||
import ToggleView from '@/components/ToggleView';
|
||||
import { ChevronLeft, ChevronRight, Plus, ChevronDown } from 'lucide-react';
|
||||
import {
|
||||
format,
|
||||
addWeeks,
|
||||
addMonths,
|
||||
addYears,
|
||||
subWeeks,
|
||||
subMonths,
|
||||
subYears,
|
||||
getWeek,
|
||||
setMonth,
|
||||
setYear,
|
||||
} from 'date-fns';
|
||||
import { fr } from 'date-fns/locale';
|
||||
import { AnimatePresence, motion } from 'framer-motion'; // Ajouter cet import
|
||||
import logger from '@/utils/logger';
|
||||
|
||||
const Calendar = ({ modeSet, onDateClick, onEventClick, planningClassName='' }) => {
|
||||
const {
|
||||
currentDate,
|
||||
setCurrentDate,
|
||||
viewType,
|
||||
setViewType,
|
||||
events,
|
||||
hiddenSchedules,
|
||||
planningMode,
|
||||
parentView
|
||||
} = usePlanning();
|
||||
const [visibleEvents, setVisibleEvents] = useState([]);
|
||||
const [showDatePicker, setShowDatePicker] = useState(false);
|
||||
|
||||
// Ajouter ces fonctions pour la gestion des mois et années
|
||||
const months = Array.from({ length: 12 }, (_, i) => ({
|
||||
value: i,
|
||||
label: format(new Date(2024, i, 1), 'MMMM', { locale: fr }),
|
||||
}));
|
||||
|
||||
const years = Array.from({ length: 10 }, (_, i) => ({
|
||||
value: new Date().getFullYear() - 5 + i,
|
||||
label: new Date().getFullYear() - 5 + i,
|
||||
}));
|
||||
|
||||
const handleMonthSelect = (monthIndex) => {
|
||||
setCurrentDate(setMonth(currentDate, monthIndex));
|
||||
setShowDatePicker(false);
|
||||
};
|
||||
|
||||
const handleYearSelect = (year) => {
|
||||
setCurrentDate(setYear(currentDate, year));
|
||||
setShowDatePicker(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// S'assurer que le filtrage est fait au niveau parent
|
||||
const filtered = events?.filter(
|
||||
(event) => !hiddenSchedules.includes(event.planning)
|
||||
);
|
||||
setVisibleEvents(filtered);
|
||||
logger.debug('Events filtrés:', filtered); // Debug
|
||||
}, [events, hiddenSchedules]);
|
||||
|
||||
const navigateDate = (direction) => {
|
||||
const getNewDate = () => {
|
||||
switch (viewType) {
|
||||
case 'week':
|
||||
return direction === 'next'
|
||||
? addWeeks(currentDate, 1)
|
||||
: subWeeks(currentDate, 1);
|
||||
case 'month':
|
||||
return direction === 'next'
|
||||
? addMonths(currentDate, 1)
|
||||
: subMonths(currentDate, 1);
|
||||
case 'year':
|
||||
return direction === 'next'
|
||||
? addYears(currentDate, 1)
|
||||
: subYears(currentDate, 1);
|
||||
default:
|
||||
return currentDate;
|
||||
}
|
||||
};
|
||||
|
||||
setCurrentDate(getNewDate());
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex-1 flex flex-col">
|
||||
<div className="flex items-center justify-between p-4 bg-white sticky top-0 z-30 border-b shadow-sm h-[64px]">
|
||||
{/* Navigation à gauche */}
|
||||
{planningMode === PlanningModes.PLANNING && (
|
||||
<div className="flex items-center gap-4">
|
||||
<button
|
||||
onClick={() => setCurrentDate(new Date())}
|
||||
className="px-3 py-1.5 text-sm font-medium text-gray-700 hover:text-gray-900 bg-gray-100 hover:bg-gray-200 rounded-md transition-colors"
|
||||
>
|
||||
Aujourd'hui
|
||||
</button>
|
||||
<button
|
||||
onClick={() => navigateDate('prev')}
|
||||
className="p-2 hover:bg-gray-100 rounded-full"
|
||||
>
|
||||
<ChevronLeft className="w-5 h-5" />
|
||||
</button>
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={() => setShowDatePicker(!showDatePicker)}
|
||||
className="flex items-center gap-1 px-2 py-1 hover:bg-gray-100 rounded-md"
|
||||
>
|
||||
<h2 className="text-xl font-semibold">
|
||||
{format(
|
||||
currentDate,
|
||||
viewType === 'year' ? 'yyyy' : 'MMMM yyyy',
|
||||
{ locale: fr }
|
||||
)}
|
||||
</h2>
|
||||
<ChevronDown className="w-4 h-4" />
|
||||
</button>
|
||||
{showDatePicker && (
|
||||
<div className="absolute top-full left-0 mt-1 bg-white border border-gray-200 rounded-lg shadow-lg z-50 w-64">
|
||||
{viewType !== 'year' && (
|
||||
<div className="p-2 border-b">
|
||||
<div className="grid grid-cols-3 gap-1">
|
||||
{months.map((month) => (
|
||||
<button
|
||||
key={month.value}
|
||||
onClick={() => handleMonthSelect(month.value)}
|
||||
className="p-2 text-sm hover:bg-gray-100 rounded-md"
|
||||
>
|
||||
{month.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="p-2">
|
||||
<div className="grid grid-cols-3 gap-1">
|
||||
{years.map((year) => (
|
||||
<button
|
||||
key={year.value}
|
||||
onClick={() => handleYearSelect(year.value)}
|
||||
className="p-2 text-sm hover:bg-gray-100 rounded-md"
|
||||
>
|
||||
{year.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => navigateDate('next')}
|
||||
className="p-2 hover:bg-gray-100 rounded-full"
|
||||
>
|
||||
<ChevronRight className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Centre : numéro de semaine ou classe/niveau */}
|
||||
<div className="flex-1 flex justify-center">
|
||||
{((planningMode === PlanningModes.PLANNING || planningMode === PlanningModes.CLASS_SCHEDULE) && viewType === 'week' && !parentView) && (
|
||||
<div className="flex items-center gap-1 text-sm font-medium text-gray-600">
|
||||
<span>Semaine</span>
|
||||
<span className="px-2 py-1 bg-gray-100 rounded-md">
|
||||
{getWeek(currentDate, { weekStartsOn: 1 })}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{parentView && (
|
||||
<span className="px-2 py-1 bg-gray-100 rounded-md text-base font-semibold">
|
||||
{/* À adapter selon les props disponibles */}
|
||||
{planningClassName}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Contrôles à droite */}
|
||||
<div className="flex items-center gap-4">
|
||||
{planningMode === PlanningModes.PLANNING && (
|
||||
<ToggleView viewType={viewType} setViewType={setViewType} />
|
||||
)}
|
||||
{(planningMode === PlanningModes.PLANNING || (planningMode === PlanningModes.CLASS_SCHEDULE && !parentView)) && (
|
||||
<button
|
||||
onClick={onDateClick}
|
||||
className="w-10 h-10 flex items-center justify-center bg-emerald-600 text-white rounded-full hover:bg-emerald-700 shadow-md transition-colors"
|
||||
>
|
||||
<Plus className="w-5 h-5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Contenu scrollable */}
|
||||
<div className="flex-1 max-h-[calc(100vh-192px)] overflow-hidden">
|
||||
<AnimatePresence mode="wait">
|
||||
{viewType === 'week' && (
|
||||
<motion.div
|
||||
key="week"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="h-full flex flex-col"
|
||||
>
|
||||
<WeekView
|
||||
onDateClick={onDateClick}
|
||||
onEventClick={onEventClick}
|
||||
events={visibleEvents}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
{viewType === 'month' && (
|
||||
<motion.div
|
||||
key="month"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<MonthView
|
||||
onDateClick={onDateClick}
|
||||
onEventClick={onEventClick}
|
||||
events={visibleEvents}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
{viewType === 'year' && (
|
||||
<motion.div
|
||||
key="year"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<YearView onDateClick={onDateClick} events={visibleEvents} />
|
||||
</motion.div>
|
||||
)}
|
||||
{viewType === 'planning' && (
|
||||
<motion.div
|
||||
key="planning"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<PlanningView
|
||||
onEventClick={onEventClick}
|
||||
events={visibleEvents}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Calendar;
|
||||
340
Front-End/src/components/Calendar/EventModal.js
Normal file
340
Front-End/src/components/Calendar/EventModal.js
Normal file
@ -0,0 +1,340 @@
|
||||
import { usePlanning, RecurrenceType } from '@/context/PlanningContext';
|
||||
import { format } from 'date-fns';
|
||||
import React from 'react';
|
||||
import { useNotification } from '@/context/NotificationContext';
|
||||
import Modal from '@/components/Modal';
|
||||
|
||||
export default function EventModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
eventData,
|
||||
setEventData,
|
||||
}) {
|
||||
const { addEvent, handleUpdateEvent, handleDeleteEvent, schedules } =
|
||||
usePlanning();
|
||||
const { showNotification } = useNotification();
|
||||
|
||||
// S'assurer que planning est défini lors du premier rendu
|
||||
React.useEffect(() => {
|
||||
if (!eventData?.planning && schedules.length > 0) {
|
||||
setEventData((prev) => ({
|
||||
...prev,
|
||||
planning: schedules[0].id,
|
||||
color: schedules[0].color,
|
||||
}));
|
||||
}
|
||||
}, [schedules, eventData?.planning]);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const recurrenceOptions = [
|
||||
{ value: RecurrenceType.NONE, label: 'Aucune' },
|
||||
{ value: RecurrenceType.DAILY, label: 'Quotidienne' },
|
||||
{ value: RecurrenceType.WEEKLY, label: 'Hebdomadaire' },
|
||||
{ value: RecurrenceType.MONTHLY, label: 'Mensuelle' },
|
||||
/* { value: RecurrenceType.CUSTOM, label: 'Personnalisée' }, */
|
||||
];
|
||||
|
||||
const daysOfWeek = [
|
||||
{ value: 1, label: 'Lun' },
|
||||
{ value: 2, label: 'Mar' },
|
||||
{ value: 3, label: 'Mer' },
|
||||
{ value: 4, label: 'Jeu' },
|
||||
{ value: 5, label: 'Ven' },
|
||||
{ value: 6, label: 'Sam' },
|
||||
{ value: 0, label: 'Dim' },
|
||||
];
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!eventData.planning) {
|
||||
showNotification(
|
||||
'Veuillez sélectionner un planning',
|
||||
'warning',
|
||||
'Attention'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedSchedule = schedules.find((s) => s.id === eventData.planning);
|
||||
|
||||
if (eventData.id) {
|
||||
handleUpdateEvent(eventData.id, {
|
||||
...eventData,
|
||||
planning: eventData.planning, // S'assurer que planning est bien défini
|
||||
color: eventData.color || selectedSchedule?.color,
|
||||
});
|
||||
} else {
|
||||
addEvent({
|
||||
...eventData,
|
||||
id: `event-${Date.now()}`,
|
||||
planning: eventData.planning, // S'assurer que planning est bien défini
|
||||
color: eventData.color || selectedSchedule?.color,
|
||||
});
|
||||
}
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
if (
|
||||
eventData.id &&
|
||||
confirm('Êtes-vous sûr de vouloir supprimer cet événement ?')
|
||||
) {
|
||||
handleDeleteEvent(eventData.id);
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
setIsOpen={onClose}
|
||||
title={eventData.id ? "Modifier l'événement" : 'Nouvel événement'}
|
||||
modalClassName="w-full max-w-md"
|
||||
>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
{/* Titre */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Titre
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={eventData.title || ''}
|
||||
onChange={(e) =>
|
||||
setEventData({ ...eventData, title: e.target.value })
|
||||
}
|
||||
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-emerald-500"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Description
|
||||
</label>
|
||||
<textarea
|
||||
value={eventData.description || ''}
|
||||
onChange={(e) =>
|
||||
setEventData({ ...eventData, description: e.target.value })
|
||||
}
|
||||
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-emerald-500"
|
||||
rows="3"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Planning */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Planning
|
||||
</label>
|
||||
<select
|
||||
value={eventData.planning || schedules[0]?.id}
|
||||
onChange={(e) => {
|
||||
const selectedSchedule = schedules.find(
|
||||
(s) => s.id === e.target.value
|
||||
);
|
||||
setEventData({
|
||||
...eventData,
|
||||
planning: e.target.value,
|
||||
color: selectedSchedule?.color || '#10b981',
|
||||
});
|
||||
}}
|
||||
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-emerald-500"
|
||||
required
|
||||
>
|
||||
{schedules.map((schedule) => (
|
||||
<option key={schedule.id} value={schedule.id}>
|
||||
{schedule.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Couleur */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Couleur
|
||||
</label>
|
||||
<input
|
||||
type="color"
|
||||
value={
|
||||
eventData.color ||
|
||||
schedules.find((s) => s.id === eventData.planning)?.color ||
|
||||
'#10b981'
|
||||
}
|
||||
onChange={(e) =>
|
||||
setEventData({ ...eventData, color: e.target.value })
|
||||
}
|
||||
className="w-full h-10 p-1 rounded border"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Récurrence */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Récurrence
|
||||
</label>
|
||||
<select
|
||||
value={eventData.recursionType || RecurrenceType.NONE}
|
||||
onChange={(e) => {
|
||||
return setEventData({
|
||||
...eventData,
|
||||
recursionType: e.target.value,
|
||||
});
|
||||
}}
|
||||
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-emerald-500"
|
||||
>
|
||||
{recurrenceOptions.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Paramètres de récurrence personnalisée */}
|
||||
{eventData.recursionType == RecurrenceType.CUSTOM && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Jours de répétition
|
||||
</label>
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
{daysOfWeek.map((day) => (
|
||||
<button
|
||||
key={day.value}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const days = eventData.selectedDays || [];
|
||||
const newDays = days.includes(day.value)
|
||||
? days.filter((d) => d !== day.value)
|
||||
: [...days, day.value];
|
||||
setEventData({ ...eventData, selectedDays: newDays });
|
||||
}}
|
||||
className={`px-3 py-1 rounded-full text-sm ${
|
||||
(eventData.selectedDays || []).includes(day.value)
|
||||
? 'bg-emerald-100 text-emerald-800'
|
||||
: 'bg-gray-100 text-gray-600'
|
||||
}`}
|
||||
>
|
||||
{day.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Date de fin de récurrence */}
|
||||
{eventData.recursionType != RecurrenceType.NONE && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Fin de récurrence
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
value={
|
||||
eventData.recursionEnd
|
||||
? format(new Date(eventData.recursionEnd), 'yyyy-MM-dd')
|
||||
: ''
|
||||
}
|
||||
onChange={(e) =>
|
||||
setEventData({
|
||||
...eventData,
|
||||
recursionEnd: e.target.value
|
||||
? new Date(e.target.value).toISOString()
|
||||
: null,
|
||||
})
|
||||
}
|
||||
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-emerald-500"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Dates */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Début
|
||||
</label>
|
||||
<input
|
||||
type="datetime-local"
|
||||
value={format(new Date(eventData.start), "yyyy-MM-dd'T'HH:mm")}
|
||||
onChange={(e) =>
|
||||
setEventData({
|
||||
...eventData,
|
||||
start: new Date(e.target.value).toISOString(),
|
||||
})
|
||||
}
|
||||
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-emerald-500"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Fin
|
||||
</label>
|
||||
<input
|
||||
type="datetime-local"
|
||||
value={format(new Date(eventData.end), "yyyy-MM-dd'T'HH:mm")}
|
||||
onChange={(e) =>
|
||||
setEventData({
|
||||
...eventData,
|
||||
end: new Date(e.target.value).toISOString(),
|
||||
})
|
||||
}
|
||||
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-emerald-500"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Lieu */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Lieu
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={eventData.location || ''}
|
||||
onChange={(e) =>
|
||||
setEventData({ ...eventData, location: e.target.value })
|
||||
}
|
||||
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-emerald-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Boutons */}
|
||||
<div className="flex justify-between gap-2 mt-6">
|
||||
<div>
|
||||
{eventData.id && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleDelete}
|
||||
className="px-4 py-2 text-red-600 hover:bg-red-50 rounded"
|
||||
>
|
||||
Supprimer
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 text-gray-600 hover:bg-gray-100 rounded"
|
||||
>
|
||||
Annuler
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="px-4 py-2 bg-emerald-600 text-white rounded hover:bg-emerald-700"
|
||||
>
|
||||
{eventData.id ? 'Modifier' : 'Créer'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@ -1,6 +1,15 @@
|
||||
import React from 'react';
|
||||
import { usePlanning } from '@/context/PlanningContext';
|
||||
import { format, startOfWeek, endOfWeek, eachDayOfInterval, startOfMonth, endOfMonth, isSameMonth, isToday } from 'date-fns';
|
||||
import {
|
||||
format,
|
||||
startOfWeek,
|
||||
endOfWeek,
|
||||
eachDayOfInterval,
|
||||
startOfMonth,
|
||||
endOfMonth,
|
||||
isSameMonth,
|
||||
isToday,
|
||||
} from 'date-fns';
|
||||
import { fr } from 'date-fns/locale';
|
||||
import { getEventsForDate } from '@/utils/events';
|
||||
|
||||
@ -35,7 +44,8 @@ const MonthView = ({ onDateClick, onEventClick }) => {
|
||||
onClick={() => handleDayClick(day)}
|
||||
>
|
||||
<div className="flex justify-between items-center mb-1">
|
||||
<span className={`text-sm font-medium rounded-full w-7 h-7 flex items-center justify-center
|
||||
<span
|
||||
className={`text-sm font-medium rounded-full w-7 h-7 flex items-center justify-center
|
||||
${isCurrentDay ? 'bg-emerald-500 text-white' : ''}
|
||||
${!isCurrentMonth ? 'text-gray-400' : ''}`}
|
||||
>
|
||||
@ -50,7 +60,7 @@ const MonthView = ({ onDateClick, onEventClick }) => {
|
||||
style={{
|
||||
backgroundColor: `${event.color}15`,
|
||||
color: event.color,
|
||||
borderLeft: `2px solid ${event.color}`
|
||||
borderLeft: `2px solid ${event.color}`,
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
@ -69,8 +79,11 @@ const MonthView = ({ onDateClick, onEventClick }) => {
|
||||
<div className="h-full flex flex-col border border-gray-200 rounded-lg bg-white">
|
||||
{/* En-tête des jours de la semaine */}
|
||||
<div className="grid grid-cols-7 border-b">
|
||||
{['Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam', 'Dim'].map(day => (
|
||||
<div key={day} className="p-2 text-center text-sm font-medium text-gray-500">
|
||||
{['Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam', 'Dim'].map((day) => (
|
||||
<div
|
||||
key={day}
|
||||
className="p-2 text-center text-sm font-medium text-gray-500"
|
||||
>
|
||||
{day}
|
||||
</div>
|
||||
))}
|
||||
@ -83,4 +96,4 @@ const MonthView = ({ onDateClick, onEventClick }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default MonthView;
|
||||
export default MonthView;
|
||||
|
||||
@ -15,17 +15,20 @@ const PlanningView = ({ events, onEventClick }) => {
|
||||
|
||||
// Sinon, créer une entrée pour chaque jour
|
||||
const days = eachDayOfInterval({ start, end });
|
||||
return days.map(day => ({
|
||||
return days.map((day) => ({
|
||||
...event,
|
||||
displayDate: day,
|
||||
isMultiDay: true
|
||||
isMultiDay: true,
|
||||
}));
|
||||
};
|
||||
|
||||
// Aplatir tous les événements en incluant les événements sur plusieurs jours
|
||||
const flattenedEvents = events
|
||||
.flatMap(splitEventByDays)
|
||||
.sort((a, b) => new Date(a.displayDate || a.start) - new Date(b.displayDate || b.start));
|
||||
.sort(
|
||||
(a, b) =>
|
||||
new Date(a.displayDate || a.start) - new Date(b.displayDate || b.start)
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="bg-white h-full overflow-auto">
|
||||
@ -58,26 +61,29 @@ const PlanningView = ({ events, onEventClick }) => {
|
||||
<td className="py-3 px-4 text-sm text-gray-900 whitespace-nowrap">
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="font-extrabold">{format(start, 'd')}</span>
|
||||
<span className="font-semibold">{format(start, 'MMM', { locale: fr }).toLowerCase()}</span>
|
||||
<span className="font-semibold">{format(start, 'EEE', { locale: fr })}</span>
|
||||
<span className="font-semibold">
|
||||
{format(start, 'MMM', { locale: fr }).toLowerCase()}
|
||||
</span>
|
||||
<span className="font-semibold">
|
||||
{format(start, 'EEE', { locale: fr })}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="py-3 px-4 text-sm text-gray-900 whitespace-nowrap">
|
||||
<div className="flex items-center">
|
||||
<div
|
||||
<div className="flex items-center">
|
||||
<div
|
||||
className="w-2 h-2 rounded-full mr-2"
|
||||
style={{ backgroundColor: event.color }}
|
||||
/>
|
||||
{isMultiDay
|
||||
? (isSameDay(start, new Date(event.start))
|
||||
? "À partir de "
|
||||
: isSameDay(start, end)
|
||||
? "Jusqu'à "
|
||||
: "Toute la journée")
|
||||
: ""
|
||||
}
|
||||
{format(new Date(event.start), 'HH:mm')}
|
||||
{!isMultiDay && ` - ${format(end, 'HH:mm')}`}
|
||||
{isMultiDay
|
||||
? isSameDay(start, new Date(event.start))
|
||||
? 'À partir de '
|
||||
: isSameDay(start, end)
|
||||
? "Jusqu'à "
|
||||
: 'Toute la journée'
|
||||
: ''}
|
||||
{format(new Date(event.start), 'HH:mm')}
|
||||
{!isMultiDay && ` - ${format(end, 'HH:mm')}`}
|
||||
</div>
|
||||
</td>
|
||||
<td className="py-3 px-4">
|
||||
@ -108,4 +114,4 @@ const PlanningView = ({ events, onEventClick }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default PlanningView;
|
||||
export default PlanningView;
|
||||
|
||||
@ -1,27 +1,44 @@
|
||||
import { useState } from 'react';
|
||||
import { usePlanning } from '@/context/PlanningContext';
|
||||
import { usePlanning, PlanningModes } from '@/context/PlanningContext';
|
||||
import { Plus, Edit2, Eye, EyeOff, Check, X } from 'lucide-react';
|
||||
import logger from '@/utils/logger';
|
||||
|
||||
export default function ScheduleNavigation() {
|
||||
const { schedules, selectedSchedule, setSelectedSchedule, hiddenSchedules, toggleScheduleVisibility, addSchedule, updateSchedule } = usePlanning();
|
||||
export default function ScheduleNavigation({ classes, modeSet = 'event' }) {
|
||||
const {
|
||||
schedules,
|
||||
selectedSchedule,
|
||||
setSelectedSchedule,
|
||||
hiddenSchedules,
|
||||
toggleScheduleVisibility,
|
||||
addSchedule,
|
||||
updateSchedule,
|
||||
planningMode,
|
||||
} = usePlanning();
|
||||
const [editingId, setEditingId] = useState(null);
|
||||
const [editedName, setEditedName] = useState('');
|
||||
const [editedColor, setEditedColor] = useState('');
|
||||
const [editedSchoolClass, setEditedSchoolClass] = useState(null);
|
||||
const [isAddingNew, setIsAddingNew] = useState(false);
|
||||
const [newSchedule, setNewSchedule] = useState({ name: '', color: '#10b981' });
|
||||
const [newSchedule, setNewSchedule] = useState({
|
||||
name: '',
|
||||
color: '#10b981',
|
||||
school_class: '', // Ajout du champ pour la classe
|
||||
});
|
||||
|
||||
const handleEdit = (schedule) => {
|
||||
setEditingId(schedule.id);
|
||||
setEditedName(schedule.name);
|
||||
setEditedColor(schedule.color);
|
||||
setEditedSchoolClass(schedule.school_class);
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
if (editingId) {
|
||||
updateSchedule(editingId, {
|
||||
...schedules.find(s => s.id === editingId),
|
||||
...schedules.find((s) => s.id === editingId),
|
||||
name: editedName,
|
||||
color: editedColor
|
||||
color: editedColor,
|
||||
school_class: editedSchoolClass, // Ajout de l'ID de la classe
|
||||
});
|
||||
setEditingId(null);
|
||||
}
|
||||
@ -29,19 +46,30 @@ export default function ScheduleNavigation() {
|
||||
|
||||
const handleAddNew = () => {
|
||||
if (newSchedule.name) {
|
||||
let payload = {
|
||||
name: newSchedule.name,
|
||||
color: newSchedule.color,
|
||||
};
|
||||
if (planningMode === PlanningModes.CLASS_SCHEDULE) {
|
||||
payload.school_class = newSchedule.school_class; // Ajout de l'ID de la classe
|
||||
}
|
||||
addSchedule({
|
||||
id: `schedule-${Date.now()}`,
|
||||
...newSchedule
|
||||
...payload,
|
||||
});
|
||||
setIsAddingNew(false);
|
||||
setNewSchedule({ name: '', color: '#10b981' });
|
||||
setNewSchedule({ name: '', color: '#10b981', school_class: '' });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<nav className="w-64 border-r p-4">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="font-semibold">Plannings</h2>
|
||||
<h2 className="font-semibold">
|
||||
{planningMode === PlanningModes.CLASS_SCHEDULE
|
||||
? 'Emplois du temps'
|
||||
: 'Plannings'}
|
||||
</h2>
|
||||
<button
|
||||
onClick={() => setIsAddingNew(true)}
|
||||
className="p-1 hover:bg-gray-100 rounded"
|
||||
@ -55,19 +83,52 @@ export default function ScheduleNavigation() {
|
||||
<input
|
||||
type="text"
|
||||
value={newSchedule.name}
|
||||
onChange={(e) => setNewSchedule(prev => ({ ...prev, name: e.target.value }))}
|
||||
onChange={(e) =>
|
||||
setNewSchedule((prev) => ({ ...prev, name: e.target.value }))
|
||||
}
|
||||
className="w-full p-1 mb-2 border rounded"
|
||||
placeholder="Nom du planning"
|
||||
placeholder={
|
||||
planningMode === PlanningModes.CLASS_SCHEDULE
|
||||
? "Nom de l'emplois du temps"
|
||||
: 'Nom du planning'
|
||||
}
|
||||
/>
|
||||
<div className="flex gap-2 items-center mb-2">
|
||||
<label className="text-sm">Couleur:</label>
|
||||
<input
|
||||
type="color"
|
||||
value={newSchedule.color}
|
||||
onChange={(e) => setNewSchedule(prev => ({ ...prev, color: e.target.value }))}
|
||||
onChange={(e) =>
|
||||
setNewSchedule((prev) => ({ ...prev, color: e.target.value }))
|
||||
}
|
||||
className="w-8 h-8"
|
||||
/>
|
||||
</div>
|
||||
{planningMode === PlanningModes.CLASS_SCHEDULE && (
|
||||
<div className="mb-2">
|
||||
<label className="text-sm">Classe (optionnel):</label>
|
||||
<select
|
||||
value={newSchedule.school_class}
|
||||
onChange={(e) =>
|
||||
setNewSchedule((prev) => ({
|
||||
...prev,
|
||||
school_class: e.target.value,
|
||||
}))
|
||||
}
|
||||
className="w-full p-1 border rounded"
|
||||
>
|
||||
<option value="">Aucune</option>
|
||||
{classes.map((classe) => {
|
||||
logger.debug({ classe });
|
||||
return (
|
||||
<option key={classe.id} value={classe.id}>
|
||||
{classe.atmosphere_name}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-end gap-2">
|
||||
<button
|
||||
onClick={() => setIsAddingNew(false)}
|
||||
@ -86,11 +147,13 @@ export default function ScheduleNavigation() {
|
||||
)}
|
||||
|
||||
<ul className="space-y-2">
|
||||
{schedules.map(schedule => (
|
||||
{schedules.map((schedule) => (
|
||||
<li
|
||||
key={schedule.id}
|
||||
className={`p-2 rounded ${
|
||||
selectedSchedule === schedule.id ? 'bg-gray-100' : 'hover:bg-gray-50'
|
||||
selectedSchedule === schedule.id
|
||||
? 'bg-gray-100'
|
||||
: 'hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
{editingId === schedule.id ? (
|
||||
@ -110,6 +173,23 @@ export default function ScheduleNavigation() {
|
||||
className="w-8 h-8"
|
||||
/>
|
||||
</div>
|
||||
{planningMode === PlanningModes.CLASS_SCHEDULE && (
|
||||
<div className="mb-2">
|
||||
<label className="text-sm">Classe:</label>
|
||||
<select
|
||||
value={editedSchoolClass}
|
||||
onChange={(e) => setEditedSchoolClass(e.target.value)}
|
||||
className="w-full p-1 border rounded"
|
||||
>
|
||||
<option value="">Aucune</option>
|
||||
{classes.map((classe) => (
|
||||
<option key={classe.id} value={classe.id}>
|
||||
{classe.atmosphere_name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-end gap-2">
|
||||
<button
|
||||
onClick={() => setEditingId(null)}
|
||||
@ -135,7 +215,13 @@ export default function ScheduleNavigation() {
|
||||
className="w-3 h-3 rounded-full"
|
||||
style={{ backgroundColor: schedule.color }}
|
||||
/>
|
||||
<span className={hiddenSchedules.includes(schedule.id) ? 'text-gray-400' : ''}>
|
||||
<span
|
||||
className={
|
||||
hiddenSchedules.includes(schedule.id)
|
||||
? 'text-gray-400'
|
||||
: ''
|
||||
}
|
||||
>
|
||||
{schedule.name}
|
||||
</span>
|
||||
</div>
|
||||
@ -167,4 +253,4 @@ export default function ScheduleNavigation() {
|
||||
</ul>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,12 +1,13 @@
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import { usePlanning } from '@/context/PlanningContext';
|
||||
import { format, startOfWeek, addDays, differenceInMinutes, isSameDay } from 'date-fns';
|
||||
import { usePlanning, PlanningModes } from '@/context/PlanningContext';
|
||||
import { format, startOfWeek, addDays, isSameDay } from 'date-fns';
|
||||
import { fr } from 'date-fns/locale';
|
||||
import { getWeekEvents } from '@/utils/events';
|
||||
import { isToday } from 'date-fns';
|
||||
|
||||
|
||||
const WeekView = ({ onDateClick, onEventClick, events }) => {
|
||||
const { currentDate } = usePlanning();
|
||||
const { currentDate, planningMode, parentView } = usePlanning();
|
||||
const [currentTime, setCurrentTime] = useState(new Date());
|
||||
const scrollContainerRef = useRef(null); // Ajouter cette référence
|
||||
|
||||
@ -16,7 +17,7 @@ const WeekView = ({ onDateClick, onEventClick, events }) => {
|
||||
const weekDays = Array.from({ length: 7 }, (_, i) => addDays(weekStart, i));
|
||||
|
||||
// Maintenant on peut utiliser weekDays
|
||||
const isCurrentWeek = weekDays.some(day => isSameDay(day, new Date()));
|
||||
const isCurrentWeek = weekDays.some((day) => isSameDay(day, new Date()));
|
||||
|
||||
// Mettre à jour la position de la ligne toutes les minutes
|
||||
useEffect(() => {
|
||||
@ -43,7 +44,8 @@ const WeekView = ({ onDateClick, onEventClick, events }) => {
|
||||
const getCurrentTimePosition = () => {
|
||||
const hours = currentTime.getHours();
|
||||
const minutes = currentTime.getMinutes();
|
||||
return `${(hours + minutes / 60) * 5}rem`;
|
||||
const rowHeight = 5; // Hauteur des lignes en rem (h-20 = 5rem)
|
||||
return `${(hours + minutes / 60) * rowHeight}rem`;
|
||||
};
|
||||
|
||||
// Utiliser les événements déjà filtrés passés en props
|
||||
@ -61,7 +63,7 @@ const WeekView = ({ onDateClick, onEventClick, events }) => {
|
||||
const eventStart = new Date(event.start);
|
||||
const eventEnd = new Date(event.end);
|
||||
|
||||
return dayEvents.filter(otherEvent => {
|
||||
return dayEvents.filter((otherEvent) => {
|
||||
if (otherEvent.id === event.id) return false;
|
||||
const otherStart = new Date(otherEvent.start);
|
||||
const otherEnd = new Date(otherEvent.end);
|
||||
@ -77,7 +79,7 @@ const WeekView = ({ onDateClick, onEventClick, events }) => {
|
||||
|
||||
// Trouver les événements qui se chevauchent
|
||||
const overlappingEvents = findOverlappingEvents(event, dayEvents);
|
||||
const eventIndex = overlappingEvents.findIndex(e => e.id > event.id) + 1;
|
||||
const eventIndex = overlappingEvents.findIndex((e) => e.id > event.id) + 1;
|
||||
const totalOverlapping = overlappingEvents.length + 1;
|
||||
|
||||
// Calculer la largeur et la position horizontale
|
||||
@ -93,7 +95,7 @@ const WeekView = ({ onDateClick, onEventClick, events }) => {
|
||||
borderLeft: `3px solid ${event.color}`,
|
||||
borderRadius: '0.25rem',
|
||||
zIndex: 1,
|
||||
transform: `translateY(${startMinutes}rem)`
|
||||
transform: `translateY(${startMinutes}rem)`,
|
||||
};
|
||||
};
|
||||
|
||||
@ -105,20 +107,34 @@ const WeekView = ({ onDateClick, onEventClick, events }) => {
|
||||
key={event.id}
|
||||
className="rounded-sm overflow-hidden cursor-pointer hover:shadow-lg"
|
||||
style={eventStyle}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onEventClick(event);
|
||||
}}
|
||||
onClick={
|
||||
parentView
|
||||
? undefined
|
||||
: (e) => {
|
||||
e.stopPropagation();
|
||||
onEventClick(event);
|
||||
}
|
||||
}
|
||||
>
|
||||
<div className="p-1">
|
||||
<div className="font-semibold text-xs truncate" style={{ color: event.color }}>
|
||||
<div
|
||||
className="font-semibold text-xs truncate"
|
||||
style={{ color: event.color }}
|
||||
>
|
||||
{event.title}
|
||||
</div>
|
||||
<div className="text-xs" style={{ color: event.color, opacity: 0.75 }}>
|
||||
{format(new Date(event.start), 'HH:mm')} - {format(new Date(event.end), 'HH:mm')}
|
||||
<div
|
||||
className="text-xs"
|
||||
style={{ color: event.color, opacity: 0.75 }}
|
||||
>
|
||||
{format(new Date(event.start), 'HH:mm')} -{' '}
|
||||
{format(new Date(event.end), 'HH:mm')}
|
||||
</div>
|
||||
{event.location && (
|
||||
<div className="text-xs truncate" style={{ color: event.color, opacity: 0.75 }}>
|
||||
<div
|
||||
className="text-xs truncate"
|
||||
style={{ color: event.color, opacity: 0.75 }}
|
||||
>
|
||||
{event.location}
|
||||
</div>
|
||||
)}
|
||||
@ -128,22 +144,27 @@ const WeekView = ({ onDateClick, onEventClick, events }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full overflow-hidden">
|
||||
<div className="flex flex-col h-full overflow-y-auto">
|
||||
{/* En-tête des jours */}
|
||||
<div className="grid gap-[1px] bg-gray-100 pr-[17px]" style={{ gridTemplateColumns: "2.5rem repeat(7, 1fr)" }}>
|
||||
<div
|
||||
className="grid gap-[1px] w-full bg-gray-100"
|
||||
style={{ gridTemplateColumns: '2.5rem repeat(7, 1fr)' }}
|
||||
>
|
||||
<div className="bg-white h-14"></div>
|
||||
{weekDays.map((day) => (
|
||||
<div
|
||||
key={day}
|
||||
className={`p-2 text-center border-b
|
||||
className={`h-14 p-2 text-center border-b
|
||||
${isWeekend(day) ? 'bg-gray-50' : 'bg-white'}
|
||||
${isToday(day) ? 'bg-emerald-100 border-x border-emerald-600' : ''}`}
|
||||
>
|
||||
<div className="text-xs font-medium text-gray-500">
|
||||
{format(day, 'EEEE', { locale: fr })}
|
||||
</div>
|
||||
<div className={`text-sm font-semibold inline-block rounded-full w-7 h-7 leading-7
|
||||
${isToday(day) ? 'bg-emerald-500 text-white' : ''}`}>
|
||||
<div
|
||||
className={`text-sm font-semibold inline-block rounded-full w-7 h-7 leading-7
|
||||
${isToday(day) ? 'bg-emerald-500 text-white' : ''}`}
|
||||
>
|
||||
{format(day, 'd', { locale: fr })}
|
||||
</div>
|
||||
</div>
|
||||
@ -151,24 +172,24 @@ const WeekView = ({ onDateClick, onEventClick, events }) => {
|
||||
</div>
|
||||
|
||||
{/* Grille horaire */}
|
||||
<div ref={scrollContainerRef} className="flex-1 overflow-y-auto relative">
|
||||
<div ref={scrollContainerRef} className="flex-1 relative">
|
||||
{/* Ligne de temps actuelle */}
|
||||
{isCurrentWeek && (
|
||||
<div
|
||||
className="absolute left-0 right-0 z-10 border-emerald-500 border pointer-events-none"
|
||||
style={{
|
||||
top: getCurrentTimePosition(),
|
||||
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="absolute -left-2 -top-1 w-2 h-2 rounded-full bg-emerald-500"
|
||||
/>
|
||||
<div className="absolute -left-2 -top-2 w-2 h-2 rounded-full bg-emerald-500" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid gap-[1px] bg-gray-100" style={{ gridTemplateColumns: "2.5rem repeat(7, 1fr)" }}>
|
||||
{timeSlots.map(hour => (
|
||||
<div
|
||||
className="grid gap-[1px] w-full bg-gray-100"
|
||||
style={{ gridTemplateColumns: '2.5rem repeat(7, 1fr)' }}
|
||||
>
|
||||
{timeSlots.map((hour) => (
|
||||
<React.Fragment key={hour}>
|
||||
<div className="h-20 p-1 text-right text-sm text-gray-500 bg-gray-100 font-medium">
|
||||
{`${hour.toString().padStart(2, '0')}:00`}
|
||||
@ -182,17 +203,23 @@ const WeekView = ({ onDateClick, onEventClick, events }) => {
|
||||
className={`h-20 relative border-b border-gray-100
|
||||
${isWeekend(day) ? 'bg-gray-50' : 'bg-white'}
|
||||
${isToday(day) ? 'bg-emerald-100/50 border-x border-emerald-600' : ''}`}
|
||||
onClick={() => {
|
||||
const date = new Date(day);
|
||||
date.setHours(hour);
|
||||
onDateClick(date);
|
||||
}}
|
||||
onClick={
|
||||
parentView
|
||||
? undefined
|
||||
: () => {
|
||||
const date = new Date(day);
|
||||
date.setHours(hour);
|
||||
onDateClick(date);
|
||||
}
|
||||
}
|
||||
>
|
||||
<div className="flex gap-1"> {/* Ajout de gap-1 */}
|
||||
{dayEvents.filter(event => {
|
||||
const eventStart = new Date(event.start);
|
||||
return eventStart.getHours() === hour;
|
||||
}).map(event => renderEventInCell(event, dayEvents))}
|
||||
<div className="grid gap-1">
|
||||
{dayEvents
|
||||
.filter((event) => {
|
||||
const eventStart = new Date(event.start);
|
||||
return eventStart.getHours() === hour;
|
||||
})
|
||||
.map((event) => renderEventInCell(event, dayEvents))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@ -205,4 +232,4 @@ const WeekView = ({ onDateClick, onEventClick, events }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default WeekView;
|
||||
export default WeekView;
|
||||
|
||||
@ -37,7 +37,7 @@ const YearView = ({ onDateClick }) => {
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-4 gap-4 p-4">
|
||||
{months.map(month => (
|
||||
{months.map((month) => (
|
||||
<MonthCard
|
||||
key={month.getTime()}
|
||||
month={month}
|
||||
@ -49,4 +49,4 @@ const YearView = ({ onDateClick }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default YearView;
|
||||
export default YearView;
|
||||
|
||||
45
Front-End/src/components/Charts/LineChart.js
Normal file
45
Front-End/src/components/Charts/LineChart.js
Normal file
@ -0,0 +1,45 @@
|
||||
import React from 'react';
|
||||
|
||||
export default function LineChart({ data }) {
|
||||
if (!data || data.length === 0) {
|
||||
return <div className="text-gray-400 text-center">Aucune donnée</div>;
|
||||
}
|
||||
|
||||
// Hauteur max du graphique en pixels
|
||||
const chartHeight = 120;
|
||||
const maxValue = Math.max(...data.map((d) => d.value), 1);
|
||||
|
||||
// Trouver les indices des barres ayant la valeur max (pour gérer les égalités)
|
||||
const maxIndices = data
|
||||
.map((d, idx) => (d.value === maxValue ? idx : -1))
|
||||
.filter((idx) => idx !== -1);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="w-full flex items-end space-x-4"
|
||||
style={{ height: chartHeight }}
|
||||
>
|
||||
{data.map((point, idx) => {
|
||||
const barHeight = Math.max((point.value / maxValue) * chartHeight, 8); // min 8px
|
||||
const isMax = maxIndices.includes(idx);
|
||||
return (
|
||||
<div key={idx} className="flex flex-col items-center flex-1">
|
||||
{/* Valeur au-dessus de la barre */}
|
||||
<span className="text-xs mb-1 text-gray-700 font-semibold">
|
||||
{point.value}
|
||||
</span>
|
||||
<div
|
||||
className={`${isMax ? 'bg-emerald-400' : 'bg-blue-400'} rounded-t w-4`}
|
||||
style={{
|
||||
height: `${barHeight}px`,
|
||||
transition: 'height 0.3s',
|
||||
}}
|
||||
title={`${point.month}: ${point.value}`}
|
||||
/>
|
||||
<span className="text-xs mt-1 text-gray-600">{point.month}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
85
Front-End/src/components/Charts/PieChart.js
Normal file
85
Front-End/src/components/Charts/PieChart.js
Normal file
@ -0,0 +1,85 @@
|
||||
import React from 'react';
|
||||
|
||||
// Utilisation de couleurs hexadécimales pour le SVG
|
||||
const COLORS = [
|
||||
'#60a5fa', // bleu-400
|
||||
'#fb923c', // orange-400
|
||||
'#a78bfa', // violet-400
|
||||
'#34d399', // émeraude-400
|
||||
];
|
||||
|
||||
export default function PieChart({ data }) {
|
||||
if (!data || data.length === 0) {
|
||||
return <div className="text-gray-400 text-center">Aucune donnée</div>;
|
||||
}
|
||||
|
||||
const total = data.reduce((acc, d) => acc + d.value, 0);
|
||||
if (total === 0) {
|
||||
return <div className="text-gray-400 text-center">Aucune donnée</div>;
|
||||
}
|
||||
|
||||
let cumulative = 0;
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-center w-full">
|
||||
<svg width={100} height={100} viewBox="0 0 32 32">
|
||||
{data.map((slice, idx) => {
|
||||
if (slice.value === 0) return null;
|
||||
const value = (slice.value / total) * 100;
|
||||
if (value === 100) {
|
||||
// Cas 100% : on dessine un cercle plein
|
||||
return (
|
||||
<circle
|
||||
key={idx}
|
||||
cx="16"
|
||||
cy="16"
|
||||
r="16"
|
||||
fill={COLORS[idx % COLORS.length]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
const startAngle = (cumulative / 100) * 360;
|
||||
const endAngle = ((cumulative + value) / 100) * 360;
|
||||
const largeArc = value > 50 ? 1 : 0;
|
||||
const x1 = 16 + 16 * Math.cos((Math.PI * (startAngle - 90)) / 180);
|
||||
const y1 = 16 + 16 * Math.sin((Math.PI * (startAngle - 90)) / 180);
|
||||
const x2 = 16 + 16 * Math.cos((Math.PI * (endAngle - 90)) / 180);
|
||||
const y2 = 16 + 16 * Math.sin((Math.PI * (endAngle - 90)) / 180);
|
||||
const pathData = `
|
||||
M16,16
|
||||
L${x1},${y1}
|
||||
A16,16 0 ${largeArc} 1 ${x2},${y2}
|
||||
Z
|
||||
`;
|
||||
cumulative += value;
|
||||
return (
|
||||
<path
|
||||
key={idx}
|
||||
d={pathData}
|
||||
fill={COLORS[idx % COLORS.length]}
|
||||
stroke="#fff"
|
||||
strokeWidth="0.5"
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</svg>
|
||||
<div className="ml-4 flex flex-col space-y-1">
|
||||
{data.map((slice, idx) =>
|
||||
slice.value > 0 && (
|
||||
<div
|
||||
key={idx}
|
||||
className="flex items-center text-xs font-semibold"
|
||||
style={{ color: COLORS[idx % COLORS.length] }}
|
||||
>
|
||||
<span
|
||||
className="inline-block w-3 h-3 mr-2 rounded"
|
||||
style={{ backgroundColor: COLORS[idx % COLORS.length] }}
|
||||
/>
|
||||
{slice.label} : {slice.value}
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
79
Front-End/src/components/Chat/ConnectionStatus.js
Normal file
79
Front-End/src/components/Chat/ConnectionStatus.js
Normal file
@ -0,0 +1,79 @@
|
||||
import React from 'react';
|
||||
import { Wifi, WifiOff, RotateCcw } from 'lucide-react';
|
||||
|
||||
const ConnectionStatus = ({ status, onReconnect }) => {
|
||||
const getStatusInfo = () => {
|
||||
switch (status) {
|
||||
case 'connected':
|
||||
return {
|
||||
icon: <Wifi className="w-4 h-4" />,
|
||||
text: 'Connecté',
|
||||
className: 'text-green-600 bg-green-50 border-green-200',
|
||||
};
|
||||
case 'disconnected':
|
||||
return {
|
||||
icon: <WifiOff className="w-4 h-4" />,
|
||||
text: 'Déconnecté',
|
||||
className: 'text-red-600 bg-red-50 border-red-200',
|
||||
};
|
||||
case 'reconnecting':
|
||||
return {
|
||||
icon: <RotateCcw className="w-4 h-4 animate-spin" />,
|
||||
text: 'Reconnexion...',
|
||||
className: 'text-yellow-600 bg-yellow-50 border-yellow-200',
|
||||
};
|
||||
case 'error':
|
||||
return {
|
||||
icon: <WifiOff className="w-4 h-4" />,
|
||||
text: 'Erreur de connexion',
|
||||
className: 'text-red-600 bg-red-50 border-red-200',
|
||||
};
|
||||
case 'failed':
|
||||
return {
|
||||
icon: <WifiOff className="w-4 h-4" />,
|
||||
text: 'Connexion échouée',
|
||||
className: 'text-red-600 bg-red-50 border-red-200',
|
||||
};
|
||||
default:
|
||||
return {
|
||||
icon: <WifiOff className="w-4 h-4" />,
|
||||
text: 'Inconnu',
|
||||
className: 'text-gray-600 bg-gray-50 border-gray-200',
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
if (status === 'connected') {
|
||||
return (
|
||||
<div className="flex items-center justify-between px-3 py-2 border rounded-lg text-green-600 bg-green-50 border-green-200">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Wifi className="w-4 h-4" />
|
||||
<span className="text-sm font-medium">Connecté</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const { icon, text, className } = getStatusInfo();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex items-center justify-between px-3 py-2 border rounded-lg ${className}`}
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
{icon}
|
||||
<span className="text-sm font-medium">{text}</span>
|
||||
</div>
|
||||
{(status === 'failed' || status === 'error') && onReconnect && (
|
||||
<button
|
||||
onClick={onReconnect}
|
||||
className="text-sm underline hover:no-underline"
|
||||
>
|
||||
Réessayer
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConnectionStatus;
|
||||
196
Front-End/src/components/Chat/ConversationItem.js
Normal file
196
Front-End/src/components/Chat/ConversationItem.js
Normal file
@ -0,0 +1,196 @@
|
||||
import React from 'react';
|
||||
import { User, Trash2 } from 'lucide-react';
|
||||
import { getGravatarUrl } from '@/utils/gravatar';
|
||||
|
||||
const ConversationItem = ({
|
||||
conversation,
|
||||
isSelected,
|
||||
onClick,
|
||||
onDelete, // Nouvelle prop pour la suppression
|
||||
unreadCount = 0,
|
||||
lastMessage,
|
||||
isTyping = false,
|
||||
userPresences = {}, // Nouveau prop pour les statuts de présence
|
||||
}) => {
|
||||
const formatTime = (dateString) => {
|
||||
if (!dateString) return '';
|
||||
const date = new Date(dateString);
|
||||
const now = new Date();
|
||||
const diffInHours = (now - date) / (1000 * 60 * 60);
|
||||
|
||||
if (diffInHours < 24) {
|
||||
return date.toLocaleTimeString('fr-FR', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
} else {
|
||||
return date.toLocaleDateString('fr-FR', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const getInterlocutorName = () => {
|
||||
if (conversation.interlocuteur) {
|
||||
// Si nous avons le nom et prénom, les utiliser
|
||||
if (
|
||||
conversation.interlocuteur.first_name &&
|
||||
conversation.interlocuteur.last_name
|
||||
) {
|
||||
return `${conversation.interlocuteur.first_name} ${conversation.interlocuteur.last_name}`;
|
||||
}
|
||||
// Sinon, utiliser l'email comme fallback
|
||||
if (conversation.interlocuteur.email) {
|
||||
return conversation.interlocuteur.email;
|
||||
}
|
||||
}
|
||||
return conversation.name || 'Utilisateur inconnu';
|
||||
};
|
||||
|
||||
const getLastMessageText = () => {
|
||||
if (isTyping) {
|
||||
return (
|
||||
<span className="text-emerald-500 italic">Tape un message...</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (lastMessage) {
|
||||
return lastMessage.content || lastMessage.corpus || 'Message...';
|
||||
}
|
||||
|
||||
if (conversation.last_message) {
|
||||
return (
|
||||
conversation.last_message.content ||
|
||||
conversation.last_message.corpus ||
|
||||
'Message...'
|
||||
);
|
||||
}
|
||||
|
||||
return 'Aucun message';
|
||||
};
|
||||
|
||||
const getLastMessageTime = () => {
|
||||
if (lastMessage) {
|
||||
return formatTime(lastMessage.created_at || lastMessage.date_envoi);
|
||||
}
|
||||
|
||||
if (conversation.last_message) {
|
||||
return formatTime(
|
||||
conversation.last_message.created_at ||
|
||||
conversation.last_message.date_envoi
|
||||
);
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
const getUserPresenceStatus = () => {
|
||||
if (conversation.interlocuteur?.id) {
|
||||
const presence = userPresences[conversation.interlocuteur.id];
|
||||
return presence?.status || 'offline';
|
||||
}
|
||||
return 'offline';
|
||||
};
|
||||
|
||||
const getPresenceColor = (status) => {
|
||||
switch (status) {
|
||||
case 'online':
|
||||
return 'bg-emerald-400';
|
||||
case 'away':
|
||||
return 'bg-yellow-400';
|
||||
case 'busy':
|
||||
return 'bg-red-400';
|
||||
case 'offline':
|
||||
default:
|
||||
return 'bg-gray-400';
|
||||
}
|
||||
};
|
||||
|
||||
const getPresenceLabel = (status) => {
|
||||
switch (status) {
|
||||
case 'online':
|
||||
return 'En ligne';
|
||||
case 'away':
|
||||
return 'Absent';
|
||||
case 'busy':
|
||||
return 'Occupé';
|
||||
case 'offline':
|
||||
default:
|
||||
return 'Hors ligne';
|
||||
}
|
||||
};
|
||||
|
||||
const presenceStatus = getUserPresenceStatus();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`group flex items-center p-3 cursor-pointer rounded-lg transition-all duration-200 hover:bg-gray-50 ${
|
||||
isSelected
|
||||
? 'bg-emerald-50 border-l-4 border-emerald-500'
|
||||
: 'hover:bg-gray-50'
|
||||
}`}
|
||||
onClick={onClick}
|
||||
>
|
||||
{/* Avatar */}
|
||||
<div className="relative">
|
||||
<img
|
||||
src={getGravatarUrl(
|
||||
conversation.interlocuteur?.email || 'default',
|
||||
48
|
||||
)}
|
||||
alt={`Avatar de ${getInterlocutorName()}`}
|
||||
className="w-12 h-12 rounded-full object-cover shadow-md"
|
||||
/>
|
||||
|
||||
{/* Indicateur de statut en ligne */}
|
||||
<div
|
||||
className={`absolute -bottom-0.5 -right-0.5 w-4 h-4 ${getPresenceColor(presenceStatus)} border-2 border-white rounded-full`}
|
||||
title={getPresenceLabel(presenceStatus)}
|
||||
></div>
|
||||
</div>
|
||||
|
||||
{/* Contenu de la conversation */}
|
||||
<div className="flex-1 ml-3 overflow-hidden">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3
|
||||
className={`font-semibold truncate ${
|
||||
isSelected ? 'text-emerald-700' : 'text-gray-900'
|
||||
}`}
|
||||
>
|
||||
{getInterlocutorName()}
|
||||
</h3>
|
||||
<div className="flex items-center space-x-2">
|
||||
{unreadCount > 0 && (
|
||||
<span className="bg-red-500 text-white text-xs rounded-full w-4 h-4 text-center"></span>
|
||||
)}
|
||||
<span className="text-xs text-gray-500">
|
||||
{getLastMessageTime()}
|
||||
</span>
|
||||
{/* Bouton de suppression */}
|
||||
{onDelete && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation(); // Empêcher la sélection de la conversation
|
||||
onDelete();
|
||||
}}
|
||||
className="opacity-0 group-hover:opacity-100 hover:bg-red-100 p-1 rounded transition-all duration-200"
|
||||
title="Supprimer la conversation"
|
||||
>
|
||||
<Trash2 className="w-4 h-4 text-red-500 hover:text-red-700" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p
|
||||
className={`text-sm truncate mt-1 ${isTyping ? '' : 'text-gray-600'}`}
|
||||
>
|
||||
{getLastMessageText()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConversationItem;
|
||||
115
Front-End/src/components/Chat/FileAttachment.js
Normal file
115
Front-End/src/components/Chat/FileAttachment.js
Normal file
@ -0,0 +1,115 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Download,
|
||||
FileText,
|
||||
Image,
|
||||
Film,
|
||||
Music,
|
||||
Archive,
|
||||
AlertCircle,
|
||||
} from 'lucide-react';
|
||||
|
||||
const FileAttachment = ({
|
||||
fileName,
|
||||
fileSize,
|
||||
fileType,
|
||||
fileUrl,
|
||||
onDownload = null,
|
||||
}) => {
|
||||
// Obtenir l'icône en fonction du type de fichier
|
||||
const getFileIcon = (type) => {
|
||||
if (type.startsWith('image/')) {
|
||||
return <Image className="w-6 h-6 text-blue-500" />;
|
||||
}
|
||||
if (type.startsWith('video/')) {
|
||||
return <Film className="w-6 h-6 text-purple-500" />;
|
||||
}
|
||||
if (type.startsWith('audio/')) {
|
||||
return <Music className="w-6 h-6 text-green-500" />;
|
||||
}
|
||||
if (type.includes('pdf')) {
|
||||
return <FileText className="w-6 h-6 text-red-500" />;
|
||||
}
|
||||
if (type.includes('zip') || type.includes('rar')) {
|
||||
return <Archive className="w-6 h-6 text-yellow-500" />;
|
||||
}
|
||||
return <FileText className="w-6 h-6 text-gray-500" />;
|
||||
};
|
||||
|
||||
// Formater la taille du fichier
|
||||
const formatFileSize = (bytes) => {
|
||||
if (!bytes) return '';
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
};
|
||||
|
||||
// Gérer le téléchargement
|
||||
const handleDownload = () => {
|
||||
if (onDownload) {
|
||||
onDownload();
|
||||
} else if (fileUrl) {
|
||||
const link = document.createElement('a');
|
||||
link.href = fileUrl;
|
||||
link.download = fileName;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
}
|
||||
};
|
||||
|
||||
// Vérifier si c'est une image pour afficher un aperçu
|
||||
const isImage = fileType && fileType.startsWith('image/');
|
||||
|
||||
return (
|
||||
<div className="max-w-sm">
|
||||
{isImage && fileUrl ? (
|
||||
// Affichage pour les images
|
||||
<div className="relative group">
|
||||
<img
|
||||
src={fileUrl}
|
||||
alt={fileName}
|
||||
className="max-w-full h-auto rounded-lg cursor-pointer hover:opacity-90 transition-opacity"
|
||||
onClick={() => window.open(fileUrl, '_blank')}
|
||||
/>
|
||||
<div className="absolute inset-0 bg-black bg-opacity-0 group-hover:bg-opacity-30 transition-all rounded-lg flex items-center justify-center">
|
||||
<button
|
||||
onClick={handleDownload}
|
||||
className="opacity-0 group-hover:opacity-100 bg-white bg-opacity-90 hover:bg-opacity-100 rounded-full p-2 transition-all"
|
||||
>
|
||||
<Download className="w-4 h-4 text-gray-700" />
|
||||
</button>
|
||||
</div>
|
||||
{fileName && (
|
||||
<p className="mt-1 text-xs text-gray-500 truncate">{fileName}</p>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
// Affichage pour les autres fichiers
|
||||
<div className="flex items-center space-x-3 p-3 bg-gray-50 rounded-lg border hover:bg-gray-100 transition-colors">
|
||||
<div className="flex-shrink-0">{getFileIcon(fileType)}</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-gray-900 truncate">
|
||||
{fileName || 'Fichier sans nom'}
|
||||
</p>
|
||||
{fileSize && (
|
||||
<p className="text-xs text-gray-500">
|
||||
{formatFileSize(fileSize)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
onClick={handleDownload}
|
||||
className="flex-shrink-0 p-1 text-gray-400 hover:text-gray-600 transition-colors"
|
||||
title="Télécharger"
|
||||
>
|
||||
<Download className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FileAttachment;
|
||||
179
Front-End/src/components/Chat/FileUpload.js
Normal file
179
Front-End/src/components/Chat/FileUpload.js
Normal file
@ -0,0 +1,179 @@
|
||||
import React, { useState } from 'react';
|
||||
import { X, Upload, FileText, Image, AlertCircle } from 'lucide-react';
|
||||
|
||||
const FileUpload = ({
|
||||
file,
|
||||
onUpload,
|
||||
onCancel,
|
||||
conversationId,
|
||||
senderId,
|
||||
maxSize = 10 * 1024 * 1024, // 10MB par défaut
|
||||
}) => {
|
||||
const [uploadProgress, setUploadProgress] = useState(0);
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
// Vérifier le type de fichier et obtenir l'icône appropriée
|
||||
const getFileIcon = (fileType) => {
|
||||
if (fileType.startsWith('image/')) {
|
||||
return <Image className="w-8 h-8 text-blue-500" alt="Icône image" />;
|
||||
}
|
||||
return <FileText className="w-8 h-8 text-gray-500" alt="Icône fichier" />;
|
||||
};
|
||||
|
||||
// Formater la taille du fichier
|
||||
const formatFileSize = (bytes) => {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
};
|
||||
|
||||
// Vérifier si le fichier est valide
|
||||
const isValidFile = () => {
|
||||
const allowedTypes = [
|
||||
'image/jpeg',
|
||||
'image/jpg',
|
||||
'image/png',
|
||||
'image/gif',
|
||||
'image/webp',
|
||||
'application/pdf',
|
||||
'application/msword',
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
'application/vnd.ms-excel',
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
'text/plain',
|
||||
];
|
||||
|
||||
if (!allowedTypes.includes(file.type)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (file.size > maxSize) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
// Gérer l'upload
|
||||
const handleUpload = async () => {
|
||||
if (!isValidFile()) {
|
||||
setError('Type de fichier non autorisé ou fichier trop volumineux');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsUploading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const result = await onUpload(
|
||||
file,
|
||||
conversationId,
|
||||
senderId,
|
||||
setUploadProgress
|
||||
);
|
||||
// L'upload s'est bien passé, le parent gère la suite
|
||||
} catch (error) {
|
||||
setError(error.message || "Erreur lors de l'upload");
|
||||
setIsUploading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!file) return null;
|
||||
|
||||
return (
|
||||
<div className="bg-white border border-gray-200 rounded-lg p-4 shadow-lg">
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="flex items-center space-x-3">
|
||||
{getFileIcon(file.type)}
|
||||
<div>
|
||||
<p className="font-medium text-gray-900 truncate max-w-xs">
|
||||
{file.name}
|
||||
</p>
|
||||
<p className="text-sm text-gray-500">{formatFileSize(file.size)}</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={onCancel}
|
||||
disabled={isUploading}
|
||||
className="text-gray-400 hover:text-gray-600 disabled:opacity-50"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Prévisualisation pour les images */}
|
||||
{file.type.startsWith('image/') && (
|
||||
<div className="mb-3">
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={URL.createObjectURL(file)}
|
||||
alt="Aperçu du fichier sélectionné"
|
||||
className="max-w-full h-32 object-cover rounded-lg"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Validation du fichier */}
|
||||
{!isValidFile() && (
|
||||
<div className="mb-3 p-2 bg-red-50 border border-red-200 rounded-lg flex items-center space-x-2">
|
||||
<AlertCircle className="w-4 h-4 text-red-500" />
|
||||
<span className="text-sm text-red-700">
|
||||
{file.size > maxSize
|
||||
? `Fichier trop volumineux (max ${formatFileSize(maxSize)})`
|
||||
: 'Type de fichier non autorisé'}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Erreur d'upload */}
|
||||
{error && (
|
||||
<div className="mb-3 p-2 bg-red-50 border border-red-200 rounded-lg flex items-center space-x-2">
|
||||
<AlertCircle className="w-4 h-4 text-red-500" />
|
||||
<span className="text-sm text-red-700">{error}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Barre de progression */}
|
||||
{isUploading && (
|
||||
<div className="mb-3">
|
||||
<div className="flex justify-between items-center mb-1">
|
||||
<span className="text-sm text-gray-600">Upload en cours...</span>
|
||||
<span className="text-sm text-gray-600">
|
||||
{Math.round(uploadProgress)}%
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-full bg-gray-200 rounded-full h-2">
|
||||
<div
|
||||
className="bg-blue-500 h-2 rounded-full transition-all duration-300"
|
||||
style={{ width: `${uploadProgress}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Boutons d'action */}
|
||||
<div className="flex justify-end space-x-2">
|
||||
<button
|
||||
onClick={onCancel}
|
||||
disabled={isUploading}
|
||||
className="px-3 py-2 text-sm text-gray-600 hover:text-gray-800 disabled:opacity-50"
|
||||
>
|
||||
Annuler
|
||||
</button>
|
||||
<button
|
||||
onClick={handleUpload}
|
||||
disabled={isUploading || !isValidFile()}
|
||||
className="flex items-center space-x-2 px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<Upload className="w-4 h-4" />
|
||||
<span>{isUploading ? 'Upload...' : 'Envoyer'}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FileUpload;
|
||||
1162
Front-End/src/components/Chat/InstantChat.js
Normal file
1162
Front-End/src/components/Chat/InstantChat.js
Normal file
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user