V1_commit_RGC
This commit is contained in:
87
SuiviREForamteur/suivireforamteur/src/App.tsx
Normal file
87
SuiviREForamteur/suivireforamteur/src/App.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
// App.tsx
|
||||
import React, { useState } from 'react';
|
||||
import { MsalProvider } from '@azure/msal-react';
|
||||
import { msalInstance } from './msalConfig';
|
||||
import { AuthProvider, useAuth } from './AuthContext';
|
||||
import type { EvaluationRecord } from './types';
|
||||
import LoginPage from './LoginPage';
|
||||
import HistoriquePage from './HistoriquePage';
|
||||
import SaisiePage from './SaisiePage';
|
||||
import './styles.css';
|
||||
|
||||
const initialRecords: EvaluationRecord[] = [
|
||||
{
|
||||
id: 1,
|
||||
nom: 'Dupont',
|
||||
prenom: 'Marie',
|
||||
formation: 'BTS NDRC 1 JV',
|
||||
autoEvaluation: 'OUI',
|
||||
dateAction: '25/01/2026',
|
||||
commentaireFormateur: 'Relance HP',
|
||||
evaluationTuteur: 'OUI',
|
||||
dateActionTuteur: '25/01/2026',
|
||||
commentaireRRE: 'Relance Tuteur',
|
||||
campus: 'CGY'
|
||||
}
|
||||
];
|
||||
|
||||
const AppContent: React.FC = () => {
|
||||
const { user, login, logout, isAuthenticated } = useAuth();
|
||||
const [currentPage, setCurrentPage] = useState('historique');
|
||||
const [records, setRecords] = useState<EvaluationRecord[]>(initialRecords);
|
||||
|
||||
const handleNewEntry = () => {
|
||||
setCurrentPage('saisie');
|
||||
};
|
||||
|
||||
const handleSave = (newRecord: EvaluationRecord) => {
|
||||
setRecords(prev => [...prev, newRecord]);
|
||||
setCurrentPage('historique');
|
||||
};
|
||||
|
||||
const handleUpdateRecord = (updatedRecord: EvaluationRecord) => {
|
||||
setRecords(prev =>
|
||||
prev.map(record =>
|
||||
record.id === updatedRecord.id ? updatedRecord : record
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setCurrentPage('historique');
|
||||
};
|
||||
|
||||
// Si pas authentifié, afficher la page de login
|
||||
if (!isAuthenticated || !user) {
|
||||
return <LoginPage onLogin={login} />;
|
||||
}
|
||||
|
||||
// Si sur la page de saisie
|
||||
if (currentPage === 'saisie') {
|
||||
return <SaisiePage onSave={handleSave} onCancel={handleCancel} user={user} />;
|
||||
}
|
||||
|
||||
// Page historique par défaut
|
||||
return (
|
||||
<HistoriquePage
|
||||
records={records}
|
||||
onNewEntry={handleNewEntry}
|
||||
onLogout={logout}
|
||||
onUpdateRecord={handleUpdateRecord}
|
||||
user={user}
|
||||
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const App: React.FC = () => {
|
||||
return (
|
||||
<MsalProvider instance={msalInstance}>
|
||||
<AuthProvider>
|
||||
<AppContent />
|
||||
</AuthProvider>
|
||||
</MsalProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
89
SuiviREForamteur/suivireforamteur/src/AuthContext.tsx
Normal file
89
SuiviREForamteur/suivireforamteur/src/AuthContext.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
// AuthContext.tsx - Context pour gérer l'authentification
|
||||
import React, { createContext, useContext, useState, useEffect } from 'react';
|
||||
import type { User } from './types';
|
||||
|
||||
interface AuthContextType {
|
||||
user: User | null;
|
||||
login: (user: User) => void;
|
||||
logout: () => void;
|
||||
isAuthenticated: boolean;
|
||||
isRRE: boolean;
|
||||
isFormateur: boolean;
|
||||
hasPermission: (permission: keyof User['permissions']) => boolean;
|
||||
}
|
||||
|
||||
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
||||
|
||||
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
// ✅ Initialisation lazy pour éviter les problèmes HMR
|
||||
const [user, setUser] = useState<User | null>(() => {
|
||||
if (typeof window === 'undefined') return null;
|
||||
|
||||
try {
|
||||
const storedUser = localStorage.getItem('user');
|
||||
return storedUser ? JSON.parse(storedUser) : null;
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la lecture du localStorage:', error);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
// ✅ Synchronisation avec localStorage quand user change
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
try {
|
||||
localStorage.setItem('user', JSON.stringify(user));
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de l\'écriture dans localStorage:', error);
|
||||
}
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
const login = (userData: User) => {
|
||||
setUser(userData);
|
||||
try {
|
||||
localStorage.setItem('user', JSON.stringify(userData));
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la sauvegarde de l\'utilisateur:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const logout = () => {
|
||||
setUser(null);
|
||||
try {
|
||||
localStorage.removeItem('user');
|
||||
localStorage.removeItem('accessToken');
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la déconnexion:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const hasPermission = (permission: keyof User['permissions']) => {
|
||||
return user?.permissions?.[permission] || false;
|
||||
};
|
||||
|
||||
// ✅ Mémoïsation de la valeur du context pour éviter les re-renders inutiles
|
||||
const value = React.useMemo(() => ({
|
||||
user,
|
||||
login,
|
||||
logout,
|
||||
isAuthenticated: !!user,
|
||||
isRRE: user?.role === 'rre',
|
||||
isFormateur: user?.role === 'formateur',
|
||||
hasPermission
|
||||
}), [user]);
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={value}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useAuth = () => {
|
||||
const context = useContext(AuthContext);
|
||||
if (!context) {
|
||||
throw new Error('useAuth doit être utilisé dans un AuthProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
982
SuiviREForamteur/suivireforamteur/src/HistoriquePage.css
Normal file
982
SuiviREForamteur/suivireforamteur/src/HistoriquePage.css
Normal file
@@ -0,0 +1,982 @@
|
||||
/* HistoriquePage.css - VERSION OPTIMISÉE RESPONSIVE */
|
||||
|
||||
/* ═══════════════════════════════════════════════════════
|
||||
VARIABLES
|
||||
═══════════════════════════════════════════════════════ */
|
||||
:root {
|
||||
--primary-color: #0ea5e9;
|
||||
--primary-dark: #0284c7;
|
||||
--success-color: #22c55e;
|
||||
--warning-color: #f59e0b;
|
||||
--danger-color: #ef4444;
|
||||
--dark-color: #1e293b;
|
||||
--gray-color: #64748b;
|
||||
--light-gray: #f1f5f9;
|
||||
--border-radius: 12px;
|
||||
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
|
||||
--transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════
|
||||
RESET
|
||||
═══════════════════════════════════════════════════════ */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/* Réduction automatique pour les petits écrans */
|
||||
@media (max-width: 1600px) {
|
||||
html {
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1400px) {
|
||||
html {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
html {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
html {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.page-container {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 100vw;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════
|
||||
NAVBAR - COMPACT
|
||||
═══════════════════════════════════════════════════════ */
|
||||
.navbar {
|
||||
background: white;
|
||||
padding: 0.75rem 1.5rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
box-shadow: var(--shadow-md);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
gap: 1rem;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.nav-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.nav-logo {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.nav-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.2rem;
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
font-weight: 700;
|
||||
font-size: 1.1rem;
|
||||
color: var(--dark-color);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.nav-subtitle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
font-size: 0.8rem;
|
||||
color: var(--gray-color);
|
||||
}
|
||||
|
||||
.role-badge {
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
padding: 0.2rem 0.6rem;
|
||||
border-radius: 9999px;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.nav-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-weight: 600;
|
||||
color: var(--dark-color);
|
||||
font-size: 0.9rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.logout-button {
|
||||
background: var(--danger-color);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: var(--border-radius);
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
transition: var(--transition);
|
||||
font-size: 0.85rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.logout-button:hover {
|
||||
background: #dc2626;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════
|
||||
MAIN CONTENT - ADAPTATIF
|
||||
═══════════════════════════════════════════════════════ */
|
||||
.main-content {
|
||||
flex: 1;
|
||||
padding: 1.25rem;
|
||||
max-width: 100%;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.content-header {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.content-header h1 {
|
||||
font-size: 1.75rem;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════
|
||||
SEARCH - COMPACT
|
||||
═══════════════════════════════════════════════════════ */
|
||||
.search-section {
|
||||
background: white;
|
||||
padding: 1.25rem;
|
||||
border-radius: var(--border-radius);
|
||||
margin-bottom: 1.25rem;
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.search-wrapper {
|
||||
position: relative;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
position: absolute;
|
||||
left: 0.9rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
font-size: 1.1rem;
|
||||
color: var(--gray-color);
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 100%;
|
||||
padding: 0.7rem 2.5rem;
|
||||
border: 2px solid var(--light-gray);
|
||||
border-radius: var(--border-radius);
|
||||
font-size: 0.9rem;
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.search-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 0 3px rgba(14, 165, 233, 0.1);
|
||||
}
|
||||
|
||||
.search-clear {
|
||||
position: absolute;
|
||||
right: 0.9rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background: var(--light-gray);
|
||||
border: none;
|
||||
width: 1.8rem;
|
||||
height: 1.8rem;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
font-size: 1.2rem;
|
||||
color: var(--gray-color);
|
||||
transition: var(--transition);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.search-clear:hover {
|
||||
background: var(--danger-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════
|
||||
FILTERS - OPTIMISÉ
|
||||
═══════════════════════════════════════════════════════ */
|
||||
.filters-container {
|
||||
background: white;
|
||||
padding: 1.25rem;
|
||||
border-radius: var(--border-radius);
|
||||
margin-bottom: 1.25rem;
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.filters-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
gap: 0.9rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
font-weight: 600;
|
||||
color: var(--dark-color);
|
||||
font-size: 0.8rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.filter-icon {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.filter-select {
|
||||
padding: 0.55rem 0.75rem;
|
||||
border: 2px solid var(--light-gray);
|
||||
border-radius: var(--border-radius);
|
||||
font-size: 0.85rem;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.filter-select:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.filter-select:disabled {
|
||||
background: var(--light-gray);
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.filter-hint {
|
||||
color: var(--gray-color);
|
||||
font-size: 0.7rem;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.reset-filters-button {
|
||||
background: var(--gray-color);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.55rem 1rem;
|
||||
border-radius: var(--border-radius);
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
transition: var(--transition);
|
||||
width: 100%;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.reset-filters-button:hover {
|
||||
background: var(--dark-color);
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════
|
||||
TABLE - ULTRA COMPACT
|
||||
═══════════════════════════════════════════════════════ */
|
||||
.table-container {
|
||||
background: white;
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: var(--shadow-md);
|
||||
overflow-x: auto;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.data-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
min-width: 1000px;
|
||||
}
|
||||
|
||||
.data-table thead {
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.data-table th {
|
||||
padding: 0.75rem 0.6rem;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
font-size: 0.8rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.data-table th.sortable {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.data-table th.sortable:hover {
|
||||
background: var(--primary-dark);
|
||||
}
|
||||
|
||||
.sort-icon {
|
||||
margin-left: 0.3rem;
|
||||
opacity: 0.5;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.sort-icon.active {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.data-table td {
|
||||
padding: 0.75rem 0.6rem;
|
||||
border-bottom: 1px solid var(--light-gray);
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.table-row {
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.table-row:hover {
|
||||
background: var(--light-gray);
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════
|
||||
CELLS - COMPACT
|
||||
═══════════════════════════════════════════════════════ */
|
||||
.student-cell {
|
||||
min-width: 160px;
|
||||
}
|
||||
|
||||
.student-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.2rem;
|
||||
}
|
||||
|
||||
.student-name {
|
||||
color: var(--dark-color);
|
||||
font-weight: 600;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.student-id {
|
||||
color: var(--gray-color);
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
.promo-text {
|
||||
color: var(--dark-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
display: inline-block;
|
||||
padding: 0.3rem 0.6rem;
|
||||
border-radius: 9999px;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.status-success {
|
||||
background: #dcfce7;
|
||||
color: #166534;
|
||||
}
|
||||
|
||||
.status-progress {
|
||||
background: #fef3c7;
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
.status-pending {
|
||||
background: #fee2e2;
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
.date-cell {
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.date-text {
|
||||
color: var(--dark-color);
|
||||
font-weight: 500;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.no-date {
|
||||
color: var(--gray-color);
|
||||
}
|
||||
|
||||
.commentaire-formateur-cell,
|
||||
.commentaire-rre-cell {
|
||||
max-width: 250px;
|
||||
}
|
||||
|
||||
.commentaire-content,
|
||||
.commentaire-rre-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.commentaire-icon {
|
||||
font-size: 0.9rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.commentaire-text {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.no-comment {
|
||||
color: var(--gray-color);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.history-count {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
width: 1.8rem;
|
||||
height: 1.8rem;
|
||||
border-radius: 50%;
|
||||
font-weight: 600;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.actions-cell {
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 0.4rem;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: 0.4rem;
|
||||
border: none;
|
||||
border-radius: var(--border-radius);
|
||||
cursor: pointer;
|
||||
font-size: 1.1rem;
|
||||
transition: var(--transition);
|
||||
background: var(--light-gray);
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.action-view:hover {
|
||||
background: var(--primary-color);
|
||||
}
|
||||
|
||||
.action-export:hover {
|
||||
background: var(--success-color);
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════
|
||||
LOADING & EMPTY
|
||||
═══════════════════════════════════════════════════════ */
|
||||
.loading-state {
|
||||
text-align: center;
|
||||
padding: 3rem 1.5rem;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
border: 3px solid var(--light-gray);
|
||||
border-top-color: var(--primary-color);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto 1rem;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 3rem 1.5rem;
|
||||
color: var(--gray-color);
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════
|
||||
MODAL - RESPONSIVE
|
||||
═══════════════════════════════════════════════════════ */
|
||||
.modal-header {
|
||||
padding: 1.2rem 1.5rem;
|
||||
background: linear-gradient(135deg, var(--primary-color) 0%, var(--primary-dark) 100%);
|
||||
color: white;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-radius: 20px 20px 0 0;
|
||||
}
|
||||
|
||||
.modal-header h2 {
|
||||
font-size: 1.3rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border: none;
|
||||
color: white;
|
||||
font-size: 1.8rem;
|
||||
width: 2.2rem;
|
||||
height: 2.2rem;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.modal-close:hover {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.modal-student-info {
|
||||
padding: 1.2rem 1.5rem;
|
||||
background: var(--light-gray);
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 0.9rem;
|
||||
border-bottom: 2px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.2rem;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
color: var(--gray-color);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
color: var(--dark-color);
|
||||
}
|
||||
|
||||
.column-section-title {
|
||||
font-size: 1.15rem;
|
||||
font-weight: 700;
|
||||
color: var(--dark-color);
|
||||
margin-bottom: 1.2rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
padding-bottom: 0.6rem;
|
||||
border-bottom: 2px solid var(--light-gray);
|
||||
}
|
||||
|
||||
.column-section-title span:first-child {
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.history-badge {
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
padding: 0.2rem 0.6rem;
|
||||
border-radius: 9999px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.no-history-message {
|
||||
text-align: center;
|
||||
padding: 2.5rem 1rem;
|
||||
color: var(--gray-color);
|
||||
}
|
||||
|
||||
.no-history-icon {
|
||||
font-size: 3.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════
|
||||
STATUS & FORM
|
||||
═══════════════════════════════════════════════════════ */
|
||||
.current-status {
|
||||
background: white;
|
||||
padding: 1.2rem;
|
||||
border-radius: var(--border-radius);
|
||||
margin-bottom: 1.2rem;
|
||||
border: 2px solid var(--light-gray);
|
||||
}
|
||||
|
||||
.current-status h3 {
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
color: var(--dark-color);
|
||||
margin-bottom: 0.9rem;
|
||||
}
|
||||
|
||||
.status-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
||||
gap: 0.9rem;
|
||||
}
|
||||
|
||||
.status-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.status-item-label {
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
color: var(--gray-color);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.status-item-value {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
color: var(--dark-color);
|
||||
}
|
||||
|
||||
.action-form {
|
||||
background: white;
|
||||
padding: 1.2rem;
|
||||
border-radius: var(--border-radius);
|
||||
border: 2px solid var(--light-gray);
|
||||
}
|
||||
|
||||
.form-grid-2col {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 0.9rem;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.form-group-full {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-weight: 600;
|
||||
color: var(--dark-color);
|
||||
font-size: 0.8rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.label-icon {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.required-star {
|
||||
color: var(--danger-color);
|
||||
margin-left: 0.2rem;
|
||||
}
|
||||
|
||||
.form-select,
|
||||
.form-input {
|
||||
padding: 0.65rem;
|
||||
border: 2px solid var(--light-gray);
|
||||
border-radius: var(--border-radius);
|
||||
font-size: 0.8rem;
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.form-select:focus,
|
||||
.form-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 0 3px rgba(14, 165, 233, 0.1);
|
||||
}
|
||||
|
||||
.form-textarea {
|
||||
padding: 0.65rem;
|
||||
border: 2px solid var(--light-gray);
|
||||
border-radius: var(--border-radius);
|
||||
font-size: 0.8rem;
|
||||
font-family: inherit;
|
||||
resize: vertical;
|
||||
transition: var(--transition);
|
||||
min-height: 80px;
|
||||
}
|
||||
|
||||
.form-textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 0 3px rgba(14, 165, 233, 0.1);
|
||||
}
|
||||
|
||||
.form-textarea-highlight {
|
||||
border-color: var(--warning-color);
|
||||
background: #fffbeb;
|
||||
}
|
||||
|
||||
.form-info-note {
|
||||
grid-column: 1 / -1;
|
||||
background: #dbeafe;
|
||||
color: #1e40af;
|
||||
padding: 0.65rem 0.9rem;
|
||||
border-radius: var(--border-radius);
|
||||
font-size: 0.8rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.rre-readonly-info {
|
||||
background: var(--light-gray);
|
||||
padding: 0.9rem;
|
||||
border-radius: var(--border-radius);
|
||||
margin-bottom: 1.2rem;
|
||||
border-left: 4px solid var(--primary-color);
|
||||
}
|
||||
|
||||
.rre-readonly-info h4 {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 700;
|
||||
color: var(--dark-color);
|
||||
margin-bottom: 0.6rem;
|
||||
}
|
||||
|
||||
.rre-readonly-info p {
|
||||
font-size: 0.8rem;
|
||||
color: var(--dark-color);
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════
|
||||
FOOTER
|
||||
═══════════════════════════════════════════════════════ */
|
||||
.modal-footer {
|
||||
padding: 1.2rem 1.5rem;
|
||||
background: var(--light-gray);
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.9rem;
|
||||
border-radius: 0 0 20px 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.secondary-button {
|
||||
background: var(--gray-color);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.65rem 1.2rem;
|
||||
border-radius: var(--border-radius);
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
transition: var(--transition);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.secondary-button:hover {
|
||||
background: var(--dark-color);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.btn-add-action {
|
||||
background: var(--success-color);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.65rem 1.2rem;
|
||||
border-radius: var(--border-radius);
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
transition: var(--transition);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.btn-add-action:hover:not(:disabled) {
|
||||
background: #16a34a;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.btn-add-action:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════
|
||||
SCROLLBAR
|
||||
═══════════════════════════════════════════════════════ */
|
||||
.table-container::-webkit-scrollbar {
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
.table-container::-webkit-scrollbar-track {
|
||||
background: var(--light-gray);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.table-container::-webkit-scrollbar-thumb {
|
||||
background: var(--primary-color);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.table-container::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--primary-dark);
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════
|
||||
RESPONSIVE - MOBILE
|
||||
═══════════════════════════════════════════════════════ */
|
||||
@media (max-width: 768px) {
|
||||
html {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
padding: 0.9rem;
|
||||
}
|
||||
|
||||
.nav-right {
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.filters-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.data-table {
|
||||
min-width: 800px;
|
||||
}
|
||||
|
||||
div[style*="grid-template-columns: 1fr 1fr"] {
|
||||
grid-template-columns: 1fr !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Ajoutez ceci dans la constante styles */
|
||||
|
||||
.table-container {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||
overflow: auto; /* ✅ Changé de overflow-x à overflow */
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* Scrollbar verticale */
|
||||
.table-container::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.table-container::-webkit-scrollbar-track {
|
||||
background: #f1f5f9;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.table-container::-webkit-scrollbar-thumb {
|
||||
background: #0ea5e9;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.table-container::-webkit-scrollbar-thumb:hover {
|
||||
background: #0284c7;
|
||||
}
|
||||
|
||||
/* Corner pour les deux scrollbars */
|
||||
.table-container::-webkit-scrollbar-corner {
|
||||
background: #f1f5f9;
|
||||
}
|
||||
2540
SuiviREForamteur/suivireforamteur/src/HistoriquePage.tsx
Normal file
2540
SuiviREForamteur/suivireforamteur/src/HistoriquePage.tsx
Normal file
File diff suppressed because it is too large
Load Diff
101
SuiviREForamteur/suivireforamteur/src/Historytimeline.tsx
Normal file
101
SuiviREForamteur/suivireforamteur/src/Historytimeline.tsx
Normal file
@@ -0,0 +1,101 @@
|
||||
import React from 'react';
|
||||
import type { HistoryEntry } from './types';
|
||||
import { formatDateTime } from './types';
|
||||
|
||||
interface HistoryTimelineProps {
|
||||
history: HistoryEntry[];
|
||||
}
|
||||
|
||||
const HistoryTimeline: React.FC<HistoryTimelineProps> = ({ history }) => {
|
||||
// Trier par date décroissante (plus récent en premier)
|
||||
const sortedHistory = [...history].sort(
|
||||
(a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
|
||||
);
|
||||
|
||||
if (sortedHistory.length === 0) {
|
||||
return (
|
||||
<div className="history-empty">
|
||||
<svg width="48" height="48" viewBox="0 0 48 48" fill="none">
|
||||
<circle cx="24" cy="24" r="20" stroke="#cbd5e1" strokeWidth="2" strokeDasharray="4 4" />
|
||||
<path d="M24 16v8l6 3" stroke="#cbd5e1" strokeWidth="2" strokeLinecap="round" />
|
||||
</svg>
|
||||
<p>Aucun historique disponible</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="history-timeline">
|
||||
{sortedHistory.map((entry, index) => {
|
||||
const isLatest = index === 0;
|
||||
const isCreation = entry.action === 'creation';
|
||||
|
||||
return (
|
||||
<div
|
||||
key={entry.id}
|
||||
className={`timeline-entry ${isLatest ? 'latest' : ''} ${isCreation ? 'creation' : 'modification'}`}
|
||||
>
|
||||
<div className="timeline-connector">
|
||||
<div className={`timeline-dot ${isCreation ? 'dot-creation' : 'dot-modification'}`}>
|
||||
{isCreation ? (
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
|
||||
<path d="M7 3v8M3 7h8" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
|
||||
<path d="M10 4L5 9 3 7" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
{index < sortedHistory.length - 1 && <div className="timeline-line" />}
|
||||
</div>
|
||||
|
||||
<div className="timeline-content">
|
||||
<div className="timeline-header-simple">
|
||||
<div className="timeline-action">
|
||||
<span className={`action-badge ${isCreation ? 'badge-creation' : 'badge-modification'}`}>
|
||||
{isCreation ? '🎯 Création' : '✏️ Nouvelle Action'}
|
||||
</span>
|
||||
{isLatest && <span className="latest-badge">⭐ Actuel</span>}
|
||||
</div>
|
||||
|
||||
<div className="timeline-meta">
|
||||
<span className="timeline-date">
|
||||
📅 {formatDateTime(entry.timestamp)}
|
||||
</span>
|
||||
<span className="timeline-user">
|
||||
👤 {entry.userName}
|
||||
<span className={`role-tag ${entry.userRole}`}>
|
||||
{entry.userRole === 'formateur' ? 'Formateur' : 'RRE'}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="timeline-reason">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||
<path d="M2 4h12M2 8h8M2 12h10" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
|
||||
</svg>
|
||||
<span className="reason-text">{entry.reason}</span>
|
||||
</div>
|
||||
|
||||
{/* Afficher les anciennes valeurs uniquement */}
|
||||
{entry.changes.length > 0 && (
|
||||
<div className="timeline-old-values">
|
||||
{entry.changes.map((change, changeIndex) => (
|
||||
<div key={changeIndex} className="old-value-item">
|
||||
<span className="field-name">{change.fieldLabel}:</span>
|
||||
<span className="field-old-value">{change.oldValue}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default HistoryTimeline;
|
||||
0
SuiviREForamteur/suivireforamteur/src/Index.css
Normal file
0
SuiviREForamteur/suivireforamteur/src/Index.css
Normal file
386
SuiviREForamteur/suivireforamteur/src/LoginPage.tsx
Normal file
386
SuiviREForamteur/suivireforamteur/src/LoginPage.tsx
Normal file
@@ -0,0 +1,386 @@
|
||||
// LoginPage.tsx
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { msalInstance, loginRequest } from './msalConfig';
|
||||
import type { User } from './types';
|
||||
|
||||
interface LoginPageProps {
|
||||
onLogin: (user: User) => void;
|
||||
}
|
||||
|
||||
const LoginPage: React.FC<LoginPageProps> = ({ onLogin }) => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// Gérer le callback après redirection Microsoft
|
||||
msalInstance.handleRedirectPromise()
|
||||
.then(async (response) => {
|
||||
if (response) {
|
||||
console.log('✅ Authentification réussie');
|
||||
await handleAuthSuccess(response.account.username);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('❌ Erreur auth:', error);
|
||||
setError('Erreur lors de l\'authentification');
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleAuthSuccess = async (email: string) => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
// Vérifier l'utilisateur dans la base de données via le backend
|
||||
const response = await fetch('/auth/verify', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ email }),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok && data.valid) {
|
||||
localStorage.setItem('user', JSON.stringify(data.user));
|
||||
onLogin(data.user);
|
||||
} else {
|
||||
setError('Utilisateur non autorisé. Contactez l\'administrateur.');
|
||||
}
|
||||
} catch (err) {
|
||||
setError('Erreur de connexion au serveur');
|
||||
console.error(err);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMicrosoftLogin = async () => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
await msalInstance.loginRedirect(loginRequest);
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Erreur lors de la connexion');
|
||||
setIsLoading(false);
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={styles.container}>
|
||||
{/* Panneau gauche - Branding */}
|
||||
<div style={styles.leftPanel}>
|
||||
<div style={styles.brandContent}>
|
||||
<div style={styles.logoContainer}>
|
||||
<svg width="72" height="72" viewBox="0 0 72 72" fill="none">
|
||||
<rect width="72" height="72" rx="16" fill="rgba(255,255,255,0.15)" />
|
||||
<path
|
||||
d="M22 36L32 46L50 28"
|
||||
stroke="#fff"
|
||||
strokeWidth="5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<h1 style={styles.brandTitle}>Suivi Évaluations</h1>
|
||||
<p style={styles.brandSubtitle}>Plateforme de Gestion des Formations</p>
|
||||
|
||||
<div style={styles.features}>
|
||||
{[
|
||||
'Suivi en temps réel des évaluations',
|
||||
'Gestion centralisée des apprenants',
|
||||
'Collaboration formateurs et RRE'
|
||||
].map((feature, index) => (
|
||||
<div key={index} style={styles.featureItem}>
|
||||
<svg width="22" height="22" viewBox="0 0 22 22" fill="none">
|
||||
<circle cx="11" cy="11" r="10" stroke="#fff" strokeWidth="1.5" opacity="0.8" />
|
||||
<path d="M7 11l3 3 5-5" stroke="#fff" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
<span>{feature}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Panneau droit - Connexion */}
|
||||
<div style={styles.rightPanel}>
|
||||
<div style={styles.loginCard}>
|
||||
<div style={styles.cardHeader}>
|
||||
<h2 style={styles.cardTitle}>Bienvenue</h2>
|
||||
<p style={styles.cardSubtitle}>
|
||||
Connectez-vous avec votre compte Microsoft 365 professionnel
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style={styles.cardBody}>
|
||||
{error && (
|
||||
<div style={styles.errorAlert}>
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
|
||||
<circle cx="10" cy="10" r="9" stroke="#ef4444" strokeWidth="2" />
|
||||
<path d="M10 6v4M10 14h.01" stroke="#ef4444" strokeWidth="2" strokeLinecap="round" />
|
||||
</svg>
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={styles.microsoftIconWrapper}>
|
||||
<svg width="80" height="80" viewBox="0 0 23 23" fill="none">
|
||||
<rect x="1" y="1" width="10" height="10" fill="#f25022" />
|
||||
<rect x="12" y="1" width="10" height="10" fill="#7fba00" />
|
||||
<rect x="1" y="12" width="10" height="10" fill="#00a4ef" />
|
||||
<rect x="12" y="12" width="10" height="10" fill="#ffb900" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleMicrosoftLogin}
|
||||
disabled={isLoading}
|
||||
style={{
|
||||
...styles.microsoftButton,
|
||||
...(isLoading ? styles.microsoftButtonLoading : {})
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (!isLoading) {
|
||||
e.currentTarget.style.transform = 'translateY(-2px)';
|
||||
e.currentTarget.style.boxShadow = '0 8px 25px rgba(0, 120, 212, 0.35)';
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.transform = 'translateY(0)';
|
||||
e.currentTarget.style.boxShadow = '0 4px 15px rgba(0, 120, 212, 0.25)';
|
||||
}}
|
||||
>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<div style={styles.spinner} />
|
||||
<span>Connexion en cours...</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<svg width="20" height="20" viewBox="0 0 23 23" fill="none">
|
||||
<rect x="1" y="1" width="10" height="10" fill="#f25022" />
|
||||
<rect x="12" y="1" width="10" height="10" fill="#7fba00" />
|
||||
<rect x="1" y="12" width="10" height="10" fill="#00a4ef" />
|
||||
<rect x="12" y="12" width="10" height="10" fill="#ffb900" />
|
||||
</svg>
|
||||
<span>Se connecter avec Microsoft 365</span>
|
||||
<svg width="18" height="18" viewBox="0 0 20 20" fill="none">
|
||||
<path
|
||||
d="M5 10h10M12 7l3 3-3 3"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
<p style={styles.securityNote}>
|
||||
<svg width="16" height="16" viewBox="0 0 20 20" fill="none" style={{ flexShrink: 0 }}>
|
||||
<path
|
||||
d="M10 2L3 6v4c0 4.5 3 8.5 7 10 4-1.5 7-5.5 7-10V6l-7-4z"
|
||||
stroke="#22c55e"
|
||||
strokeWidth="1.5"
|
||||
fill="none"
|
||||
/>
|
||||
<path
|
||||
d="M7 10l2 2 4-4"
|
||||
stroke="#22c55e"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
<span>Connexion sécurisée via authentification Microsoft</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style={styles.cardFooter}>
|
||||
<p style={styles.footerText}>
|
||||
Besoin d'aide ? <a href="#" style={styles.footerLink}>Contactez le support technique</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>{`
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(20px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const styles: { [key: string]: React.CSSProperties } = {
|
||||
container: {
|
||||
display: 'flex',
|
||||
minHeight: '100vh',
|
||||
fontFamily: "'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
|
||||
},
|
||||
leftPanel: {
|
||||
flex: '1',
|
||||
background: 'linear-gradient(135deg, #0078d4 0%, #005a9e 50%, #004578 100%)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: '3rem',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
brandContent: {
|
||||
color: '#fff',
|
||||
maxWidth: '420px',
|
||||
zIndex: 1,
|
||||
},
|
||||
logoContainer: {
|
||||
marginBottom: '2rem',
|
||||
},
|
||||
brandTitle: {
|
||||
fontSize: '2.5rem',
|
||||
fontWeight: 700,
|
||||
marginBottom: '0.75rem',
|
||||
letterSpacing: '-0.02em',
|
||||
},
|
||||
brandSubtitle: {
|
||||
fontSize: '1.1rem',
|
||||
opacity: 0.9,
|
||||
marginBottom: '3rem',
|
||||
fontWeight: 400,
|
||||
},
|
||||
features: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '1.25rem',
|
||||
},
|
||||
featureItem: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '1rem',
|
||||
fontSize: '1rem',
|
||||
opacity: 0.95,
|
||||
},
|
||||
rightPanel: {
|
||||
flex: '1',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: '3rem',
|
||||
backgroundColor: '#f8fafc',
|
||||
},
|
||||
loginCard: {
|
||||
backgroundColor: '#fff',
|
||||
borderRadius: '20px',
|
||||
boxShadow: '0 25px 50px -12px rgba(0, 0, 0, 0.1)',
|
||||
padding: '3rem',
|
||||
width: '100%',
|
||||
maxWidth: '440px',
|
||||
animation: 'fadeIn 0.5s ease-out',
|
||||
},
|
||||
cardHeader: {
|
||||
textAlign: 'center',
|
||||
marginBottom: '2.5rem',
|
||||
},
|
||||
cardTitle: {
|
||||
fontSize: '2rem',
|
||||
fontWeight: 700,
|
||||
color: '#1e293b',
|
||||
marginBottom: '0.75rem',
|
||||
},
|
||||
cardSubtitle: {
|
||||
fontSize: '1rem',
|
||||
color: '#64748b',
|
||||
lineHeight: 1.6,
|
||||
},
|
||||
cardBody: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
gap: '1.5rem',
|
||||
},
|
||||
errorAlert: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '10px',
|
||||
width: '100%',
|
||||
padding: '12px 16px',
|
||||
backgroundColor: '#fee2e2',
|
||||
border: '1px solid #fecaca',
|
||||
borderRadius: '8px',
|
||||
color: '#991b1b',
|
||||
fontSize: '0.875rem',
|
||||
},
|
||||
microsoftIconWrapper: {
|
||||
padding: '1.5rem',
|
||||
backgroundColor: '#f1f5f9',
|
||||
borderRadius: '16px',
|
||||
marginBottom: '0.5rem',
|
||||
},
|
||||
microsoftButton: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: '12px',
|
||||
width: '100%',
|
||||
padding: '16px 24px',
|
||||
fontSize: '1rem',
|
||||
fontWeight: 600,
|
||||
color: '#fff',
|
||||
backgroundColor: '#0078d4',
|
||||
border: 'none',
|
||||
borderRadius: '12px',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s ease',
|
||||
boxShadow: '0 4px 15px rgba(0, 120, 212, 0.25)',
|
||||
},
|
||||
microsoftButtonLoading: {
|
||||
opacity: 0.8,
|
||||
cursor: 'not-allowed',
|
||||
},
|
||||
spinner: {
|
||||
width: '22px',
|
||||
height: '22px',
|
||||
border: '3px solid rgba(255,255,255,0.3)',
|
||||
borderTopColor: '#fff',
|
||||
borderRadius: '50%',
|
||||
animation: 'spin 0.8s linear infinite',
|
||||
},
|
||||
securityNote: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
fontSize: '0.875rem',
|
||||
color: '#64748b',
|
||||
marginTop: '0.5rem',
|
||||
},
|
||||
cardFooter: {
|
||||
marginTop: '2.5rem',
|
||||
paddingTop: '1.5rem',
|
||||
borderTop: '1px solid #e2e8f0',
|
||||
textAlign: 'center',
|
||||
},
|
||||
footerText: {
|
||||
fontSize: '0.875rem',
|
||||
color: '#64748b',
|
||||
},
|
||||
footerLink: {
|
||||
color: '#0078d4',
|
||||
textDecoration: 'none',
|
||||
fontWeight: 500,
|
||||
},
|
||||
};
|
||||
|
||||
export default LoginPage;
|
||||
53
SuiviREForamteur/suivireforamteur/src/ProtectedRoute.tsx
Normal file
53
SuiviREForamteur/suivireforamteur/src/ProtectedRoute.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
// ProtectedRoute.tsx
|
||||
import React from 'react';
|
||||
import { useAuth } from './AuthContext';
|
||||
|
||||
interface ProtectedRouteProps {
|
||||
children: React.ReactNode;
|
||||
requiredRole?: 'formateur' | 'rre';
|
||||
requiredPermission?: string;
|
||||
}
|
||||
|
||||
export const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
|
||||
children,
|
||||
requiredRole,
|
||||
requiredPermission
|
||||
}) => {
|
||||
const { user, hasPermission } = useAuth();
|
||||
|
||||
if (!user) {
|
||||
return <div>Accès non autorisé. Veuillez vous connecter.</div>;
|
||||
}
|
||||
|
||||
if (requiredRole && user.role !== requiredRole) {
|
||||
return (
|
||||
<div style={styles.accessDenied}>
|
||||
<h2>Accès refusé</h2>
|
||||
<p>Vous n'avez pas les permissions nécessaires pour accéder à cette page.</p>
|
||||
<p>Rôle requis: {requiredRole}</p>
|
||||
<p>Votre rôle: {user.role}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (requiredPermission && !hasPermission(requiredPermission as any)) {
|
||||
return (
|
||||
<div style={styles.accessDenied}>
|
||||
<h2>Accès refusé</h2>
|
||||
<p>Vous n'avez pas la permission requise pour cette action.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
const styles = {
|
||||
accessDenied: {
|
||||
padding: '2rem',
|
||||
textAlign: 'center' as const,
|
||||
backgroundColor: '#fee2e2',
|
||||
borderRadius: '8px',
|
||||
margin: '2rem',
|
||||
}
|
||||
};
|
||||
352
SuiviREForamteur/suivireforamteur/src/SaisiePage.tsx
Normal file
352
SuiviREForamteur/suivireforamteur/src/SaisiePage.tsx
Normal file
@@ -0,0 +1,352 @@
|
||||
// SaisiePage.tsx - Mise à jour avec user.id
|
||||
import React, { useState } from 'react';
|
||||
import type { User, EvaluationRecord, HistoryEntry, FieldChange } from './types';
|
||||
import { generateId } from './types';
|
||||
import './styles.css';
|
||||
|
||||
interface SaisiePageProps {
|
||||
onSave: (record: EvaluationRecord) => void;
|
||||
onCancel: () => void;
|
||||
user: User;
|
||||
}
|
||||
|
||||
const SaisiePage: React.FC<SaisiePageProps> = ({ onSave, onCancel, user }) => {
|
||||
const [formData, setFormData] = useState({
|
||||
nom: '',
|
||||
prenom: '',
|
||||
formation: '',
|
||||
autoEvaluation: '',
|
||||
dateAction: '',
|
||||
commentaireFormateur: '',
|
||||
evaluationTuteur: '',
|
||||
dateActionTuteur: '',
|
||||
commentaireRRE: ''
|
||||
});
|
||||
|
||||
const [creationReason, setCreationReason] = useState('');
|
||||
const [reasonError, setReasonError] = useState(false);
|
||||
|
||||
const handleChange = (field: string, value: string) => {
|
||||
setFormData(prev => ({ ...prev, [field]: value }));
|
||||
};
|
||||
|
||||
const handleApprenantChange = (value: string) => {
|
||||
if (value) {
|
||||
const [nom, prenom, formation] = value.split('|');
|
||||
setFormData(prev => ({ ...prev, nom, prenom, formation }));
|
||||
} else {
|
||||
setFormData(prev => ({ ...prev, nom: '', prenom: '', formation: '' }));
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
// Vérifier la raison
|
||||
if (!creationReason.trim()) {
|
||||
setReasonError(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const now = new Date().toISOString();
|
||||
const recordId = generateId();
|
||||
|
||||
// Créer l'entrée d'historique initiale
|
||||
const initialHistory: HistoryEntry = {
|
||||
id: generateId(),
|
||||
timestamp: now,
|
||||
userId: user.id || user.email,
|
||||
userName: user.nomComplet || user.name,
|
||||
userRole: user.role as 'formateur' | 'rre',
|
||||
action: 'creation',
|
||||
reason: creationReason.trim(),
|
||||
changes: []
|
||||
};
|
||||
|
||||
// Créer l'enregistrement complet
|
||||
const newRecord: EvaluationRecord = {
|
||||
...formData,
|
||||
id: recordId,
|
||||
campus: user.campus || 'CGY',
|
||||
createdAt: now,
|
||||
createdBy: user.id || user.email,
|
||||
createdByName: user.nomComplet || user.name,
|
||||
history: [initialHistory],
|
||||
autoEvaluation: formData.autoEvaluation as 'OUI' | 'NON' | 'EN COURS',
|
||||
evaluationTuteur: formData.evaluationTuteur as 'OUI' | 'NON' | 'EN COURS'
|
||||
};
|
||||
|
||||
onSave(newRecord);
|
||||
};
|
||||
|
||||
const formations = ['BTS NDRC 1 JV', 'BTS MCO 1', 'BTS GPME 2', 'Bachelor RH', 'Master Management'];
|
||||
|
||||
// Liste d'apprenants (normalement cela viendrait d'une API/base de données)
|
||||
const apprenants = [
|
||||
{ nom: 'Dupont', prenom: 'Marie', formation: 'BTS NDRC 1 JV' },
|
||||
{ nom: 'Martin', prenom: 'Pierre', formation: 'BTS MCO 1' },
|
||||
{ nom: 'Bernard', prenom: 'Sophie', formation: 'BTS GPME 2' },
|
||||
{ nom: 'Dubois', prenom: 'Lucas', formation: 'Bachelor RH' },
|
||||
{ nom: 'Thomas', prenom: 'Emma', formation: 'Master Management' }
|
||||
];
|
||||
|
||||
const isFormateur = user.role === 'formateur';
|
||||
const isRRE = user.role === 'rre';
|
||||
|
||||
return (
|
||||
<div className="page-container">
|
||||
<nav className="navbar">
|
||||
<div className="nav-left">
|
||||
<button onClick={onCancel} className="back-button">
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
|
||||
<path d="M15 10H5M8 13l-3-3 3-3" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</button>
|
||||
<div className="nav-info">
|
||||
<span className="nav-title">Nouvelle Évaluation</span>
|
||||
<span className="nav-subtitle">Saisie complète</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="nav-right">
|
||||
<div className="user-info">
|
||||
<div className="user-text">
|
||||
<span className="user-name">{user.nomComplet || user.name}</span>
|
||||
<span className="user-role">
|
||||
{isFormateur ? 'Formateur Référent' : 'Responsable RRE'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="user-avatar">{user.name[0].toUpperCase()}</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main className="form-main">
|
||||
<form onSubmit={handleSubmit} className="form-container">
|
||||
{/* Section Apprenant */}
|
||||
<div className="form-section">
|
||||
<div className="section-header">
|
||||
<div className="section-icon student">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
|
||||
<circle cx="12" cy="8" r="4" stroke="currentColor" strokeWidth="2" />
|
||||
<path d="M4 20c0-4 3.58-7 8-7s8 3 8 7" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="section-title">Informations Apprenant</h2>
|
||||
<p className="section-subtitle">Identification et formation</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-grid">
|
||||
<div className="form-field full-width">
|
||||
<label className="field-label">Sélectionner un apprenant *</label>
|
||||
<select
|
||||
value={formData.nom && formData.prenom ? `${formData.nom}|${formData.prenom}|${formData.formation}` : ''}
|
||||
onChange={(e) => handleApprenantChange(e.target.value)}
|
||||
className="field-select"
|
||||
required
|
||||
>
|
||||
<option value="">Choisir un apprenant...</option>
|
||||
{apprenants.map((app, idx) => (
|
||||
<option key={idx} value={`${app.nom}|${app.prenom}|${app.formation}`}>
|
||||
{app.prenom} {app.nom} - {app.formation}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{formData.nom && (
|
||||
<div className="form-field full-width">
|
||||
<div className="selected-info">
|
||||
<div className="info-row">
|
||||
<span className="info-label">Nom:</span>
|
||||
<span className="info-value">{formData.nom}</span>
|
||||
</div>
|
||||
<div className="info-row">
|
||||
<span className="info-label">Prénom:</span>
|
||||
<span className="info-value">{formData.prenom}</span>
|
||||
</div>
|
||||
<div className="info-row">
|
||||
<span className="info-label">Formation:</span>
|
||||
<span className="info-value">{formData.formation}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section Formateur */}
|
||||
<div className={`form-section ${isRRE ? 'disabled-section' : ''}`}>
|
||||
<div className="section-header">
|
||||
<div className="section-icon formateur">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
|
||||
<rect x="3" y="4" width="18" height="16" rx="2" stroke="currentColor" strokeWidth="2" />
|
||||
<path d="M8 10h8M8 14h5" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="section-title">Saisie Formateur Référent</h2>
|
||||
<p className="section-subtitle">Suivi et commentaires</p>
|
||||
{isRRE && <span className="disabled-badge">Section réservée au formateur</span>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-grid">
|
||||
<div className="form-field">
|
||||
<label className="field-label">Auto-évaluation du jeune</label>
|
||||
<div className="radio-group">
|
||||
{['OUI', 'NON', 'EN COURS'].map(opt => (
|
||||
<label key={opt} className="radio-label">
|
||||
<input
|
||||
type="radio"
|
||||
name="autoEvaluation"
|
||||
value={opt}
|
||||
checked={formData.autoEvaluation === opt}
|
||||
onChange={(e) => handleChange('autoEvaluation', e.target.value)}
|
||||
className="radio-input"
|
||||
disabled={isRRE}
|
||||
/>
|
||||
<span className={`radio-custom ${formData.autoEvaluation === opt ? 'active' : ''} ${isRRE ? 'disabled' : ''}`}>
|
||||
{opt}
|
||||
</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-field">
|
||||
<label className="field-label">Date d'action</label>
|
||||
<input
|
||||
type="date"
|
||||
value={formData.dateAction}
|
||||
onChange={(e) => handleChange('dateAction', e.target.value)}
|
||||
className="field-input"
|
||||
disabled={isRRE}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-field full-width">
|
||||
<label className="field-label">Commentaire du formateur</label>
|
||||
<textarea
|
||||
value={formData.commentaireFormateur}
|
||||
onChange={(e) => handleChange('commentaireFormateur', e.target.value)}
|
||||
placeholder={isRRE ? "Réservé au formateur" : "Ajoutez vos observations..."}
|
||||
className="field-textarea"
|
||||
rows={4}
|
||||
disabled={isRRE}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section RRE */}
|
||||
<div className={`form-section ${isFormateur ? 'disabled-section' : ''}`}>
|
||||
<div className="section-header">
|
||||
<div className="section-icon rre">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M9 12l2 2 4-4" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<circle cx="12" cy="12" r="9" stroke="currentColor" strokeWidth="2" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="section-title">Saisie Responsable RRE</h2>
|
||||
<p className="section-subtitle">Validation tuteur</p>
|
||||
{isFormateur && <span className="disabled-badge">Section réservée au RRE</span>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-grid">
|
||||
<div className="form-field">
|
||||
<label className="field-label">Évaluation par le tuteur</label>
|
||||
<div className="radio-group">
|
||||
{['OUI', 'NON', 'EN COURS'].map(opt => (
|
||||
<label key={opt} className="radio-label">
|
||||
<input
|
||||
type="radio"
|
||||
name="evaluationTuteur"
|
||||
value={opt}
|
||||
checked={formData.evaluationTuteur === opt}
|
||||
onChange={(e) => handleChange('evaluationTuteur', e.target.value)}
|
||||
className="radio-input"
|
||||
disabled={isFormateur}
|
||||
/>
|
||||
<span className={`radio-custom ${formData.evaluationTuteur === opt ? 'active' : ''} ${isFormateur ? 'disabled' : ''}`}>
|
||||
{opt}
|
||||
</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-field">
|
||||
<label className="field-label">Date d'action</label>
|
||||
<input
|
||||
type="date"
|
||||
value={formData.dateActionTuteur}
|
||||
onChange={(e) => handleChange('dateActionTuteur', e.target.value)}
|
||||
className="field-input"
|
||||
disabled={isFormateur}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-field full-width">
|
||||
<label className="field-label">Commentaire RRE</label>
|
||||
<textarea
|
||||
value={formData.commentaireRRE}
|
||||
onChange={(e) => handleChange('commentaireRRE', e.target.value)}
|
||||
placeholder={isFormateur ? "Réservé au RRE" : "Ajoutez vos observations..."}
|
||||
className="field-textarea"
|
||||
rows={4}
|
||||
disabled={isFormateur}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Raison de création */}
|
||||
<div className="detail-section reason-section">
|
||||
<h3 className="detail-section-title required-title">
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none">
|
||||
<path d="M9 2v8M9 14h.01" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
|
||||
</svg>
|
||||
Contexte de création
|
||||
<span className="required-asterisk">*</span>
|
||||
</h3>
|
||||
<div className="form-field full-width">
|
||||
<textarea
|
||||
value={creationReason}
|
||||
onChange={(e) => {
|
||||
setCreationReason(e.target.value);
|
||||
if (e.target.value.trim()) setReasonError(false);
|
||||
}}
|
||||
className={`field-textarea reason-textarea ${reasonError ? 'error' : ''}`}
|
||||
rows={3}
|
||||
placeholder="Décrivez le contexte de cette création (obligatoire pour l'audit)..."
|
||||
/>
|
||||
{reasonError && (
|
||||
<div className="field-error">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||
<circle cx="8" cy="8" r="7" stroke="currentColor" strokeWidth="1.5" />
|
||||
<path d="M8 5v3M8 11h.01" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
|
||||
</svg>
|
||||
Le contexte de création est obligatoire pour la traçabilité
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-actions">
|
||||
<button type="button" onClick={onCancel} className="cancel-button">
|
||||
Annuler
|
||||
</button>
|
||||
<button type="submit" className="submit-button">
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
|
||||
<path d="M16 5L7 14 3 10" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
Enregistrer l'évaluation
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SaisiePage;
|
||||
1
SuiviREForamteur/suivireforamteur/src/assets/react.svg
Normal file
1
SuiviREForamteur/suivireforamteur/src/assets/react.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 4.0 KiB |
5
SuiviREForamteur/suivireforamteur/src/index.ts
Normal file
5
SuiviREForamteur/suivireforamteur/src/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export type { User, EvaluationRecord } from './types';
|
||||
export { default as LoginPage } from './LoginPage';
|
||||
export { default as HistoriquePage } from './HistoriquePage';
|
||||
export { default as SaisiePage } from './SaisiePage';
|
||||
export { default as App } from './App';
|
||||
47
SuiviREForamteur/suivireforamteur/src/main.tsx
Normal file
47
SuiviREForamteur/suivireforamteur/src/main.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App.tsx';
|
||||
import './Index.css';
|
||||
import { msalInstance } from './msalConfig';
|
||||
|
||||
// Fonction d'initialisation
|
||||
const initializeApp = async () => {
|
||||
try {
|
||||
// Initialiser MSAL
|
||||
await msalInstance.initialize();
|
||||
console.log('✓ MSAL initialisé');
|
||||
|
||||
// Gérer la redirection après login
|
||||
await msalInstance.handleRedirectPromise();
|
||||
|
||||
const rootElement = document.getElementById('root');
|
||||
|
||||
if (!rootElement) {
|
||||
throw new Error('Root element not found');
|
||||
}
|
||||
|
||||
ReactDOM.createRoot(rootElement).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Erreur lors de l\'initialisation:', error);
|
||||
|
||||
// Fallback: monter l'app même en cas d'erreur
|
||||
const rootElement = document.getElementById('root');
|
||||
if (rootElement) {
|
||||
ReactDOM.createRoot(rootElement).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Lancer l'initialisation
|
||||
initializeApp();
|
||||
45
SuiviREForamteur/suivireforamteur/src/msalConfig.ts
Normal file
45
SuiviREForamteur/suivireforamteur/src/msalConfig.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
// msalConfig.ts
|
||||
import { PublicClientApplication, LogLevel } from '@azure/msal-browser';
|
||||
|
||||
export const msalConfig = {
|
||||
auth: {
|
||||
clientId: '1225bb0a-37ec-4b82-be22-bb1f4a1e0231',
|
||||
authority: 'https://login.microsoftonline.com/9840a2a0-6ae1-4688-b03d-d2ec291be0f9',
|
||||
redirectUri: window.location.origin,
|
||||
},
|
||||
cache: {
|
||||
cacheLocation: 'sessionStorage',
|
||||
storeAuthStateInCookie: false,
|
||||
},
|
||||
system: {
|
||||
loggerOptions: {
|
||||
loggerCallback: (level: LogLevel, message: string, containsPii: boolean) => {
|
||||
if (containsPii) return;
|
||||
switch (level) {
|
||||
case LogLevel.Error:
|
||||
console.error(message);
|
||||
return;
|
||||
case LogLevel.Info:
|
||||
console.info(message);
|
||||
return;
|
||||
case LogLevel.Verbose:
|
||||
console.debug(message);
|
||||
return;
|
||||
case LogLevel.Warning:
|
||||
console.warn(message);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const loginRequest = {
|
||||
scopes: ['User.Read']
|
||||
};
|
||||
|
||||
// Créer l'instance (ne pas l'initialiser ici)
|
||||
export const msalInstance = new PublicClientApplication(msalConfig);
|
||||
|
||||
// ❌ SUPPRIMER cette ligne - l'initialisation se fait dans main.tsx
|
||||
// await msalInstance.initialize();
|
||||
408
SuiviREForamteur/suivireforamteur/src/styles.css
Normal file
408
SuiviREForamteur/suivireforamteur/src/styles.css
Normal file
@@ -0,0 +1,408 @@
|
||||
/* ===== STYLES DE BASE ===== */
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%);
|
||||
min-height: 100vh;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
/* ===== CONTENEUR PRINCIPAL ===== */
|
||||
.page-container {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* ===== NAVBAR ===== */
|
||||
.navbar {
|
||||
background: white;
|
||||
padding: 0.75rem 2rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.nav-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.nav-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.nav-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
font-weight: 700;
|
||||
font-size: 1.125rem;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.nav-subtitle {
|
||||
font-size: 0.75rem;
|
||||
color: #64748b;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.nav-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.user-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.user-role {
|
||||
font-size: 0.75rem;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #0ea5e9, #06b6d4);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-weight: 700;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.logout-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
background: white;
|
||||
color: #64748b;
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.logout-button:hover {
|
||||
border-color: #ef4444;
|
||||
color: #ef4444;
|
||||
background: #fef2f2;
|
||||
}
|
||||
|
||||
/* ===== CONTENU PRINCIPAL ===== */
|
||||
.main-content {
|
||||
flex: 1;
|
||||
padding: 2rem;
|
||||
max-width: 1600px;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.content-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 1.75rem;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
color: #64748b;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.count-badge {
|
||||
background: linear-gradient(135deg, #0ea5e9, #06b6d4);
|
||||
color: white;
|
||||
padding: 0.125rem 0.5rem;
|
||||
border-radius: 9999px;
|
||||
font-weight: 600;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.primary-button,
|
||||
.secondary-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.625rem 1.25rem;
|
||||
border-radius: 10px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.primary-button {
|
||||
background: linear-gradient(135deg, #0ea5e9, #06b6d4);
|
||||
color: white;
|
||||
border: none;
|
||||
box-shadow: 0 2px 8px rgba(14, 165, 233, 0.3);
|
||||
}
|
||||
|
||||
.primary-button:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(14, 165, 233, 0.4);
|
||||
}
|
||||
|
||||
.secondary-button {
|
||||
background: white;
|
||||
color: #475569;
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.secondary-button:hover {
|
||||
border-color: #0ea5e9;
|
||||
color: #0ea5e9;
|
||||
}
|
||||
|
||||
/* ===== SECTION RECHERCHE ===== */
|
||||
.search-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
position: relative;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
position: absolute;
|
||||
left: 1rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 100%;
|
||||
padding: 0.75rem 1rem 0.75rem 3rem;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 10px;
|
||||
font-size: 0.875rem;
|
||||
background: white;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.search-input:focus {
|
||||
outline: none;
|
||||
border-color: #0ea5e9;
|
||||
box-shadow: 0 0 0 3px rgba(14, 165, 233, 0.1);
|
||||
}
|
||||
|
||||
.filter-chips {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.chip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 9999px;
|
||||
background: white;
|
||||
color: #64748b;
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.chip:hover {
|
||||
border-color: #0ea5e9;
|
||||
color: #0ea5e9;
|
||||
}
|
||||
|
||||
.chip.active {
|
||||
background: linear-gradient(135deg, #0ea5e9, #06b6d4);
|
||||
color: white;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.chip-count {
|
||||
font-size: 0.75rem;
|
||||
padding: 0.125rem 0.375rem;
|
||||
border-radius: 9999px;
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.chip.active .chip-count {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
/* ===== TABLEAU ===== */
|
||||
.table-container {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.table thead {
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.table th {
|
||||
padding: 1rem;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: #64748b;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.table td {
|
||||
padding: 1rem;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.table tbody tr:hover {
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.th-actions {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.name-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 8px;
|
||||
background: linear-gradient(135deg, #e0f2fe, #cffafe);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #0ea5e9;
|
||||
font-weight: 600;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.name-primary {
|
||||
font-weight: 600;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.name-secondary {
|
||||
font-size: 0.75rem;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.formation-badge {
|
||||
display: inline-block;
|
||||
padding: 0.25rem 0.75rem;
|
||||
background: #f1f5f9;
|
||||
border-radius: 6px;
|
||||
font-size: 0.8125rem;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 9999px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.status-badge.success {
|
||||
background: #dcfce7;
|
||||
color: #16a34a;
|
||||
}
|
||||
|
||||
.status-badge.error {
|
||||
background: #fee2e2;
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.status-badge.warning {
|
||||
background: #fef3c7;
|
||||
color: #d97706;
|
||||
}
|
||||
|
||||
.date-cell {
|
||||
color: #64748b;
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.comment-text {
|
||||
display: block;
|
||||
max-width: 150px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
color: #64748b;
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
177
SuiviREForamteur/suivireforamteur/src/types.ts
Normal file
177
SuiviREForamteur/suivireforamteur/src/types.ts
Normal file
@@ -0,0 +1,177 @@
|
||||
// types.ts - Types pour le système de suivi des évaluations avec audit complet et authentification
|
||||
|
||||
// Interface User étendue avec authentification Microsoft
|
||||
export interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
name: string;
|
||||
nomComplet: string;
|
||||
campus: string | null;
|
||||
role: 'formateur' | 'rre' | 'super'|'lecteur'; // ✅ Changé 'admin' en 'super'
|
||||
permissions?: {
|
||||
peut_valider_evaluations?: boolean;
|
||||
peut_gerer_formateurs?: boolean;
|
||||
peut_saisir_evaluations?: boolean;
|
||||
peut_voir_tous_campus?: boolean;
|
||||
peut_exporter_donnees?: boolean;
|
||||
peut_usurper_identite?: boolean;
|
||||
};
|
||||
}
|
||||
// Permissions utilisateur
|
||||
export interface UserPermissions {
|
||||
peut_valider_evaluations: boolean;
|
||||
peut_gerer_formateurs: boolean;
|
||||
peut_saisir_evaluations: boolean;
|
||||
peut_voir_tous_campus: boolean;
|
||||
peut_exporter_donnees: boolean;
|
||||
}
|
||||
|
||||
// Entrée d'historique pour l'audit complet
|
||||
export interface HistoryEntry {
|
||||
id: string;
|
||||
timestamp: string;
|
||||
userId: string;
|
||||
userName: string;
|
||||
userRole: 'formateur' | 'rre' | 'super' | 'lecteur'; // ✅ Ajout de 'super'
|
||||
action: 'creation' | 'Nouvelle Action'; // ✅ Changé 'modification' en 'Nouvelle Action'
|
||||
reason: string;
|
||||
changes: FieldChange[];
|
||||
}
|
||||
|
||||
// Détail d'un changement de champ
|
||||
export interface FieldChange {
|
||||
field: keyof EvaluationRecord;
|
||||
fieldLabel: string;
|
||||
oldValue: string;
|
||||
newValue: string;
|
||||
}
|
||||
|
||||
// Enregistrement d'évaluation avec historique
|
||||
export interface EvaluationRecord {
|
||||
id: string;
|
||||
nom: string;
|
||||
prenom: string;
|
||||
email: string;
|
||||
campus: string;
|
||||
campusPromo?: string;
|
||||
formation: string;
|
||||
nomDip?: string;
|
||||
annee?: string;
|
||||
autoEvaluation: string;
|
||||
evaluationTuteur: string;
|
||||
dateAction?: string;
|
||||
dateActionRRE?: string;
|
||||
commentaireFormateur?: string;
|
||||
dateEvalTuteur?: string;
|
||||
commentaireRRE?: string;
|
||||
commentaire?: string;
|
||||
lastUpdate?: string;
|
||||
formateurEmail?: string;
|
||||
history?: HistoryEntry[];
|
||||
}
|
||||
|
||||
// Labels français pour les champs
|
||||
export const FIELD_LABELS: Record<string, string> = {
|
||||
auto_evaluation: 'Auto-évaluation',
|
||||
evaluation_tuteur: 'Évaluation tuteur',
|
||||
date_action: 'Date d\'action formateur',
|
||||
date_action_rre: 'Date d\'action RRE',
|
||||
commentaire_formateur: 'Commentaire formateur',
|
||||
date_eval_tuteur: 'Date évaluation tuteur',
|
||||
commentaire_rre: 'Commentaire RRE',
|
||||
commentaire: 'Commentaire général',
|
||||
autoEvaluation: 'Auto-évaluation',
|
||||
evaluationTuteur: 'Évaluation tuteur',
|
||||
dateAction: 'Date d\'action formateur',
|
||||
dateActionRRE: 'Date d\'action RRE',
|
||||
commentaireFormateur: 'Commentaire formateur',
|
||||
dateEvalTuteur: 'Date évaluation tuteur',
|
||||
commentaireRRE: 'Commentaire RRE'
|
||||
};
|
||||
|
||||
// Fonction utilitaire pour générer un ID unique
|
||||
export const generateId = (): string => {
|
||||
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
};
|
||||
|
||||
// Fonction pour formater une date ISO en format lisible (avec protection)
|
||||
export const formatDateTime = (isoString: string | null | undefined): string => {
|
||||
// Protection contre les valeurs nulles ou undefined
|
||||
if (!isoString) {
|
||||
return 'Date non disponible';
|
||||
}
|
||||
|
||||
try {
|
||||
const date = new Date(isoString);
|
||||
|
||||
// Vérifier si la date est valide
|
||||
if (isNaN(date.getTime())) {
|
||||
console.warn('Date invalide reçue:', isoString);
|
||||
return 'Date invalide';
|
||||
}
|
||||
|
||||
return new Intl.DateTimeFormat('fr-FR', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
}).format(date);
|
||||
} catch (error) {
|
||||
console.error('Erreur formatage date:', error, 'Valeur:', isoString);
|
||||
return 'Date invalide';
|
||||
}
|
||||
};
|
||||
|
||||
// Fonction pour formater une date courte (avec protection)
|
||||
export const formatDateShort = (isoString: string | null | undefined): string => {
|
||||
// Protection contre les valeurs nulles ou undefined
|
||||
if (!isoString) {
|
||||
return 'Date non disponible';
|
||||
}
|
||||
|
||||
try {
|
||||
const date = new Date(isoString);
|
||||
|
||||
// Vérifier si la date est valide
|
||||
if (isNaN(date.getTime())) {
|
||||
console.warn('Date invalide reçue:', isoString);
|
||||
return 'Date invalide';
|
||||
}
|
||||
|
||||
return new Intl.DateTimeFormat('fr-FR', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
}).format(date);
|
||||
} catch (error) {
|
||||
console.error('Erreur formatage date:', error, 'Valeur:', isoString);
|
||||
return 'Date invalide';
|
||||
}
|
||||
};
|
||||
|
||||
// Fonction pour calculer les différences entre deux versions
|
||||
export const calculateChanges = (
|
||||
oldRecord: Partial<EvaluationRecord>,
|
||||
newRecord: Partial<EvaluationRecord>,
|
||||
fieldsToCompare: (keyof EvaluationRecord)[]
|
||||
): FieldChange[] => {
|
||||
const changes: FieldChange[] = [];
|
||||
|
||||
for (const field of fieldsToCompare) {
|
||||
const oldValue = String(oldRecord[field] ?? '');
|
||||
const newValue = String(newRecord[field] ?? '');
|
||||
|
||||
if (oldValue !== newValue) {
|
||||
changes.push({
|
||||
field,
|
||||
fieldLabel: FIELD_LABELS[field] || field,
|
||||
oldValue: oldValue || '(vide)',
|
||||
newValue: newValue || '(vide)',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return changes;
|
||||
};
|
||||
Reference in New Issue
Block a user