feat: Add DetailPage and HomePage components with KPI visualization

- Implemented DetailPage for detailed KPI analysis including charts and status badges.
- Created HomePage to display an overview of KPIs categorized by security, quality, delays, costs, and maintenance.
- Introduced KPI types and data structures for better type safety.
- Added styles for DetailPage, HomePage, KPICard, and charts for improved UI.
- Integrated web vitals reporting and setup tests for better performance tracking and testing.
- Included a CSV file with mathematical formulas for KPI calculations.
This commit is contained in:
paul.roost 2025-10-21 12:23:45 +02:00
parent aa13c49772
commit 5ecda7eef7
31 changed files with 19870 additions and 0 deletions

23
dashboard-sqdc/.gitignore vendored Normal file
View File

@ -0,0 +1,23 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*

46
dashboard-sqdc/README.md Normal file
View File

@ -0,0 +1,46 @@
# Getting Started with Create React App
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
## Available Scripts
In the project directory, you can run:
### `npm start`
Runs the app in the development mode.\
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
The page will reload if you make edits.\
You will also see any lint errors in the console.
### `npm test`
Launches the test runner in the interactive watch mode.\
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
### `npm run build`
Builds the app for production to the `build` folder.\
It correctly bundles React in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.\
Your app is ready to be deployed!
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
### `npm run eject`
**Note: this is a one-way operation. Once you `eject`, you cant go back!**
If you arent satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point youre on your own.
You dont have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldnt feel obligated to use this feature. However we understand that this tool wouldnt be useful if you couldnt customize it when you are ready for it.
## Learn More
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
To learn React, check out the [React documentation](https://reactjs.org/).

17628
dashboard-sqdc/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,47 @@
{
"name": "dashboard-sqdc",
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^13.5.0",
"@types/jest": "^27.5.2",
"@types/node": "^16.18.126",
"@types/react": "^19.2.2",
"@types/react-dom": "^19.2.2",
"chart.js": "^4.5.1",
"lucide-react": "^0.546.0",
"react": "^19.2.0",
"react-chartjs-2": "^5.3.0",
"react-dom": "^19.2.0",
"react-scripts": "5.0.1",
"typescript": "^4.9.5",
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@ -0,0 +1,43 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

@ -0,0 +1,25 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

View File

@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

145
dashboard-sqdc/src/App.css Normal file
View File

@ -0,0 +1,145 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: #f5f7fa;
}
.app {
display: flex;
flex-direction: column;
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
/* HEADER */
.app-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 2rem;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.header-content {
max-width: 1400px;
margin: 0 auto;
display: flex;
justify-content: space-between;
align-items: center;
}
.header-title h1 {
font-size: 2rem;
margin-bottom: 0.5rem;
}
.header-title p {
opacity: 0.9;
font-size: 0.95rem;
}
.header-date {
font-size: 0.9rem;
opacity: 0.8;
}
/* NAVIGATION */
.app-nav {
display: flex;
gap: 0;
background: white;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
overflow-x: auto;
position: sticky;
top: 0;
z-index: 100;
}
.nav-btn {
flex: 1;
min-width: 120px;
padding: 1rem;
border: none;
background: white;
color: #333;
font-size: 0.95rem;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
border-bottom: 3px solid transparent;
}
.nav-btn:hover {
background: #f5f7fa;
}
.nav-btn.active {
background: white;
color: #667eea;
border-bottom-color: #667eea;
}
/* CONTENT */
.app-content {
flex: 1;
max-width: 1400px;
width: 100%;
margin: 0 auto;
padding: 2rem 1rem;
}
/* CHARTS PAGE */
.charts-page {
color: white;
}
.charts-header {
margin-bottom: 2rem;
}
.charts-header h1 {
font-size: 2rem;
margin-bottom: 0.5rem;
}
.charts-header p {
opacity: 0.9;
}
.charts-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(500px, 1fr));
gap: 2rem;
}
/* FOOTER */
.app-footer {
background: rgba(0, 0, 0, 0.2);
color: white;
text-align: center;
padding: 1.5rem;
margin-top: auto;
font-size: 0.9rem;
}
/* RESPONSIVE */
@media (max-width: 768px) {
.header-content {
flex-direction: column;
gap: 1rem;
}
.nav-btn {
min-width: 100px;
padding: 0.75rem;
font-size: 0.85rem;
}
.charts-grid {
grid-template-columns: 1fr;
}
}

View File

@ -0,0 +1,9 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});

125
dashboard-sqdc/src/App.tsx Normal file
View File

@ -0,0 +1,125 @@
import React, { useState } from 'react';
import { HomePage } from './pages/HomePage';
import { DetailPage } from './pages/DetailPage';
import { TrendChart, CategoryDistributionChart, StatusChart, CNQChart } from './components/Charts';
import { kpiData } from './data/kpiData';
import './App.css';
type TabType = 'home' | 'security' | 'quality' | 'delays' | 'costs' | 'maintenance' | 'charts';
function App() {
const [activeTab, setActiveTab] = useState<TabType>('home');
return (
<div className="app">
<header className="app-header">
<div className="header-content">
<div className="header-title">
<h1>📊 Dashboard SQDC</h1>
<p>Indicateurs de Performance - Sécurité, Qualité, Délais, Coûts</p>
</div>
<div className="header-date">
{new Date().toLocaleDateString('fr-FR', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric'
})}
</div>
</div>
</header>
<nav className="app-nav">
<button
className={`nav-btn ${activeTab === 'home' ? 'active' : ''}`}
onClick={() => setActiveTab('home')}
>
🏠 Accueil
</button>
<button
className={`nav-btn ${activeTab === 'security' ? 'active' : ''}`}
onClick={() => setActiveTab('security')}
>
🛡 Sécurité
</button>
<button
className={`nav-btn ${activeTab === 'quality' ? 'active' : ''}`}
onClick={() => setActiveTab('quality')}
>
🎯 Qualité
</button>
<button
className={`nav-btn ${activeTab === 'delays' ? 'active' : ''}`}
onClick={() => setActiveTab('delays')}
>
Délais
</button>
<button
className={`nav-btn ${activeTab === 'costs' ? 'active' : ''}`}
onClick={() => setActiveTab('costs')}
>
💰 Coûts
</button>
<button
className={`nav-btn ${activeTab === 'maintenance' ? 'active' : ''}`}
onClick={() => setActiveTab('maintenance')}
>
🔧 Maintenance
</button>
<button
className={`nav-btn ${activeTab === 'charts' ? 'active' : ''}`}
onClick={() => setActiveTab('charts')}
>
📈 Analyses
</button>
</nav>
<main className="app-content">
{activeTab === 'home' && (
<HomePage kpiData={kpiData} />
)}
{activeTab === 'security' && (
<DetailPage category="security" kpis={kpiData.security} />
)}
{activeTab === 'quality' && (
<DetailPage category="quality" kpis={kpiData.quality} />
)}
{activeTab === 'delays' && (
<DetailPage category="delays" kpis={kpiData.delays} />
)}
{activeTab === 'costs' && (
<DetailPage category="costs" kpis={kpiData.costs} />
)}
{activeTab === 'maintenance' && (
<DetailPage category="maintenance" kpis={kpiData.maintenance} />
)}
{activeTab === 'charts' && (
<div className="charts-page">
<div className="charts-header">
<h1>📈 Analyses et Tendances</h1>
<p>Vue globale des tendances et des analyses comparatives</p>
</div>
<div className="charts-grid">
<TrendChart />
<CategoryDistributionChart />
<StatusChart />
<CNQChart />
</div>
</div>
)}
</main>
<footer className="app-footer">
<p>Dashboard SQDC | Dernière mise à jour: {new Date().toLocaleTimeString('fr-FR')} | Données en temps réel</p>
</footer>
</div>
);
}
export default App;

View File

@ -0,0 +1,197 @@
import React from 'react';
import { Line, Doughnut, Bar } from 'react-chartjs-2';
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
PointElement,
LineElement,
BarElement,
ArcElement,
Tooltip,
Legend
} from 'chart.js';
import { kpiData, getCategoryColor } from '../data/kpiData';
import '../styles/Charts.css';
ChartJS.register(
CategoryScale,
LinearScale,
PointElement,
LineElement,
BarElement,
ArcElement,
Tooltip,
Legend
);
export const TrendChart: React.FC = () => {
const data = {
labels: ['Semaine 1', 'Semaine 2', 'Semaine 3', 'Semaine 4'],
datasets: [
{
label: 'Sécurité (%)',
data: [94, 95, 96, 96],
borderColor: getCategoryColor('security'),
backgroundColor: 'rgba(231, 76, 60, 0.1)',
tension: 0.4
},
{
label: 'Qualité (%)',
data: [75, 76, 77, 78.5],
borderColor: getCategoryColor('quality'),
backgroundColor: 'rgba(52, 152, 219, 0.1)',
tension: 0.4
},
{
label: 'Délais (%)',
data: [97, 97.5, 97.8, 98],
borderColor: getCategoryColor('delays'),
backgroundColor: 'rgba(243, 156, 18, 0.1)',
tension: 0.4
},
{
label: 'Coûts (%)',
data: [92, 91, 90.5, 89],
borderColor: getCategoryColor('costs'),
backgroundColor: 'rgba(39, 174, 96, 0.1)',
tension: 0.4
}
]
};
return (
<div className="chart-container">
<h2>📈 Tendances SQDC</h2>
<Line data={data} options={{
responsive: true,
maintainAspectRatio: false,
plugins: { legend: { position: 'top' as const } },
scales: { y: { beginAtZero: true, max: 100 } }
}} />
</div>
);
};
export const CategoryDistributionChart: React.FC = () => {
const categoryCounts = {
'Sécurité': kpiData.security.length,
'Qualité': kpiData.quality.length,
'Délais': kpiData.delays.length,
'Coûts': kpiData.costs.length,
'Maintenance': kpiData.maintenance.length
};
const data = {
labels: Object.keys(categoryCounts),
datasets: [{
data: Object.values(categoryCounts),
backgroundColor: [
getCategoryColor('security'),
getCategoryColor('quality'),
getCategoryColor('delays'),
getCategoryColor('costs'),
getCategoryColor('maintenance')
],
borderColor: 'white',
borderWidth: 2
}]
};
return (
<div className="chart-container">
<h2>📊 Répartition KPI par Catégorie</h2>
<Doughnut data={data} options={{
responsive: true,
maintainAspectRatio: false,
plugins: { legend: { position: 'bottom' as const } }
}} />
</div>
);
};
export const StatusChart: React.FC = () => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const allKPIs = [
...kpiData.security,
...kpiData.quality,
...kpiData.delays,
...kpiData.costs,
...kpiData.maintenance
];
const categories = ['Sécurité', 'Qualité', 'Délais', 'Coûts', 'Maintenance'];
const categoryKPIs = [
kpiData.security,
kpiData.quality,
kpiData.delays,
kpiData.costs,
kpiData.maintenance
];
const data = {
labels: categories,
datasets: [
{
label: 'Bon',
data: categoryKPIs.map(cat => cat.filter(k => k.status === 'good').length),
backgroundColor: '#27ae60'
},
{
label: 'À améliorer',
data: categoryKPIs.map(cat => cat.filter(k => k.status === 'warning').length),
backgroundColor: '#f39c12'
},
{
label: 'Critique',
data: categoryKPIs.map(cat => cat.filter(k => k.status === 'critical').length),
backgroundColor: '#e74c3c'
}
]
};
return (
<div className="chart-container">
<h2>📊 État des KPI par Catégorie</h2>
<Bar data={data} options={{
responsive: true,
maintainAspectRatio: false,
indexAxis: 'y' as const,
plugins: { legend: { position: 'top' as const } },
scales: {
x: { stacked: true },
y: { stacked: true }
}
}} />
</div>
);
};
export const CNQChart: React.FC = () => {
const data = {
labels: ['Rebuts', 'Retouches', 'Retours Clients'],
datasets: [{
label: 'Coût (€)',
data: [8500, 7200, 2800],
backgroundColor: [
'#e74c3c',
'#f39c12',
'#3498db'
],
borderColor: 'white',
borderWidth: 2
}]
};
return (
<div className="chart-container">
<h2>💔 Coûts des Non-Qualité</h2>
<Bar data={data} options={{
responsive: true,
maintainAspectRatio: false,
plugins: { legend: { display: false } },
scales: { y: { beginAtZero: true } }
}} />
</div>
);
};

View File

@ -0,0 +1,67 @@
import React from 'react';
import { KPI } from '../types';
import '../styles/KPICard.css';
interface KPICardProps {
kpi: KPI;
color: string;
}
export const KPICard: React.FC<KPICardProps> = ({ kpi, color }) => {
const getStatusIcon = () => {
switch (kpi.status) {
case 'good':
return '✓';
case 'warning':
return '⚠';
case 'critical':
return '🔴';
default:
return '•';
}
};
const getTrendIcon = () => {
switch (kpi.trend) {
case 'up':
return '📈';
case 'down':
return '📉';
case 'stable':
return '➡️';
default:
return '•';
}
};
const getStatusClass = () => {
return `status-${kpi.status}`;
};
return (
<div className="kpi-card" style={{ borderLeftColor: color }}>
<div className="kpi-header" style={{ color }}>
<h3 className="kpi-title">{kpi.name}</h3>
<span className="kpi-trend">{getTrendIcon()}</span>
</div>
<div className="kpi-value-section">
<div className="kpi-value">{kpi.value}</div>
<div className="kpi-unit">{kpi.unit}</div>
</div>
<div className="kpi-footer">
<span className={`kpi-status ${getStatusClass()}`}>
{getStatusIcon()} {kpi.status.charAt(0).toUpperCase() + kpi.status.slice(1)}
</span>
{kpi.target && (
<span className="kpi-target">
Objectif: {kpi.target} {kpi.unit}
</span>
)}
</div>
<p className="kpi-description">{kpi.description}</p>
</div>
);
};

View File

@ -0,0 +1,342 @@
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { KPI, CategoryData } from '../types';
export const kpiData: CategoryData = {
security: [
{
id: 'tf',
name: 'Taux de Fréquence',
value: 0.5,
unit: 'par 1M heures',
category: 'security',
target: 1.0,
status: 'good',
trend: 'down',
description: 'Mesurer la fréquence des accidents avec arrêt.',
formula: '(Nombre d\'Accidents avec Arrêt / Nombre d\'Heures Travaillées) × 1000000',
data: [1.2, 0.9, 0.7, 0.5],
labels: ['Semaine 1', 'Semaine 2', 'Semaine 3', 'Semaine 4']
},
{
id: 'incidents',
name: 'Incidents/Near Miss',
value: 12,
unit: 'incidents',
category: 'security',
target: 8,
status: 'warning',
trend: 'up',
description: 'Évaluer la culture de sécurité et la proactivité.',
formula: 'Compte des rapports d\'incidents (sans blessure/dommage)',
data: [8, 10, 11, 12],
labels: ['Semaine 1', 'Semaine 2', 'Semaine 3', 'Semaine 4']
},
{
id: 'audit_compliance',
name: 'Conformité Audits',
value: 96,
unit: '%',
category: 'security',
target: 95,
status: 'good',
trend: 'up',
description: 'Mesurer le respect des procédures de sécurité.',
formula: '(Points de Contrôle Conformes / Total Points de Contrôle) × 100',
data: [93, 94, 95, 96],
labels: ['Semaine 1', 'Semaine 2', 'Semaine 3', 'Semaine 4']
}
],
quality: [
{
id: 'scrap_rate',
name: 'Taux de Rebut',
value: 2.1,
unit: '%',
category: 'quality',
target: 1.5,
status: 'warning',
trend: 'down',
description: 'Mesurer le pourcentage d\'unités jetées (irréparables).',
formula: '(Nombre d\'Unités Rebutées / Nombre Total d\'Unités Produites) × 100',
data: [3.2, 2.8, 2.4, 2.1],
labels: ['Semaine 1', 'Semaine 2', 'Semaine 3', 'Semaine 4']
},
{
id: 'rework_rate',
name: 'Taux de Retouche',
value: 3.8,
unit: '%',
category: 'quality',
target: 2.0,
status: 'warning',
trend: 'down',
description: 'Mesurer le pourcentage d\'unités nécessitant une reprise.',
formula: '(Nombre d\'Unités Retouchées / Nombre Total d\'Unités Produites) × 100',
data: [5.2, 4.6, 4.2, 3.8],
labels: ['Semaine 1', 'Semaine 2', 'Semaine 3', 'Semaine 4']
},
{
id: 'dpu',
name: 'Défauts par Unité (DPU)',
value: 0.45,
unit: 'défauts/unité',
category: 'quality',
target: 0.5,
status: 'good',
trend: 'down',
description: 'Mesurer le nombre moyen de défauts par produit.',
formula: 'Nombre Total de Défauts Trouvés / Nombre Total d\'Unités Inspectées',
data: [0.68, 0.58, 0.52, 0.45],
labels: ['Semaine 1', 'Semaine 2', 'Semaine 3', 'Semaine 4']
},
{
id: 'return_rate',
name: 'Taux de Retours Clients',
value: 1.2,
unit: '%',
category: 'quality',
target: 0.8,
status: 'good',
trend: 'down',
description: 'Mesurer l\'impact de la non-qualité chez le client.',
formula: '(Nombre d\'Unités Retournées / Nombre Total d\'Unités Vendues) × 100',
data: [2.1, 1.8, 1.5, 1.2],
labels: ['Semaine 1', 'Semaine 2', 'Semaine 3', 'Semaine 4']
},
{
id: 'trs',
name: 'TRS (Rendement Synthétique)',
value: 78.5,
unit: '%',
category: 'quality',
target: 85,
status: 'warning',
trend: 'up',
description: 'Rendement global de la ligne de production.',
formula: 'Nb pièces bonnes × Temps cycle / Temps d\'ouverture',
data: [75.2, 76.5, 77.8, 78.5],
labels: ['Semaine 1', 'Semaine 2', 'Semaine 3', 'Semaine 4']
},
{
id: 'oee',
name: 'OEE (Overall Equipment Effectiveness)',
value: 72.3,
unit: '%',
category: 'quality',
target: 80,
status: 'warning',
trend: 'up',
description: 'Efficacité combinée (Disponibilité, Performance, Qualité).',
formula: 'Disponibilité × Performance × Qualité',
data: [68.5, 70.2, 71.5, 72.3],
labels: ['Semaine 1', 'Semaine 2', 'Semaine 3', 'Semaine 4']
}
],
delays: [
{
id: 'schedule_adherence',
name: 'Respect du Plan',
value: 98,
unit: '%',
category: 'delays',
target: 95,
status: 'good',
trend: 'up',
description: 'Mesurer la capacité à atteindre le volume planifié.',
formula: '(Quantité Réellement Produite / Quantité Planifiée) × 100',
data: [96.5, 97.2, 97.6, 98.0],
labels: ['Semaine 1', 'Semaine 2', 'Semaine 3', 'Semaine 4']
},
{
id: 'cycle_time',
name: 'Temps de Cycle',
value: 45,
unit: 'min/unité',
category: 'delays',
target: 50,
status: 'good',
trend: 'down',
description: 'Mesurer le temps nécessaire pour assembler une unité.',
formula: 'Temps Total de Production / Nombre Total d\'Unités Produites',
data: [52, 49, 47, 45],
labels: ['Semaine 1', 'Semaine 2', 'Semaine 3', 'Semaine 4']
},
{
id: 'tack_time',
name: 'Tack Time',
value: 50,
unit: 'min/unité',
category: 'delays',
target: 50,
status: 'good',
trend: 'stable',
description: 'Temps de production requis par unité demandée.',
formula: 'Temps de production / Nombre de pièces demandées',
data: [50, 50, 50, 50],
labels: ['Semaine 1', 'Semaine 2', 'Semaine 3', 'Semaine 4']
},
{
id: 'downtime',
name: 'Temps d\'Arrêt Imprévu',
value: 2.5,
unit: 'h/jour',
category: 'delays',
target: 1.5,
status: 'warning',
trend: 'up',
description: 'Mesurer le temps d\'arrêt non planifié de la ligne.',
formula: 'Somme des Périodes d\'Arrêt Non Planifié',
data: [1.8, 2.1, 2.3, 2.5],
labels: ['Semaine 1', 'Semaine 2', 'Semaine 3', 'Semaine 4']
}
],
costs: [
{
id: 'cpu',
name: 'Coût par Unité',
value: 245,
unit: '€',
category: 'costs',
target: 240,
status: 'good',
trend: 'down',
description: 'Mesurer l\'efficacité des coûts de production.',
formula: 'Coût Total de Production / Nombre Total d\'Unités Produites',
data: [258, 252, 248, 245],
labels: ['Semaine 1', 'Semaine 2', 'Semaine 3', 'Semaine 4']
},
{
id: 'labor_productivity',
name: 'Productivité MOD',
value: 8.2,
unit: 'unités/h',
category: 'costs',
target: 8.0,
status: 'good',
trend: 'up',
description: 'Mesurer l\'efficacité de l\'équipe d\'assemblage.',
formula: 'Nombre d\'Unités Produites / Total Heures Main-d\'œuvre Directe',
data: [7.8, 8.0, 8.1, 8.2],
labels: ['Semaine 1', 'Semaine 2', 'Semaine 3', 'Semaine 4']
},
{
id: 'cnq',
name: 'Coût des Non-Qualité',
value: 18500,
unit: '€',
category: 'costs',
target: 10000,
status: 'critical',
trend: 'up',
description: 'Mesurer le coût des défauts (retouche, rebut, retours).',
formula: 'Coût des Rebuts + Coût des Retouches + Retours clients',
data: [15200, 16800, 17800, 18500],
labels: ['Semaine 1', 'Semaine 2', 'Semaine 3', 'Semaine 4']
}
],
maintenance: [
{
id: 'mtbf',
name: 'MTBF (Temps Moyen Entre Pannes)',
value: 450,
unit: 'heures',
category: 'maintenance',
target: 400,
status: 'good',
trend: 'up',
description: 'Mesurer la fiabilité des équipements.',
formula: 'Temps Total de Fonctionnement / Nombre Total de Pannes',
data: [380, 400, 425, 450],
labels: ['Semaine 1', 'Semaine 2', 'Semaine 3', 'Semaine 4']
},
{
id: 'mttr',
name: 'MTTR (Temps Moyen de Réparation)',
value: 2.1,
unit: 'heures',
category: 'maintenance',
target: 2.5,
status: 'good',
trend: 'down',
description: 'Mesurer la rapidité des interventions de maintenance.',
formula: 'Temps Total de Réparation / Nombre Total de Pannes',
data: [2.8, 2.5, 2.3, 2.1],
labels: ['Semaine 1', 'Semaine 2', 'Semaine 3', 'Semaine 4']
},
{
id: 'pm_cm_ratio',
name: 'Ratio Maintenance Préventive/Corrective',
value: 65,
unit: '%',
category: 'maintenance',
target: 70,
status: 'good',
trend: 'up',
description: 'Évaluer la stratégie de maintenance (proactif vs réactif).',
formula: 'Heures MP / (Heures MP + Heures MC)',
data: [58, 61, 63, 65],
labels: ['Semaine 1', 'Semaine 2', 'Semaine 3', 'Semaine 4']
},
{
id: 'preventive_plan',
name: 'Achèvement Plan Préventif',
value: 92,
unit: '%',
category: 'maintenance',
target: 95,
status: 'good',
trend: 'stable',
description: 'Mesurer le respect des programmes d\'entretien.',
formula: '(Tâches MP Terminées / Tâches MP Planifiées) × 100',
data: [92, 92, 92, 92],
labels: ['Semaine 1', 'Semaine 2', 'Semaine 3', 'Semaine 4']
},
{
id: 'maintenance_cost_per_unit',
name: 'Coût Maintenance/Unité',
value: 28,
unit: '€',
category: 'maintenance',
target: 30,
status: 'good',
trend: 'down',
description: 'Relier les dépenses de maintenance à la production.',
formula: 'Coûts Totaux de Maintenance / Nombre Total d\'Unités Produites',
data: [32, 30, 29, 28],
labels: ['Semaine 1', 'Semaine 2', 'Semaine 3', 'Semaine 4']
}
]
};
export const getCategoryColor = (category: string): string => {
const colors: Record<string, string> = {
security: '#e74c3c',
quality: '#3498db',
delays: '#f39c12',
costs: '#27ae60',
maintenance: '#9b59b6'
};
return colors[category] || '#95a5a6';
};
export const getCategoryName = (category: string): string => {
const names: Record<string, string> = {
security: 'Sécurité',
quality: 'Qualité',
delays: 'Délais & Livraison',
costs: 'Coûts',
maintenance: 'Maintenance'
};
return names[category] || category;
};
export const getCategoryEmoji = (category: string): string => {
const emojis: Record<string, string> = {
security: '🛡️',
quality: '🎯',
delays: '⏱️',
costs: '💰',
maintenance: '🔧'
};
return emojis[category] || '📊';
};

View File

@ -0,0 +1,25 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body, #root {
height: 100%;
width: 100%;
}
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background: #f5f7fa;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}

View File

@ -0,0 +1,19 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -0,0 +1,153 @@
import React from 'react';
import { Line } from 'react-chartjs-2';
import { KPI } from '../types';
import { getCategoryColor, getCategoryName, getCategoryEmoji } from '../data/kpiData';
import '../styles/DetailPage.css';
interface DetailPageProps {
category: 'security' | 'quality' | 'delays' | 'costs' | 'maintenance';
kpis: KPI[];
}
export const DetailPage: React.FC<DetailPageProps> = ({ category, kpis }) => {
const [selectedKPI, setSelectedKPI] = React.useState<KPI | null>(kpis[0] || null);
const getStatusBadgeClass = (status: string) => {
return `badge-${status}`;
};
return (
<div className="detail-page">
<div className="detail-header">
<h1>
{getCategoryEmoji(category)} {getCategoryName(category)}
</h1>
<p>Analyse détaillée des indicateurs de performance</p>
</div>
<div className="detail-layout">
<div className="kpi-list">
<h2>Indicateurs</h2>
<div className="kpi-list-items">
{kpis.map(kpi => (
<div
key={kpi.id}
className={`kpi-list-item ${selectedKPI?.id === kpi.id ? 'active' : ''}`}
onClick={() => setSelectedKPI(kpi)}
style={{
borderLeftColor: selectedKPI?.id === kpi.id ? getCategoryColor(category) : '#ddd'
}}
>
<div className="kpi-list-name">{kpi.name}</div>
<div className="kpi-list-value">{kpi.value} {kpi.unit}</div>
<span className={`badge ${getStatusBadgeClass(kpi.status)}`}>
{kpi.status}
</span>
</div>
))}
</div>
</div>
<div className="kpi-detail">
{selectedKPI ? (
<>
<div className="detail-header-card" style={{ borderTopColor: getCategoryColor(category) }}>
<h2>{selectedKPI.name}</h2>
<div className="detail-value-section">
<span className="detail-value">{selectedKPI.value}</span>
<span className="detail-unit">{selectedKPI.unit}</span>
</div>
</div>
<div className="detail-info">
<div className="info-row">
<span className="info-label">État:</span>
<span className={`badge ${getStatusBadgeClass(selectedKPI.status)}`}>
{selectedKPI.status}
</span>
</div>
{selectedKPI.target && (
<div className="info-row">
<span className="info-label">Objectif:</span>
<span className="info-value">{selectedKPI.target} {selectedKPI.unit}</span>
</div>
)}
{selectedKPI.trend && (
<div className="info-row">
<span className="info-label">Tendance:</span>
<span className="info-value">
{selectedKPI.trend === 'up' && '📈 En hausse'}
{selectedKPI.trend === 'down' && '📉 En baisse'}
{selectedKPI.trend === 'stable' && '➡️ Stable'}
</span>
</div>
)}
</div>
<div className="detail-description">
<h3>Description</h3>
<p>{selectedKPI.description}</p>
</div>
{selectedKPI.formula && (
<div className="detail-formula">
<h3>Formule de Calcul</h3>
<pre>{selectedKPI.formula}</pre>
</div>
)}
{selectedKPI.data && selectedKPI.labels && (
<div className="detail-chart">
<h3>Évolution sur 4 semaines</h3>
<div className="chart-container-detail">
<Line
data={{
labels: selectedKPI.labels,
datasets: [
{
label: selectedKPI.name,
data: selectedKPI.data,
borderColor: getCategoryColor(category),
backgroundColor: `${getCategoryColor(category)}20`,
tension: 0.4,
fill: true,
pointRadius: 6,
pointBackgroundColor: getCategoryColor(category),
pointBorderColor: '#fff',
pointBorderWidth: 2
}
]
}}
options={{
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: true, position: 'top' as const }
},
scales: {
y: { beginAtZero: true }
}
}}
/>
</div>
</div>
)}
<div className="detail-actions">
<button className="btn btn-primary">📊 Exporter</button>
<button className="btn btn-secondary">🔔 Alertes</button>
<button className="btn btn-secondary">📝 Commentaires</button>
</div>
</>
) : (
<div className="no-selection">
<p>Sélectionnez un indicateur pour voir les détails</p>
</div>
)}
</div>
</div>
</div>
);
};

View File

@ -0,0 +1,211 @@
import React from 'react';
import { KPI } from '../types';
import { KPICard } from '../components/KPICard';
import { getCategoryColor } from '../data/kpiData';
import '../styles/HomePage.css';
interface HomePageProps {
kpiData: {
security: KPI[];
quality: KPI[];
delays: KPI[];
costs: KPI[];
maintenance: KPI[];
};
}
export const HomePage: React.FC<HomePageProps> = ({ kpiData }) => {
// Fonction pour obtenir les KPI les plus importants par catégorie
const getTopKPIs = (category: KPI[]): KPI[] => {
return category.slice(0, 2);
};
const topSecurityKPIs = getTopKPIs(kpiData.security);
const topQualityKPIs = getTopKPIs(kpiData.quality);
const topDelaysKPIs = getTopKPIs(kpiData.delays);
const topCostsKPIs = getTopKPIs(kpiData.costs);
const topMaintenanceKPIs = getTopKPIs(kpiData.maintenance);
// Calculer les statistiques globales
const allKPIs = [
...kpiData.security,
...kpiData.quality,
...kpiData.delays,
...kpiData.costs,
...kpiData.maintenance
];
const stats = {
total: allKPIs.length,
good: allKPIs.filter(k => k.status === 'good').length,
warning: allKPIs.filter(k => k.status === 'warning').length,
critical: allKPIs.filter(k => k.status === 'critical').length
};
const avgPerformance = Math.round(
((stats.good * 100 + stats.warning * 50) / (stats.total * 100)) * 100
);
// État pour la sélection de la plage temporelle
const [timeRange, setTimeRange] = React.useState<'day' | 'month' | 'year'>('month');
return (
<div className="home-page">
<div className="stats-overview">
<div className="overview-header">
<h1>📊 Dashboard SQDC</h1>
<div className="time-range-selector">
<button
className={`time-btn ${timeRange === 'day' ? 'active' : ''}`}
onClick={() => setTimeRange('day')}
>
Jour
</button>
<button
className={`time-btn ${timeRange === 'month' ? 'active' : ''}`}
onClick={() => setTimeRange('month')}
>
Mois
</button>
<button
className={`time-btn ${timeRange === 'year' ? 'active' : ''}`}
onClick={() => setTimeRange('year')}
>
Année
</button>
</div>
</div>
<div className="stats-grid">
<div className="stat-card">
<div className="stat-value" style={{ color: '#27ae60' }}>
{stats.good}
</div>
<div className="stat-label">KPI Bon</div>
<div className="stat-description">
{Math.round((stats.good / stats.total) * 100)}% des KPI
</div>
</div>
<div className="stat-card">
<div className="stat-value" style={{ color: '#f39c12' }}>
{stats.warning}
</div>
<div className="stat-label">À Améliorer</div>
<div className="stat-description">
{Math.round((stats.warning / stats.total) * 100)}% des KPI
</div>
</div>
<div className="stat-card">
<div className="stat-value" style={{ color: '#e74c3c' }}>
{stats.critical}
</div>
<div className="stat-label">Critique</div>
<div className="stat-description">
{Math.round((stats.critical / stats.total) * 100)}% des KPI
</div>
</div>
<div className="stat-card">
<div className="stat-value" style={{ color: '#3498db' }}>
{avgPerformance}%
</div>
<div className="stat-label">Performance Globale</div>
<div className="stat-description">Indicateur synthétique</div>
</div>
</div>
</div>
<div className="top-kpis-section">
<h2>🎯 KPI Clés par Catégorie</h2>
<div className="categories-grid">
{/* BLOC SYNTHÈSE */}
<div className="summary-section">
<h3>📋 Alertes Prioritaires</h3>
<div className="alerts-list">
<div className="alert-item critical">
<span className="alert-icon">🔴</span>
<div className="alert-content">
<div className="alert-title">Coût des Non-Qualité</div>
<div className="alert-value">18 500 </div>
</div>
</div>
<div className="alert-item warning">
<span className="alert-icon"></span>
<div className="alert-content">
<div className="alert-title">Taux de Retouche</div>
<div className="alert-value">3.8%</div>
</div>
</div>
<div className="alert-item warning">
<span className="alert-icon"></span>
<div className="alert-content">
<div className="alert-title">Downtime</div>
<div className="alert-value">2.5h/jour</div>
</div>
</div>
<div className="alert-item warning">
<span className="alert-icon"></span>
<div className="alert-content">
<div className="alert-title">OEE</div>
<div className="alert-value">72.3%</div>
</div>
</div>
</div>
<div className="score-section">
<div className="score-label">Performance Globale</div>
<div className="score-circle">{avgPerformance}%</div>
</div>
</div>
<div className="category-section">
<h3 style={{ color: getCategoryColor('security') }}>🛡 Sécurité</h3>
<div className="kpis-grid">
{topSecurityKPIs.map(kpi => (
<KPICard key={kpi.id} kpi={kpi} color={getCategoryColor('security')} />
))}
</div>
</div>
<div className="category-section">
<h3 style={{ color: getCategoryColor('quality') }}>🎯 Qualité</h3>
<div className="kpis-grid">
{topQualityKPIs.map(kpi => (
<KPICard key={kpi.id} kpi={kpi} color={getCategoryColor('quality')} />
))}
</div>
</div>
<div className="category-section">
<h3 style={{ color: getCategoryColor('delays') }}> Délais & Livraison</h3>
<div className="kpis-grid">
{topDelaysKPIs.map(kpi => (
<KPICard key={kpi.id} kpi={kpi} color={getCategoryColor('delays')} />
))}
</div>
</div>
<div className="category-section">
<h3 style={{ color: getCategoryColor('costs') }}>💰 Coûts</h3>
<div className="kpis-grid">
{topCostsKPIs.map(kpi => (
<KPICard key={kpi.id} kpi={kpi} color={getCategoryColor('costs')} />
))}
</div>
</div>
<div className="category-section">
<h3 style={{ color: getCategoryColor('maintenance') }}>🔧 Maintenance</h3>
<div className="kpis-grid">
{topMaintenanceKPIs.map(kpi => (
<KPICard key={kpi.id} kpi={kpi} color={getCategoryColor('maintenance')} />
))}
</div>
</div>
</div>
</div>
</div>
);
};

1
dashboard-sqdc/src/react-app-env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="react-scripts" />

View File

@ -0,0 +1,15 @@
import { ReportHandler } from 'web-vitals';
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
});
}
};
export default reportWebVitals;

View File

@ -0,0 +1,5 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';

View File

@ -0,0 +1,17 @@
.chart-container {
background: rgba(255, 255, 255, 0.95);
border-radius: 12px;
padding: 2rem;
color: #333;
}
.chart-container h2 {
font-size: 1.3rem;
margin-bottom: 1.5rem;
color: #333;
}
.chart-container {
position: relative;
height: 400px;
}

View File

@ -0,0 +1,290 @@
.detail-page {
color: white;
}
.detail-header {
margin-bottom: 2rem;
}
.detail-header h1 {
font-size: 2rem;
margin-bottom: 0.5rem;
}
.detail-header p {
font-size: 1.1rem;
opacity: 0.9;
}
.detail-layout {
display: grid;
grid-template-columns: 300px 1fr;
gap: 2rem;
}
/* KPI LIST */
.kpi-list {
background: rgba(255, 255, 255, 0.95);
border-radius: 12px;
padding: 1.5rem;
color: #333;
height: fit-content;
}
.kpi-list h2 {
font-size: 1.2rem;
margin-bottom: 1rem;
color: #333;
}
.kpi-list-items {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.kpi-list-item {
padding: 1rem;
border-left: 3px solid #ddd;
border-radius: 4px;
cursor: pointer;
transition: all 0.3s ease;
background: white;
}
.kpi-list-item:hover {
background: #f5f7fa;
}
.kpi-list-item.active {
background: #f0f2ff;
}
.kpi-list-name {
font-weight: 600;
margin-bottom: 0.5rem;
color: #333;
}
.kpi-list-value {
font-size: 0.9rem;
color: #666;
margin-bottom: 0.5rem;
}
.badge {
display: inline-block;
padding: 0.25rem 0.6rem;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 600;
}
.badge-good {
background: #d4edda;
color: #155724;
}
.badge-warning {
background: #fff3cd;
color: #856404;
}
.badge-critical {
background: #f8d7da;
color: #721c24;
}
/* DETAIL SECTION */
.kpi-detail {
background: rgba(255, 255, 255, 0.95);
border-radius: 12px;
padding: 2rem;
color: #333;
}
.detail-header-card {
border-top: 4px solid;
padding-bottom: 1.5rem;
margin-bottom: 1.5rem;
border-bottom: 1px solid #f0f0f0;
}
.detail-header-card h2 {
font-size: 1.8rem;
margin-bottom: 1rem;
margin-top: 0;
}
.detail-value-section {
display: flex;
align-items: baseline;
gap: 0.5rem;
}
.detail-value {
font-size: 2.5rem;
font-weight: bold;
color: #333;
}
.detail-unit {
font-size: 1rem;
color: #999;
}
/* INFO SECTION */
.detail-info {
margin-bottom: 2rem;
padding-bottom: 2rem;
border-bottom: 1px solid #f0f0f0;
}
.info-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
padding: 0.75rem;
background: #f9f9f9;
border-radius: 4px;
}
.info-label {
font-weight: 600;
color: #666;
}
.info-value {
color: #333;
}
/* DESCRIPTION & FORMULA */
.detail-description,
.detail-formula {
margin-bottom: 2rem;
padding-bottom: 2rem;
border-bottom: 1px solid #f0f0f0;
}
.detail-description h3,
.detail-formula h3 {
font-size: 1.1rem;
font-weight: 600;
margin-bottom: 1rem;
color: #333;
}
.detail-description p {
color: #666;
line-height: 1.6;
margin: 0;
}
.detail-formula pre {
background: #f5f7fa;
padding: 1rem;
border-radius: 4px;
overflow-x: auto;
font-size: 0.9rem;
line-height: 1.4;
color: #333;
}
/* CHART DETAIL */
.detail-chart {
margin-bottom: 2rem;
padding-bottom: 2rem;
border-bottom: 1px solid #f0f0f0;
}
.detail-chart h3 {
font-size: 1.1rem;
font-weight: 600;
margin-bottom: 1rem;
color: #333;
}
.chart-container-detail {
position: relative;
height: 350px;
margin-bottom: 1rem;
}
/* ACTIONS */
.detail-actions {
display: flex;
gap: 1rem;
}
.btn {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 6px;
font-size: 0.95rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
}
.btn-primary {
background: #667eea;
color: white;
}
.btn-primary:hover {
background: #764ba2;
}
.btn-secondary {
background: #f0f0f0;
color: #333;
}
.btn-secondary:hover {
background: #e0e0e0;
}
.no-selection {
text-align: center;
padding: 3rem;
color: #999;
font-size: 1.1rem;
}
/* RESPONSIVE */
@media (max-width: 1024px) {
.detail-layout {
grid-template-columns: 1fr;
}
.kpi-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
height: auto;
}
.kpi-list-items {
grid-column: 1 / -1;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
}
}
@media (max-width: 768px) {
.detail-header h1 {
font-size: 1.5rem;
}
.detail-value {
font-size: 2rem;
}
.detail-actions {
flex-direction: column;
}
.btn {
width: 100%;
}
}

View File

@ -0,0 +1,257 @@
.home-page {
color: white;
}
/* STATS OVERVIEW */
.stats-overview {
margin-bottom: 2rem;
}
.overview-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
gap: 2rem;
}
.overview-header h1 {
font-size: 1.8rem;
margin: 0;
white-space: nowrap;
}
/* TIME RANGE SELECTOR */
.time-range-selector {
display: flex;
gap: 0.5rem;
background: rgba(255, 255, 255, 0.1);
border-radius: 8px;
padding: 0.4rem;
}
.time-btn {
padding: 0.5rem 1rem;
border: none;
background: transparent;
color: white;
font-weight: 600;
font-size: 0.9rem;
cursor: pointer;
border-radius: 6px;
transition: all 0.3s ease;
}
.time-btn:hover {
background: rgba(255, 255, 255, 0.2);
}
.time-btn.active {
background: rgba(255, 255, 255, 0.3);
color: white;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
margin-bottom: 1.5rem;
}
.stat-card {
background: rgba(255, 255, 255, 0.95);
border-radius: 8px;
padding: 1.2rem;
text-align: center;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
}
.stat-card:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.15);
}
.stat-value {
font-size: 2.2rem;
font-weight: bold;
margin-bottom: 0.3rem;
}
.stat-label {
font-size: 0.85rem;
font-weight: 600;
color: #333;
margin-bottom: 0.3rem;
}
.stat-description {
font-size: 0.75rem;
color: #999;
}
/* TOP KPIs SECTION */
.top-kpis-section {
background: rgba(255, 255, 255, 0.98);
border-radius: 8px;
padding: 1.5rem;
color: #333;
}
.top-kpis-section h2 {
font-size: 1.3rem;
margin-bottom: 1.2rem;
color: #333;
}
/* CATEGORIES GRID - CÔTE À CÔTE */
.categories-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
gap: 1.5rem;
}
/* SUMMARY SECTION */
.summary-section {
padding: 1rem;
background: linear-gradient(135deg, #f5f7fa 0%, #e8ebf0 100%);
border-radius: 6px;
border-left: 4px solid #667eea;
}
.summary-section h3 {
font-size: 1rem;
margin-bottom: 0.8rem;
color: #333;
}
.alerts-list {
display: flex;
flex-direction: column;
gap: 0.6rem;
margin-bottom: 1rem;
max-height: 180px;
overflow-y: auto;
}
.alert-item {
display: flex;
align-items: center;
gap: 0.6rem;
padding: 0.7rem;
border-radius: 4px;
background: white;
border-left: 3px solid #ddd;
}
.alert-item.critical {
border-left-color: #e74c3c;
background: rgba(231, 76, 60, 0.05);
}
.alert-item.warning {
border-left-color: #f39c12;
background: rgba(243, 156, 18, 0.05);
}
.alert-icon {
font-size: 1.2rem;
flex-shrink: 0;
}
.alert-content {
flex: 1;
min-width: 0;
}
.alert-title {
font-size: 0.8rem;
color: #666;
margin-bottom: 0.2rem;
}
.alert-value {
font-size: 0.95rem;
font-weight: 600;
color: #333;
}
.score-section {
text-align: center;
padding: 0.8rem;
background: white;
border-radius: 4px;
border: 1px solid #e0e0e0;
}
.score-label {
font-size: 0.8rem;
color: #999;
margin-bottom: 0.5rem;
}
.score-circle {
font-size: 2rem;
font-weight: bold;
color: #667eea;
}
.category-section {
padding: 1rem;
background: #f9f9f9;
border-radius: 6px;
border-left: 4px solid #ddd;
}
.category-section h3 {
font-size: 1rem;
margin-bottom: 0.8rem;
display: flex;
align-items: center;
gap: 0.5rem;
color: #333;
}
.kpis-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 0.8rem;
}
/* RESPONSIVE */
@media (max-width: 1200px) {
.kpis-grid {
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
}
.categories-grid {
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
}
}
@media (max-width: 768px) {
.overview-header {
flex-direction: column;
align-items: flex-start;
}
.overview-header h1 {
font-size: 1.3rem;
}
.stats-grid {
grid-template-columns: repeat(2, 1fr);
}
.top-kpis-section {
padding: 1rem;
}
.categories-grid {
grid-template-columns: 1fr;
}
.kpis-grid {
grid-template-columns: repeat(2, 1fr);
}
}

View File

@ -0,0 +1,97 @@
.kpi-card {
background: white;
border-radius: 6px;
padding: 1.2rem;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
border-left: 4px solid #667eea;
transition: all 0.3s ease;
}
.kpi-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
}
.kpi-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 0.8rem;
padding-bottom: 0.8rem;
border-bottom: 1px solid #f0f0f0;
}
.kpi-title {
font-size: 0.95rem;
font-weight: 600;
margin: 0;
color: inherit;
}
.kpi-trend {
font-size: 1.2rem;
}
.kpi-value-section {
margin-bottom: 0.8rem;
}
.kpi-value {
font-size: 1.8rem;
font-weight: bold;
color: #333;
line-height: 1;
margin-bottom: 0.2rem;
}
.kpi-unit {
font-size: 0.75rem;
color: #999;
}
.kpi-footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.8rem;
gap: 0.5rem;
}
.kpi-status {
display: inline-block;
padding: 0.3rem 0.6rem;
border-radius: 16px;
font-size: 0.7rem;
font-weight: 600;
white-space: nowrap;
}
.kpi-status.status-good {
background: #d4edda;
color: #155724;
}
.kpi-status.status-warning {
background: #fff3cd;
color: #856404;
}
.kpi-status.status-critical {
background: #f8d7da;
color: #721c24;
}
.kpi-target {
font-size: 0.7rem;
color: #999;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.kpi-description {
font-size: 0.75rem;
color: #666;
line-height: 1.3;
margin: 0;
}

View File

@ -0,0 +1,31 @@
// Types pour les KPI et données
export interface KPI {
id: string;
name: string;
value: number | string;
unit: string;
category: 'security' | 'quality' | 'delays' | 'costs' | 'maintenance';
target?: number;
status: 'good' | 'warning' | 'critical';
trend?: 'up' | 'down' | 'stable';
description: string;
formula?: string;
data?: number[];
labels?: string[];
}
export interface CategoryData {
security: KPI[];
quality: KPI[];
delays: KPI[];
costs: KPI[];
maintenance: KPI[];
}
export interface DashboardStats {
totalKPIs: number;
goodCount: number;
warningCount: number;
criticalCount: number;
avgPerformance: number;
}

View File

@ -0,0 +1,26 @@
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": [
"src"
]
}

View File

@ -0,0 +1,22 @@
Catégorie,KPI (Indicateur),Objectif,Formule de Calcul Ajustée
SÉCURITÉ,Taux de Fréquence (TF),Mesurer la fréquence des accidents avec arrêt.,(Nombre dAccidents avec Arret/Nombre dHeures Travaillees)×1000000
SÉCURITÉ,Nombre d'Incidents/Near Miss,Évaluer la culture de sécurité et la proactivité.,Compte des rapports dincidents (sans blessure/dommage)
SÉCURITÉ,Taux de Conformité aux Audits,Mesurer le respect des procédures de sécurité.,(Points de Controle Conformites/Total Points de Controle)×100
QUALITÉ,Taux de Rebut (Scrap Rate),Mesurer le pourcentage d'unités jetées (irréparables).,(Nombre dUnites Rebutees/Nombre Total dUnites Produites)×100
QUALITÉ,Taux de Retouche (Rework Rate),Mesurer le pourcentage d'unités nécessitant une reprise.,(Nombre dUnites Retouchees/Nombre Total dUnites Produites)×100
QUALITÉ,Nombre de Défauts par Unité (DPU),Mesurer le nombre moyen de défauts par produit.,Nombre Total de Defauts Trouves/Nombre Total dUnites Inspectees
QUALITÉ,Taux de Retours Clients,Mesurer l'impact de la non-qualité chez le client.,(Nombre dUnites Retournees/Nombre Total dUnites Vendues)×100
QUALITÉ,Taux de rendement synthétique (TRS),,Nb pièces bonnes x Temps cycle / Temps douverture
DÉLAIS / LIVRAISON,Efficacité Globale de l'Équipement (OEE),"Mesurer l'efficacité combinée (Disponibilité, Performance, Qualité).",Disponibilite×Performance×Qualite
DÉLAIS / LIVRAISON,Taux de Respect du Plan (Schedule Adherence),Mesurer la capacité à atteindre le volume planifié.,(Quantite Reellement Produite/Quantite Planifiee)×100
DÉLAIS / LIVRAISON,"Temps de Cycle (Cycle Time, TC)",Mesurer le temps nécessaire pour assembler une unité.,Temps Total de Production/Nombre Total dUnites Produites
DÉLAIS / LIVRAISON,Tack Time (TT),,Temps de production / Nombre de pièces demandées
DÉLAIS / LIVRAISON,Temps d'Arrêt Imprévu (Downtime),Mesurer le temps d'arrêt non planifié de la ligne.,Somme des Periodes dArret Non Planifie
COÛT,Coût par Unité (CPU),Mesurer l'efficacité des coûts de production.,Cout Total de Production/Nombre Total dUnites Produites
COÛT,Productivité de la Main-d'œuvre,Mesurer l'efficacité de l'équipe d'assemblage.,Nombre dUnites Produites/Total Heures Main-dœuvre Directe
COÛT,Coût des Non-Qualité (CNQ),"Mesurer le coût des défauts (retouche, rebut, retours).",Cout des Rebuts+Cout des Retouches
MAINTENANCE,Temps Moyen Entre Pannes (MTBF),Mesurer la fiabilité des équipements.,Temps Total de Fonctionnement/Nombre Total de Pannes
MAINTENANCE,Temps Moyen de Réparation (MTTR),Mesurer la rapidité des interventions de maintenance.,Temps Total de Reparation/Nombre Total de Pannes
MAINTENANCE,Ratio Maintenance Préventive / Corrective,Évaluer la stratégie de maintenance (proactif vs réactif).,Heures MP/Heures MC
MAINTENANCE,Taux d'Achèvement du Plan Préventif,Mesurer le respect des programmes d'entretien.,(Taches MP Terminees/Taches MP Planifiees)×100
MAINTENANCE,Coût de Maintenance par Unité Produite,Relier les dépenses de maintenance à la production.,Couts Totaux de Maintenance/Nombre Total dUnites Produites
1 Catégorie KPI (Indicateur) Objectif Formule de Calcul Ajustée
2 SÉCURITÉ Taux de Fréquence (TF) Mesurer la fréquence des accidents avec arrêt. (Nombre d’Accidents avec Arret/Nombre d’Heures Travaillees)×1000000
3 SÉCURITÉ Nombre d'Incidents/Near Miss Évaluer la culture de sécurité et la proactivité. Compte des rapports d’incidents (sans blessure/dommage)
4 SÉCURITÉ Taux de Conformité aux Audits Mesurer le respect des procédures de sécurité. (Points de Controle Conformites/Total Points de Controle)×100
5 QUALITÉ Taux de Rebut (Scrap Rate) Mesurer le pourcentage d'unités jetées (irréparables). (Nombre d’Unites Rebutees/Nombre Total d’Unites Produites)×100
6 QUALITÉ Taux de Retouche (Rework Rate) Mesurer le pourcentage d'unités nécessitant une reprise. (Nombre d’Unites Retouchees/Nombre Total d’Unites Produites)×100
7 QUALITÉ Nombre de Défauts par Unité (DPU) Mesurer le nombre moyen de défauts par produit. Nombre Total de Defauts Trouves/Nombre Total d’Unites Inspectees
8 QUALITÉ Taux de Retours Clients Mesurer l'impact de la non-qualité chez le client. (Nombre d’Unites Retournees/Nombre Total d’Unites Vendues)×100
9 QUALITÉ Taux de rendement synthétique (TRS) Nb pièces bonnes x Temps cycle / Temps d’ouverture
10 DÉLAIS / LIVRAISON Efficacité Globale de l'Équipement (OEE) Mesurer l'efficacité combinée (Disponibilité, Performance, Qualité). Disponibilite×Performance×Qualite
11 DÉLAIS / LIVRAISON Taux de Respect du Plan (Schedule Adherence) Mesurer la capacité à atteindre le volume planifié. (Quantite Reellement Produite/Quantite Planifiee)×100
12 DÉLAIS / LIVRAISON Temps de Cycle (Cycle Time, TC) Mesurer le temps nécessaire pour assembler une unité. Temps Total de Production/Nombre Total d’Unites Produites
13 DÉLAIS / LIVRAISON Tack Time (TT) Temps de production / Nombre de pièces demandées
14 DÉLAIS / LIVRAISON Temps d'Arrêt Imprévu (Downtime) Mesurer le temps d'arrêt non planifié de la ligne. Somme des Periodes d’Arret Non Planifie
15 COÛT Coût par Unité (CPU) Mesurer l'efficacité des coûts de production. Cout Total de Production/Nombre Total d’Unites Produites
16 COÛT Productivité de la Main-d'œuvre Mesurer l'efficacité de l'équipe d'assemblage. Nombre d’Unites Produites/Total Heures Main-d’œuvre Directe
17 COÛT Coût des Non-Qualité (CNQ) Mesurer le coût des défauts (retouche, rebut, retours). Cout des Rebuts+Cout des Retouches
18 MAINTENANCE Temps Moyen Entre Pannes (MTBF) Mesurer la fiabilité des équipements. Temps Total de Fonctionnement/Nombre Total de Pannes
19 MAINTENANCE Temps Moyen de Réparation (MTTR) Mesurer la rapidité des interventions de maintenance. Temps Total de Reparation/Nombre Total de Pannes
20 MAINTENANCE Ratio Maintenance Préventive / Corrective Évaluer la stratégie de maintenance (proactif vs réactif). Heures MP/Heures MC
21 MAINTENANCE Taux d'Achèvement du Plan Préventif Mesurer le respect des programmes d'entretien. (Taches MP Terminees/Taches MP Planifiees)×100
22 MAINTENANCE Coût de Maintenance par Unité Produite Relier les dépenses de maintenance à la production. Couts Totaux de Maintenance/Nombre Total d’Unites Produites