Major Changes: - Migrate UI to Shadcn components with Tailwind CSS v3 - Implement dark theme as default with improved color scheme - Optimize homepage layout to fit single screen without scrolling - Fix chart visibility with explicit colors for dark mode Deployment Infrastructure: - Add Docker multi-stage build with Nginx + Node.js - Create Kubernetes manifests (deployment, service, ingress, PVC) - Configure Gitea CI/CD workflow with registry integration - Add deployment scripts with registry support CI/CD Configuration: - Registry: gitea.vidoks.fr/sortifal/pfee - Automatic build and push on commits - Kubernetes deployment with image pull secrets - Three-stage pipeline: build, deploy, notify Documentation: - Add DEPLOYMENT.md with comprehensive deployment guide - Add SETUP-REGISTRY.md with step-by-step registry setup - Add workflow README with troubleshooting guide - Include configuration examples and best practices 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
148 lines
3.6 KiB
TypeScript
148 lines
3.6 KiB
TypeScript
import React, { useState } from 'react';
|
|
import { Chart as ChartJS, CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend } from 'chart.js';
|
|
import { Line } from 'react-chartjs-2';
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from './ui/dialog';
|
|
import { Button } from './ui/button';
|
|
import { LineChart } from 'lucide-react';
|
|
|
|
ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend);
|
|
|
|
interface RangeChartModalProps {
|
|
isOpen: boolean;
|
|
kpi: any;
|
|
measurements: any[];
|
|
getMeasurementsForRange: (days: number) => any[];
|
|
onClose: () => void;
|
|
}
|
|
|
|
export const RangeChartModal: React.FC<RangeChartModalProps> = ({
|
|
isOpen,
|
|
kpi,
|
|
measurements,
|
|
getMeasurementsForRange,
|
|
onClose,
|
|
}) => {
|
|
const [selectedRange, setSelectedRange] = useState<number>(30);
|
|
|
|
if (!kpi) return null;
|
|
|
|
const filteredMeasurements = getMeasurementsForRange(selectedRange);
|
|
|
|
const labels = filteredMeasurements
|
|
.map(m => new Date(m.measurement_date).toLocaleDateString('fr-FR'))
|
|
.reverse();
|
|
|
|
const values = filteredMeasurements
|
|
.map(m => m.value)
|
|
.reverse();
|
|
|
|
const chartData = {
|
|
labels,
|
|
datasets: [
|
|
{
|
|
label: kpi.name,
|
|
data: values,
|
|
borderColor: '#60a5fa',
|
|
backgroundColor: 'rgba(96, 165, 250, 0.1)',
|
|
tension: 0.4,
|
|
fill: true,
|
|
pointRadius: 4,
|
|
pointBackgroundColor: '#60a5fa',
|
|
pointBorderColor: '#1e293b',
|
|
pointBorderWidth: 2,
|
|
},
|
|
],
|
|
};
|
|
|
|
const chartOptions = {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: {
|
|
display: true,
|
|
position: 'top' as const,
|
|
labels: {
|
|
color: '#cbd5e1',
|
|
},
|
|
},
|
|
title: {
|
|
display: false,
|
|
},
|
|
},
|
|
scales: {
|
|
y: {
|
|
beginAtZero: true,
|
|
title: {
|
|
display: true,
|
|
text: kpi.unit,
|
|
color: '#cbd5e1',
|
|
},
|
|
ticks: {
|
|
color: '#94a3b8',
|
|
},
|
|
grid: {
|
|
color: 'rgba(148, 163, 184, 0.1)',
|
|
},
|
|
},
|
|
x: {
|
|
display: true,
|
|
ticks: {
|
|
color: '#94a3b8',
|
|
},
|
|
grid: {
|
|
color: 'rgba(148, 163, 184, 0.1)',
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
const ranges = [
|
|
{ value: 7, label: 'Semaine' },
|
|
{ value: 30, label: 'Mois' },
|
|
{ value: 90, label: 'Trimestre' },
|
|
{ value: 365, label: 'Année' },
|
|
{ value: -1, label: 'Tout' },
|
|
];
|
|
|
|
return (
|
|
<Dialog open={isOpen} onOpenChange={onClose}>
|
|
<DialogContent className="sm:max-w-[900px] max-h-[90vh]">
|
|
<DialogHeader>
|
|
<DialogTitle className="flex items-center gap-2">
|
|
<LineChart className="h-5 w-5" />
|
|
{kpi.name}
|
|
</DialogTitle>
|
|
<DialogDescription>
|
|
Mesures: {filteredMeasurements.length} | Période: {labels[0]} à {labels[labels.length - 1]}
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<div className="flex gap-2 flex-wrap py-2">
|
|
{ranges.map((range) => (
|
|
<Button
|
|
key={range.value}
|
|
variant={selectedRange === range.value ? 'default' : 'outline'}
|
|
size="sm"
|
|
onClick={() => setSelectedRange(range.value)}
|
|
>
|
|
{range.label}
|
|
</Button>
|
|
))}
|
|
</div>
|
|
|
|
<div className="py-4">
|
|
<div style={{ height: '500px', width: '100%' }}>
|
|
<Line data={chartData} options={chartOptions} />
|
|
</div>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
};
|