Commi_GTFRH
This commit is contained in:
25
GTFRRH/project/.gitignore
vendored
Normal file
25
GTFRRH/project/.gitignore
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
.env
|
||||
28
GTFRRH/project/eslint.config.js
Normal file
28
GTFRRH/project/eslint.config.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import js from '@eslint/js';
|
||||
import globals from 'globals';
|
||||
import reactHooks from 'eslint-plugin-react-hooks';
|
||||
import reactRefresh from 'eslint-plugin-react-refresh';
|
||||
import tseslint from 'typescript-eslint';
|
||||
|
||||
export default tseslint.config(
|
||||
{ ignores: ['dist'] },
|
||||
{
|
||||
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
},
|
||||
plugins: {
|
||||
'react-hooks': reactHooks,
|
||||
'react-refresh': reactRefresh,
|
||||
},
|
||||
rules: {
|
||||
...reactHooks.configs.recommended.rules,
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
},
|
||||
}
|
||||
);
|
||||
13
GTFRRH/project/index.html
Normal file
13
GTFRRH/project/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>GTF</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
4043
GTFRRH/project/package-lock.json
generated
Normal file
4043
GTFRRH/project/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
33
GTFRRH/project/package.json
Normal file
33
GTFRRH/project/package.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "vite-react-typescript-starter",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"lucide-react": "^0.344.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.9.1",
|
||||
"@types/react": "^18.3.5",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@vitejs/plugin-react": "^4.3.1",
|
||||
"autoprefixer": "^10.4.18",
|
||||
"eslint": "^9.9.1",
|
||||
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.11",
|
||||
"globals": "^15.9.0",
|
||||
"postcss": "^8.4.35",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"typescript": "^5.5.3",
|
||||
"typescript-eslint": "^8.3.0",
|
||||
"vite": "^5.4.20"
|
||||
}
|
||||
}
|
||||
6
GTFRRH/project/postcss.config.js
Normal file
6
GTFRRH/project/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
12
GTFRRH/project/src/App.tsx
Normal file
12
GTFRRH/project/src/App.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import React from 'react';
|
||||
import RHDashboard from './components/RHDashboard';
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<RHDashboard />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
545
GTFRRH/project/src/components/Calendar.tsx
Normal file
545
GTFRRH/project/src/components/Calendar.tsx
Normal file
@@ -0,0 +1,545 @@
|
||||
import React, { useState } from 'react';
|
||||
import { ChevronLeft, ChevronRight, Clock, FileText, Users } from 'lucide-react';
|
||||
|
||||
interface TimeEntry {
|
||||
date: string;
|
||||
type: 'preparation' | 'correction';
|
||||
hours: number;
|
||||
description: string;
|
||||
}
|
||||
|
||||
const Calendar: React.FC = () => {
|
||||
const [currentDate, setCurrentDate] = useState(new Date());
|
||||
const [selectedDate, setSelectedDate] = useState<string | null>(null);
|
||||
const [timeEntries, setTimeEntries] = useState<TimeEntry[]>([]);
|
||||
const [isRHView, setIsRHView] = useState(false);
|
||||
|
||||
const monthNames = [
|
||||
'Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin',
|
||||
'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre'
|
||||
];
|
||||
|
||||
const dayNames = ['Lun', 'Mar', 'Mer', 'Jeu', 'Ven'];
|
||||
|
||||
const getDaysInMonth = (date: Date) => {
|
||||
const year = date.getFullYear();
|
||||
const month = date.getMonth();
|
||||
const firstDay = new Date(year, month, 1);
|
||||
const lastDay = new Date(year, month + 1, 0);
|
||||
const daysInMonth = lastDay.getDate();
|
||||
const startingDayOfWeek = (firstDay.getDay() + 6) % 7; // Adjust for Monday start
|
||||
|
||||
const days = [];
|
||||
|
||||
// Add empty cells for days before the first day of the month
|
||||
for (let i = 0; i < startingDayOfWeek; i++) {
|
||||
days.push(null);
|
||||
}
|
||||
|
||||
// Add all days of the month (only Monday to Friday)
|
||||
for (let day = 1; day <= daysInMonth; day++) {
|
||||
const currentDay = new Date(year, month, day);
|
||||
const dayOfWeek = currentDay.getDay();
|
||||
|
||||
// Only include Monday (1) through Friday (5)
|
||||
if (dayOfWeek >= 1 && dayOfWeek <= 5) {
|
||||
days.push(day);
|
||||
}
|
||||
}
|
||||
|
||||
return days;
|
||||
};
|
||||
|
||||
const navigateMonth = (direction: 'prev' | 'next') => {
|
||||
const newDate = new Date(currentDate);
|
||||
if (direction === 'prev') {
|
||||
newDate.setMonth(currentDate.getMonth() - 1);
|
||||
} else {
|
||||
newDate.setMonth(currentDate.getMonth() + 1);
|
||||
}
|
||||
setCurrentDate(newDate);
|
||||
};
|
||||
|
||||
const formatDateString = (day: number) => {
|
||||
const year = currentDate.getFullYear();
|
||||
const month = currentDate.getMonth();
|
||||
return `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
const hasEntry = (day: number) => {
|
||||
const dateString = formatDateString(day);
|
||||
return timeEntries.some(entry => entry.date === dateString);
|
||||
};
|
||||
|
||||
const getEntryForDate = (day: number) => {
|
||||
const dateString = formatDateString(day);
|
||||
return timeEntries.find(entry => entry.date === dateString);
|
||||
};
|
||||
|
||||
const handleDateClick = (day: number) => {
|
||||
const dateString = formatDateString(day);
|
||||
setSelectedDate(dateString);
|
||||
};
|
||||
|
||||
const days = getDaysInMonth(currentDate);
|
||||
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto p-4">
|
||||
{/* Header with view toggle */}
|
||||
<div className="flex justify-between items-center mb-8">
|
||||
<h1 className="text-3xl font-bold text-gray-800 flex items-center gap-2">
|
||||
<Clock className="text-blue-600" />
|
||||
{isRHView ? 'Vue RH - Déclarations d\'heures' : 'Déclaration d\'heures formateur'}
|
||||
</h1>
|
||||
<button
|
||||
onClick={() => setIsRHView(!isRHView)}
|
||||
className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg flex items-center gap-2 transition-colors"
|
||||
>
|
||||
{isRHView ? <FileText size={20} /> : <Users size={20} />}
|
||||
{isRHView ? 'Vue Formateur' : 'Vue RH'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{isRHView ? (
|
||||
<RHView timeEntries={timeEntries} />
|
||||
) : (
|
||||
<>
|
||||
{/* Calendar Navigation */}
|
||||
<div className="bg-white rounded-xl shadow-lg p-6 mb-6">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<button
|
||||
onClick={() => navigateMonth('prev')}
|
||||
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
|
||||
>
|
||||
<ChevronLeft className="text-gray-600" />
|
||||
</button>
|
||||
<h2 className="text-2xl font-semibold text-gray-800">
|
||||
{monthNames[currentDate.getMonth()]} {currentDate.getFullYear()}
|
||||
</h2>
|
||||
<button
|
||||
onClick={() => navigateMonth('next')}
|
||||
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
|
||||
>
|
||||
<ChevronRight className="text-gray-600" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Day Headers */}
|
||||
<div className="grid grid-cols-5 gap-2 mb-4">
|
||||
{dayNames.map(day => (
|
||||
<div key={day} className="text-center text-sm font-medium text-gray-600 py-2">
|
||||
{day}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Calendar Grid */}
|
||||
<div className="grid grid-cols-5 gap-2">
|
||||
{days.map((day, index) => {
|
||||
if (day === null) {
|
||||
return <div key={index} className="h-16"></div>;
|
||||
}
|
||||
|
||||
const entry = getEntryForDate(day);
|
||||
const dateString = formatDateString(day);
|
||||
const isToday = dateString === new Date().toISOString().split('T')[0];
|
||||
|
||||
return (
|
||||
<button
|
||||
key={day}
|
||||
onClick={() => handleDateClick(day)}
|
||||
className={`
|
||||
h-16 rounded-lg border-2 transition-all duration-200 hover:shadow-md relative
|
||||
${isToday
|
||||
? 'border-blue-500 bg-blue-50'
|
||||
: hasEntry(day)
|
||||
? 'border-green-400 bg-green-50'
|
||||
: 'border-gray-200 hover:border-gray-300 bg-white'
|
||||
}
|
||||
`}
|
||||
>
|
||||
<div className="text-sm font-medium text-gray-700">{day}</div>
|
||||
{entry && (
|
||||
<div className="absolute bottom-1 left-1 right-1">
|
||||
<div className={`text-xs px-1 py-0.5 rounded text-white ${
|
||||
entry.type === 'preparation' ? 'bg-blue-500' : 'bg-orange-500'
|
||||
}`}>
|
||||
{entry.hours}h
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Quick Stats */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
|
||||
<div className="bg-white rounded-xl shadow-lg p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-600">Heures ce mois</p>
|
||||
<p className="text-2xl font-bold text-blue-600">
|
||||
{timeEntries.reduce((sum, entry) => sum + entry.hours, 0)}h
|
||||
</p>
|
||||
</div>
|
||||
<Clock className="text-blue-600" size={32} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl shadow-lg p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-600">Préparation</p>
|
||||
<p className="text-2xl font-bold text-blue-600">
|
||||
{timeEntries.filter(e => e.type === 'preparation').reduce((sum, entry) => sum + entry.hours, 0)}h
|
||||
</p>
|
||||
</div>
|
||||
<FileText className="text-blue-600" size={32} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl shadow-lg p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-600">Correction</p>
|
||||
<p className="text-2xl font-bold text-orange-600">
|
||||
{timeEntries.filter(e => e.type === 'correction').reduce((sum, entry) => sum + entry.hours, 0)}h
|
||||
</p>
|
||||
</div>
|
||||
<FileText className="text-orange-600" size={32} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Modal Form */}
|
||||
{selectedDate && !isRHView && (
|
||||
<TimeEntryModal
|
||||
date={selectedDate}
|
||||
existingEntry={getEntryForDate(parseInt(selectedDate.split('-')[2]))}
|
||||
onClose={() => setSelectedDate(null)}
|
||||
onSave={(entry) => {
|
||||
const existingIndex = timeEntries.findIndex(e => e.date === selectedDate);
|
||||
if (existingIndex >= 0) {
|
||||
const updatedEntries = [...timeEntries];
|
||||
updatedEntries[existingIndex] = entry;
|
||||
setTimeEntries(updatedEntries);
|
||||
} else {
|
||||
setTimeEntries([...timeEntries, entry]);
|
||||
}
|
||||
setSelectedDate(null);
|
||||
}}
|
||||
onDelete={() => {
|
||||
setTimeEntries(timeEntries.filter(e => e.date !== selectedDate));
|
||||
setSelectedDate(null);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// RH View Component
|
||||
const RHView: React.FC<{ timeEntries: TimeEntry[] }> = ({ timeEntries }) => {
|
||||
const [selectedCampus, setSelectedCampus] = useState<string>('all');
|
||||
|
||||
const campuses = ['Cergy', 'Nantes', 'SQY', 'Marseille'];
|
||||
|
||||
const handleExport = () => {
|
||||
// Simulate export functionality
|
||||
const csvContent = timeEntries.map(entry =>
|
||||
`${entry.date},${entry.type},${entry.hours},${entry.description}`
|
||||
).join('\n');
|
||||
|
||||
const blob = new Blob([`Date,Type,Heures,Description\n${csvContent}`], { type: 'text/csv' });
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.setAttribute('hidden', '');
|
||||
a.setAttribute('href', url);
|
||||
a.setAttribute('download', 'declarations_heures.csv');
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Filters */}
|
||||
<div className="bg-white rounded-xl shadow-lg p-6">
|
||||
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Campus
|
||||
</label>
|
||||
<select
|
||||
value={selectedCampus}
|
||||
onChange={(e) => setSelectedCampus(e.target.value)}
|
||||
className="border border-gray-300 rounded-lg px-4 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
>
|
||||
<option value="all">Tous les campus</option>
|
||||
{campuses.map(campus => (
|
||||
<option key={campus} value={campus}>{campus}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleExport}
|
||||
className="bg-green-600 hover:bg-green-700 text-white px-6 py-2 rounded-lg transition-colors"
|
||||
>
|
||||
Exporter CSV
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Campus Stats */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{campuses.map(campus => (
|
||||
<div key={campus} className="bg-white rounded-xl shadow-lg p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-800 mb-2">{campus}</h3>
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm text-gray-600">Total: <span className="font-medium">45h</span></p>
|
||||
<p className="text-sm text-gray-600">Préparation: <span className="font-medium text-blue-600">28h</span></p>
|
||||
<p className="text-sm text-gray-600">Correction: <span className="font-medium text-orange-600">17h</span></p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Data Table */}
|
||||
<div className="bg-white rounded-xl shadow-lg overflow-hidden">
|
||||
<div className="p-6 border-b border-gray-200">
|
||||
<h3 className="text-lg font-semibold text-gray-800">Déclarations récentes</h3>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Formateur
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Campus
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Date
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Type
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Heures
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Description
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{/* Sample data */}
|
||||
<tr>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
|
||||
Jean Dupont
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
Cergy
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
15/01/2025
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
|
||||
Préparation
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||
4h
|
||||
</td>
|
||||
<td className="px-6 py-4 text-sm text-gray-500">
|
||||
Préparation cours React
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
|
||||
Marie Martin
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
Nantes
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
14/01/2025
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-orange-100 text-orange-800">
|
||||
Correction
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||
6h
|
||||
</td>
|
||||
<td className="px-6 py-4 text-sm text-gray-500">
|
||||
Correction examens PHP
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Modal Component
|
||||
interface TimeEntryModalProps {
|
||||
date: string;
|
||||
existingEntry?: TimeEntry;
|
||||
onClose: () => void;
|
||||
onSave: (entry: TimeEntry) => void;
|
||||
onDelete: () => void;
|
||||
}
|
||||
|
||||
const TimeEntryModal: React.FC<TimeEntryModalProps> = ({ date, existingEntry, onClose, onSave, onDelete }) => {
|
||||
const [type, setType] = useState<'preparation' | 'correction'>(existingEntry?.type || 'preparation');
|
||||
const [hours, setHours] = useState(existingEntry?.hours.toString() || '');
|
||||
const [description, setDescription] = useState(existingEntry?.description || '');
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!hours || parseFloat(hours) <= 0 || parseFloat(hours) > 8) {
|
||||
alert('Veuillez saisir un nombre d\'heures valide (maximum 8h par jour)');
|
||||
return;
|
||||
}
|
||||
|
||||
onSave({
|
||||
date,
|
||||
type,
|
||||
hours: parseFloat(hours),
|
||||
description: description.trim()
|
||||
});
|
||||
};
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('fr-FR', {
|
||||
weekday: 'long',
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
|
||||
<div className="bg-white rounded-xl shadow-2xl w-full max-w-md max-h-[90vh] overflow-y-auto">
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="p-6 border-b border-gray-200">
|
||||
<h3 className="text-lg font-semibold text-gray-800">
|
||||
{existingEntry ? 'Modifier la déclaration' : 'Nouvelle déclaration'}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600 mt-1 capitalize">
|
||||
{formatDate(date)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="p-6 space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Type d'activité *
|
||||
</label>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setType('preparation')}
|
||||
className={`p-3 rounded-lg border-2 transition-all ${
|
||||
type === 'preparation'
|
||||
? 'border-blue-500 bg-blue-50 text-blue-700'
|
||||
: 'border-gray-200 hover:border-gray-300'
|
||||
}`}
|
||||
>
|
||||
<div className="text-center">
|
||||
<FileText className="mx-auto mb-1" size={20} />
|
||||
<div className="text-sm font-medium">Préparation</div>
|
||||
<div className="text-xs text-gray-500">& Recherche</div>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setType('correction')}
|
||||
className={`p-3 rounded-lg border-2 transition-all ${
|
||||
type === 'correction'
|
||||
? 'border-orange-500 bg-orange-50 text-orange-700'
|
||||
: 'border-gray-200 hover:border-gray-300'
|
||||
}`}
|
||||
>
|
||||
<div className="text-center">
|
||||
<FileText className="mx-auto mb-1" size={20} />
|
||||
<div className="text-sm font-medium">Correction</div>
|
||||
<div className="text-xs text-gray-500">d'examens</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Nombre d'heures *
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
min="0.5"
|
||||
max="8"
|
||||
step="0.5"
|
||||
value={hours}
|
||||
onChange={(e) => setHours(e.target.value)}
|
||||
className="w-full border border-gray-300 rounded-lg px-4 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
placeholder="Ex: 4"
|
||||
required
|
||||
/>
|
||||
<p className="text-xs text-gray-500 mt-1">Maximum 8 heures par jour</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Description
|
||||
</label>
|
||||
<textarea
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
rows={3}
|
||||
className="w-full border border-gray-300 rounded-lg px-4 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-none"
|
||||
placeholder="Décrivez brièvement l'activité réalisée..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-6 border-t border-gray-200 flex flex-col sm:flex-row gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="flex-1 px-4 py-2 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
Annuler
|
||||
</button>
|
||||
{existingEntry && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onDelete}
|
||||
className="flex-1 px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors"
|
||||
>
|
||||
Supprimer
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="submit"
|
||||
className="flex-1 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
{existingEntry ? 'Modifier' : 'Enregistrer'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Calendar;
|
||||
525
GTFRRH/project/src/components/RHDashboard.tsx
Normal file
525
GTFRRH/project/src/components/RHDashboard.tsx
Normal file
@@ -0,0 +1,525 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Users, Download, Calendar, Clock, FileText, Filter } from 'lucide-react';
|
||||
|
||||
interface TimeEntry {
|
||||
id: string;
|
||||
formateur: string;
|
||||
campus: string;
|
||||
date: string;
|
||||
type: 'preparation' | 'correction';
|
||||
hours: number;
|
||||
description: string;
|
||||
status: 'pending' | 'approved' | 'rejected';
|
||||
}
|
||||
|
||||
const RHDashboard: React.FC = () => {
|
||||
const [selectedCampus, setSelectedCampus] = useState<string>('all');
|
||||
const [selectedMonth, setSelectedMonth] = useState<string>('2025-01');
|
||||
const [selectedFormateur, setSelectedFormateur] = useState<string>('all');
|
||||
const [showFormateursList, setShowFormateursList] = useState(false);
|
||||
|
||||
const campuses = ['Cergy', 'Nantes', 'SQY', 'Marseille'];
|
||||
|
||||
// Liste complète des formateurs par campus
|
||||
const formateursByCampus = {
|
||||
'Cergy': [
|
||||
'Jean Dupont', 'Marie Dubois', 'Pierre Martin', 'Sophie Leroy',
|
||||
'Antoine Bernard', 'Claire Moreau', 'Nicolas Petit', 'Isabelle Roux'
|
||||
],
|
||||
'Nantes': [
|
||||
'Marie Martin', 'Thomas Durand', 'Julie Blanc', 'François Simon',
|
||||
'Camille Garnier', 'Olivier Faure', 'Nathalie Girard', 'Julien Morel'
|
||||
],
|
||||
'SQY': [
|
||||
'Pierre Durand', 'Sandrine Lefebvre', 'Maxime Rousseau', 'Émilie Mercier',
|
||||
'Sébastien Fournier', 'Valérie Bonnet', 'Christophe Lambert', 'Aurélie Muller'
|
||||
],
|
||||
'Marseille': [
|
||||
'Sophie Leroy', 'David Fontaine', 'Laure Chevalier', 'Romain Gauthier',
|
||||
'Céline Masson', 'Fabrice Dupuis', 'Stéphanie Roy', 'Alexandre Perrin'
|
||||
]
|
||||
};
|
||||
|
||||
// Données d'exemple
|
||||
const timeEntries: TimeEntry[] = [
|
||||
{
|
||||
id: '1',
|
||||
formateur: 'Jean Dupont',
|
||||
campus: 'Cergy',
|
||||
date: '2025-01-15',
|
||||
type: 'preparation',
|
||||
hours: 4,
|
||||
description: 'Préparation cours React avancé',
|
||||
status: 'approved'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
formateur: 'Marie Martin',
|
||||
campus: 'Nantes',
|
||||
date: '2025-01-14',
|
||||
type: 'correction',
|
||||
hours: 6,
|
||||
description: 'Correction examens PHP',
|
||||
status: 'pending'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
formateur: 'Pierre Durand',
|
||||
campus: 'SQY',
|
||||
date: '2025-01-13',
|
||||
type: 'preparation',
|
||||
hours: 3.5,
|
||||
description: 'Recherche documentation Node.js',
|
||||
status: 'approved'
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
formateur: 'Sophie Leroy',
|
||||
campus: 'Marseille',
|
||||
date: '2025-01-12',
|
||||
type: 'correction',
|
||||
hours: 5,
|
||||
description: 'Correction projets JavaScript',
|
||||
status: 'approved'
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
formateur: 'Jean Dupont',
|
||||
campus: 'Cergy',
|
||||
date: '2025-01-11',
|
||||
type: 'preparation',
|
||||
hours: 2,
|
||||
description: 'Préparation TP bases de données',
|
||||
status: 'pending'
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
formateur: 'Marie Martin',
|
||||
campus: 'Nantes',
|
||||
date: '2025-01-10',
|
||||
type: 'preparation',
|
||||
hours: 4.5,
|
||||
description: 'Préparation cours architecture logicielle',
|
||||
status: 'approved'
|
||||
},
|
||||
{
|
||||
id: '7',
|
||||
formateur: 'Thomas Durand',
|
||||
campus: 'Nantes',
|
||||
date: '2025-01-09',
|
||||
type: 'preparation',
|
||||
hours: 3,
|
||||
description: 'Préparation cours DevOps',
|
||||
status: 'approved'
|
||||
},
|
||||
{
|
||||
id: '8',
|
||||
formateur: 'Sandrine Lefebvre',
|
||||
campus: 'SQY',
|
||||
date: '2025-01-08',
|
||||
type: 'correction',
|
||||
hours: 4,
|
||||
description: 'Correction projets Angular',
|
||||
status: 'pending'
|
||||
},
|
||||
{
|
||||
id: '9',
|
||||
formateur: 'David Fontaine',
|
||||
campus: 'Marseille',
|
||||
date: '2025-01-07',
|
||||
type: 'preparation',
|
||||
hours: 2.5,
|
||||
description: 'Recherche nouvelles technologies',
|
||||
status: 'approved'
|
||||
}
|
||||
];
|
||||
|
||||
// Filtrage des données
|
||||
const filteredEntries = timeEntries.filter(entry => {
|
||||
const campusMatch = selectedCampus === 'all' || entry.campus === selectedCampus;
|
||||
const formateurMatch = selectedFormateur === 'all' || entry.formateur === selectedFormateur;
|
||||
const monthMatch = entry.date.startsWith(selectedMonth);
|
||||
return campusMatch && formateurMatch && monthMatch;
|
||||
});
|
||||
|
||||
// Liste des formateurs uniques
|
||||
const allFormateurs = Object.values(formateursByCampus).flat();
|
||||
const formateurs = Array.from(new Set(allFormateurs));
|
||||
|
||||
// Statistiques par campus
|
||||
const getStatsForCampus = (campus: string) => {
|
||||
const campusEntries = timeEntries.filter(entry => entry.campus === campus);
|
||||
const totalHours = campusEntries.reduce((sum, entry) => sum + entry.hours, 0);
|
||||
const preparationHours = campusEntries
|
||||
.filter(entry => entry.type === 'preparation')
|
||||
.reduce((sum, entry) => sum + entry.hours, 0);
|
||||
const correctionHours = campusEntries
|
||||
.filter(entry => entry.type === 'correction')
|
||||
.reduce((sum, entry) => sum + entry.hours, 0);
|
||||
|
||||
return { totalHours, preparationHours, correctionHours };
|
||||
};
|
||||
|
||||
const handleExport = () => {
|
||||
const csvHeaders = ['Date', 'Formateur', 'Campus', 'Type', 'Heures', 'Description', 'Statut'];
|
||||
const csvData = filteredEntries.map(entry => [
|
||||
new Date(entry.date).toLocaleDateString('fr-FR'),
|
||||
entry.formateur,
|
||||
entry.campus,
|
||||
entry.type === 'preparation' ? 'Préparation' : 'Correction',
|
||||
entry.hours.toString(),
|
||||
entry.description,
|
||||
entry.status === 'approved' ? 'Approuvé' : entry.status === 'pending' ? 'En attente' : 'Rejeté'
|
||||
]);
|
||||
|
||||
const csvContent = [csvHeaders, ...csvData]
|
||||
.map(row => row.map(cell => `"${cell}"`).join(','))
|
||||
.join('\n');
|
||||
|
||||
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
||||
const link = document.createElement('a');
|
||||
const url = URL.createObjectURL(blob);
|
||||
link.setAttribute('href', url);
|
||||
link.setAttribute('download', `declarations_heures_${selectedMonth}.csv`);
|
||||
link.style.visibility = 'hidden';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
};
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
return new Date(dateString).toLocaleDateString('fr-FR', {
|
||||
weekday: 'long',
|
||||
day: 'numeric',
|
||||
month: 'long'
|
||||
});
|
||||
};
|
||||
|
||||
const getStatusBadge = (status: string) => {
|
||||
const statusConfig = {
|
||||
approved: { bg: 'bg-green-100', text: 'text-green-800', label: 'Approuvé' },
|
||||
pending: { bg: 'bg-yellow-100', text: 'text-yellow-800', label: 'En attente' },
|
||||
rejected: { bg: 'bg-red-100', text: 'text-red-800', label: 'Rejeté' }
|
||||
};
|
||||
|
||||
const config = statusConfig[status as keyof typeof statusConfig];
|
||||
return (
|
||||
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${config.bg} ${config.text}`}>
|
||||
{config.label}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const handleStatusChange = (entryId: string, newStatus: 'approved' | 'rejected') => {
|
||||
|
||||
console.log(`Changement de statut pour l'entrée ${entryId}: ${newStatus}`);
|
||||
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 p-4">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold text-gray-800 flex items-center gap-3">
|
||||
<Users className="text-blue-600" />
|
||||
Vue RH - GTF
|
||||
</h1>
|
||||
<p className="text-gray-600 mt-2">
|
||||
Gestion et suivi des déclarations
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Filtres */}
|
||||
<div className="bg-white rounded-xl shadow-lg p-6 mb-6">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<Filter className="text-gray-600" size={20} />
|
||||
<h2 className="text-lg font-semibold text-gray-800">Filtres</h2>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Campus
|
||||
</label>
|
||||
<select
|
||||
value={selectedCampus}
|
||||
onChange={(e) => setSelectedCampus(e.target.value)}
|
||||
className="w-full border border-gray-300 rounded-lg px-4 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
>
|
||||
<option value="all">Tous les campus</option>
|
||||
{campuses.map(campus => (
|
||||
<option key={campus} value={campus}>{campus}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Formateur
|
||||
</label>
|
||||
<select
|
||||
value={selectedFormateur}
|
||||
onChange={(e) => setSelectedFormateur(e.target.value)}
|
||||
className="w-full border border-gray-300 rounded-lg px-4 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
>
|
||||
<option value="all">Tous les formateurs</option>
|
||||
{formateurs.map(formateur => (
|
||||
<option key={formateur} value={formateur}>{formateur}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Mois
|
||||
</label>
|
||||
<input
|
||||
type="month"
|
||||
value={selectedMonth}
|
||||
onChange={(e) => setSelectedMonth(e.target.value)}
|
||||
className="w-full border border-gray-300 rounded-lg px-4 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-end">
|
||||
<button
|
||||
onClick={handleExport}
|
||||
className="w-full bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-lg flex items-center justify-center gap-2 transition-colors"
|
||||
>
|
||||
<Download size={20} />
|
||||
Exporter CSV
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Statistiques par campus */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
|
||||
{campuses.map(campus => {
|
||||
const stats = getStatsForCampus(campus);
|
||||
const campusFormateurs = formateursByCampus[campus as keyof typeof formateursByCampus];
|
||||
return (
|
||||
<div key={campus} className="bg-white rounded-xl shadow-lg p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold text-gray-800">{campus}</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-gray-500">{campusFormateurs.length} formateurs</span>
|
||||
<div className="w-3 h-3 rounded-full bg-blue-500"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-gray-600">Total:</span>
|
||||
<span className="font-semibold text-gray-800">{stats.totalHours}h</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-blue-600">Préparation:</span>
|
||||
<span className="font-medium text-blue-600">{stats.preparationHours}h</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-orange-600">Correction:</span>
|
||||
<span className="font-medium text-orange-600">{stats.correctionHours}h</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowFormateursList(!showFormateursList)}
|
||||
className="mt-3 text-xs text-blue-600 hover:text-blue-800 underline"
|
||||
>
|
||||
Voir les formateurs
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Liste des formateurs par campus */}
|
||||
{showFormateursList && (
|
||||
<div className="bg-white rounded-xl shadow-lg p-6 mb-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold text-gray-800 flex items-center gap-2">
|
||||
<Users className="text-blue-600" />
|
||||
Formateurs par campus
|
||||
</h3>
|
||||
<button
|
||||
onClick={() => setShowFormateursList(false)}
|
||||
className="text-gray-500 hover:text-gray-700"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
{campuses.map(campus => {
|
||||
const campusFormateurs = formateursByCampus[campus as keyof typeof formateursByCampus];
|
||||
return (
|
||||
<div key={campus} className="border border-gray-200 rounded-lg p-4">
|
||||
<h4 className="font-semibold text-gray-800 mb-3 flex items-center gap-2">
|
||||
<div className="w-2 h-2 rounded-full bg-blue-500"></div>
|
||||
{campus}
|
||||
</h4>
|
||||
<div className="space-y-2">
|
||||
{campusFormateurs.map(formateur => {
|
||||
const formateurEntries = timeEntries.filter(entry =>
|
||||
entry.formateur === formateur && entry.campus === campus
|
||||
);
|
||||
const totalHours = formateurEntries.reduce((sum, entry) => sum + entry.hours, 0);
|
||||
|
||||
return (
|
||||
<div key={formateur} className="flex justify-between items-center py-1">
|
||||
<span className="text-sm text-gray-700">{formateur}</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-gray-500">{totalHours}h</span>
|
||||
<div className={`w-2 h-2 rounded-full ${
|
||||
totalHours > 0 ? 'bg-green-400' : 'bg-gray-300'
|
||||
}`}></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Résumé des résultats */}
|
||||
<div className="bg-white rounded-xl shadow-lg p-6 mb-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-gray-800">
|
||||
Résultats filtrés
|
||||
</h3>
|
||||
<p className="text-gray-600">
|
||||
{filteredEntries.length} déclaration(s) trouvée(s)
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-2xl font-bold text-blue-600">
|
||||
{filteredEntries.reduce((sum, entry) => sum + entry.hours, 0)}h
|
||||
</p>
|
||||
<p className="text-sm text-gray-600">Total des heures</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tableau des déclarations */}
|
||||
<div className="bg-white rounded-xl shadow-lg overflow-hidden">
|
||||
<div className="p-6 border-b border-gray-200">
|
||||
<h3 className="text-lg font-semibold text-gray-800 flex items-center gap-2">
|
||||
<Calendar className="text-blue-600" />
|
||||
Déclarations d'heures
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Date
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Formateur
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Campus
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Type
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Heures
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Description
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Statut
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{filteredEntries.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={7} className="px-6 py-8 text-center text-gray-500">
|
||||
Aucune déclaration trouvée pour les critères sélectionnés
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
filteredEntries.map((entry) => (
|
||||
<tr key={entry.id} className="hover:bg-gray-50">
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||
{formatDate(entry.date)}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
|
||||
{entry.formateur}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{entry.campus}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
|
||||
entry.type === 'preparation'
|
||||
? 'bg-blue-100 text-blue-800'
|
||||
: 'bg-orange-100 text-orange-800'
|
||||
}`}>
|
||||
<div className="flex items-center gap-1">
|
||||
{entry.type === 'preparation' ? (
|
||||
<FileText size={12} />
|
||||
) : (
|
||||
<Clock size={12} />
|
||||
)}
|
||||
{entry.type === 'preparation' ? 'Préparation' : 'Correction'}
|
||||
</div>
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-semibold text-gray-900">
|
||||
{entry.hours}h
|
||||
</td>
|
||||
<td className="px-6 py-4 text-sm text-gray-500 max-w-xs truncate">
|
||||
{entry.description}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
{getStatusBadge(entry.status)}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||
{entry.status === 'pending' ? (
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => handleStatusChange(entry.id, 'approved')}
|
||||
className="bg-green-600 hover:bg-green-700 text-white px-3 py-1 rounded text-xs transition-colors"
|
||||
>
|
||||
Approuver
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleStatusChange(entry.id, 'rejected')}
|
||||
className="bg-red-600 hover:bg-red-700 text-white px-3 py-1 rounded text-xs transition-colors"
|
||||
>
|
||||
Rejeter
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-gray-400 text-xs">
|
||||
{entry.status === 'approved' ? 'Approuvé' : 'Rejeté'}
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RHDashboard;
|
||||
3
GTFRRH/project/src/index.css
Normal file
3
GTFRRH/project/src/index.css
Normal file
@@ -0,0 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
10
GTFRRH/project/src/main.tsx
Normal file
10
GTFRRH/project/src/main.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import { StrictMode } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import App from './App.tsx';
|
||||
import './index.css';
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>
|
||||
);
|
||||
1
GTFRRH/project/src/vite-env.d.ts
vendored
Normal file
1
GTFRRH/project/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
8
GTFRRH/project/tailwind.config.js
Normal file
8
GTFRRH/project/tailwind.config.js
Normal file
@@ -0,0 +1,8 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
24
GTFRRH/project/tsconfig.app.json
Normal file
24
GTFRRH/project/tsconfig.app.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
7
GTFRRH/project/tsconfig.json
Normal file
7
GTFRRH/project/tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
22
GTFRRH/project/tsconfig.node.json
Normal file
22
GTFRRH/project/tsconfig.node.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2023"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
10
GTFRRH/project/vite.config.ts
Normal file
10
GTFRRH/project/vite.config.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
optimizeDeps: {
|
||||
exclude: ['lucide-react'],
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user