espace-paie-odentas/components/Calculator.tsx

298 lines
12 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import React, { useState, useEffect } from 'react';
import { X } from 'lucide-react';
interface CalculatorProps {
isOpen: boolean;
onClose: () => void;
onUseResult?: (value: number) => void;
}
export default function Calculator({ isOpen, onClose, onUseResult }: CalculatorProps) {
const [display, setDisplay] = useState('0');
const [operator, setOperator] = useState('');
const [firstValue, setFirstValue] = useState('');
const [waitingForSecond, setWaitingForSecond] = useState(false);
const [position, setPosition] = useState<{ x: number; y: number } | null>(null);
const [isDragging, setIsDragging] = useState(false);
const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 });
const [lastExpression, setLastExpression] = useState<string>("");
// Initialiser la position au centre à l'ouverture
useEffect(() => {
if (isOpen && position === null) {
const centerX = (window.innerWidth - 280) / 2;
const centerY = (window.innerHeight - 450) / 2;
setPosition({ x: centerX, y: centerY });
}
}, [isOpen, position]);
// Réinitialiser la position quand on ferme
useEffect(() => {
if (!isOpen) {
setPosition(null);
}
}, [isOpen]);
const resetCalculator = () => {
setDisplay('0');
setOperator('');
setFirstValue('');
setWaitingForSecond(false);
setLastExpression("");
};
const handleNumber = (num: string) => {
if (waitingForSecond) {
setDisplay(num);
setWaitingForSecond(false);
// Si on était après un calcul (operator vide), effacer l'expression précédente
if (!operator) setLastExpression("");
} else {
setDisplay(display === '0' ? num : display + num);
}
};
const handleOperator = (op: string) => {
if (operator && !waitingForSecond) {
calculate();
}
setFirstValue(display);
setOperator(op);
setWaitingForSecond(true);
};
const calculate = () => {
const first = parseFloat(firstValue);
const second = parseFloat(display);
let result = 0;
switch(operator) {
case '+': result = first + second; break;
case '-': result = first - second; break;
case '*': result = first * second; break;
case '/': result = second !== 0 ? first / second : 0; break;
}
// Arrondir et afficher 2 décimales dans la calculatrice
const rounded = Math.round((result + Number.EPSILON) * 100) / 100;
// Conserver l'expression utilisée
const opSymbol = operator === '/' ? '÷' : operator === '*' ? '×' : operator === '-' ? '' : operator;
const fmt = (s: string) => s.replace('.', ',');
setLastExpression(`${fmt(firstValue)} ${opSymbol} ${fmt(display)} =`);
setDisplay(rounded.toFixed(2));
setOperator('');
setFirstValue('');
// Après un calcul, la prochaine saisie numérique doit remplacer l'affichage
setWaitingForSecond(true);
};
const handleDecimal = () => {
if (waitingForSecond) {
setDisplay('0.');
setWaitingForSecond(false);
} else if (!display.includes('.')) {
setDisplay(display + '.');
}
};
const handleMouseDown = (e: React.MouseEvent) => {
if (!position) return;
setIsDragging(true);
setDragOffset({
x: e.clientX - position.x,
y: e.clientY - position.y,
});
};
useEffect(() => {
const handleMouseMove = (e: MouseEvent) => {
if (!isDragging) return;
const newX = e.clientX - dragOffset.x;
const newY = e.clientY - dragOffset.y;
const maxX = window.innerWidth - 280;
const maxY = window.innerHeight - 450;
setPosition({
x: Math.max(0, Math.min(newX, maxX)),
y: Math.max(0, Math.min(newY, maxY)),
});
};
const handleMouseUp = () => {
setIsDragging(false);
};
if (isDragging) {
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
}
return () => {
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
};
}, [isDragging, dragOffset]);
const handleUse = () => {
const result = parseFloat(display);
if (!isNaN(result) && result > 0 && onUseResult) {
const rounded = Math.round((result + Number.EPSILON) * 100) / 100;
onUseResult(rounded);
onClose();
}
};
// Gestion du clavier
useEffect(() => {
if (!isOpen) return;
const handleKeyDown = (e: KeyboardEvent) => {
// Vérifier que le focus est sur la calculatrice ou ses enfants
if (calculatorRef.current && !calculatorRef.current.contains(document.activeElement)) {
return; // Ignorer si le focus n'est pas sur la calculatrice
}
// Empêcher le comportement par défaut pour les touches de la calculatrice
if (['0','1','2','3','4','5','6','7','8','9','+','-','*','/','Enter','Escape','Backspace','Delete','c','C','.'].includes(e.key)) {
e.preventDefault();
}
// Chiffres
if (e.key >= '0' && e.key <= '9') {
handleNumber(e.key);
}
// Opérateurs
else if (e.key === '+') {
handleOperator('+');
}
else if (e.key === '-') {
handleOperator('-');
}
else if (e.key === '*' || e.key === 'x' || e.key === 'X') {
handleOperator('*');
}
else if (e.key === '/') {
handleOperator('/');
}
// Égal
else if (e.key === 'Enter' || e.key === '=') {
if (operator) calculate();
}
// Clear
else if (e.key === 'Escape' || e.key === 'c' || e.key === 'C') {
resetCalculator();
}
// Backspace / Delete
else if (e.key === 'Backspace' || e.key === 'Delete') {
if (display.length > 1) {
setDisplay(display.slice(0, -1));
} else {
setDisplay('0');
}
}
// Point décimal
else if (e.key === '.' || e.key === ',') {
handleDecimal();
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [isOpen, display, operator, firstValue, waitingForSecond]);
// Focus automatique sur la calculatrice à l'ouverture
const calculatorRef = React.useRef<HTMLDivElement>(null);
useEffect(() => {
// Focus une fois que le composant est monté (position non nulle)
if (isOpen && position !== null && calculatorRef.current) {
calculatorRef.current.focus();
}
}, [isOpen, position]);
if (!isOpen || position === null) return null;
return (
<div
ref={calculatorRef}
tabIndex={-1}
className="fixed bg-white rounded-2xl shadow-2xl flex flex-col overflow-hidden focus:outline-none"
style={{
left: `${position.x}px`,
top: `${position.y}px`,
width: '280px',
zIndex: 9999,
}}
>
{/* Header draggable */}
<div
className="flex items-center justify-between px-4 py-3 bg-gradient-to-r from-indigo-600 to-indigo-500 cursor-move select-none"
onMouseDown={handleMouseDown}
>
<span className="text-white font-semibold text-sm">Calculatrice</span>
<button
onClick={onClose}
className="w-7 h-7 flex items-center justify-center rounded-lg bg-white/20 hover:bg-white/30 transition-colors"
onMouseDown={(e) => e.stopPropagation()}
>
<X className="w-4 h-4 text-white" />
</button>
</div>
{/* Body */}
<div className="p-4">
{/* Display */}
<div className="relative bg-gradient-to-br from-slate-100 to-slate-200 border-2 border-slate-300 rounded-xl p-4 text-right text-2xl font-semibold text-slate-900 mb-4 min-h-[58px] break-all">
{/* Opérateur courant (badge) */}
{operator && (
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-sm font-semibold text-slate-600 select-none">
{operator === '/' ? '÷' : operator === '*' ? '×' : operator === '-' ? '' : operator}
</span>
)}
{/* Dernière expression calculée */}
{lastExpression && (
<div className="absolute left-3 top-1.5 text-xs font-medium text-slate-500 select-none">
{lastExpression}
</div>
)}
{display || '0'}
</div>
{/* Buttons */}
<div className="grid grid-cols-4 gap-2">
<button onClick={() => resetCalculator()} className="col-span-1 p-4 rounded-lg font-semibold bg-gradient-to-br from-red-500 to-red-600 text-white hover:from-red-600 hover:to-red-700 transition-all hover:shadow-md active:scale-95">C</button>
<button onClick={() => handleOperator('/')} className="p-4 rounded-lg font-semibold bg-gradient-to-br from-orange-400 to-orange-500 text-white hover:from-orange-500 hover:to-orange-600 transition-all hover:shadow-md active:scale-95">/</button>
<button onClick={() => handleOperator('*')} className="p-4 rounded-lg font-semibold bg-gradient-to-br from-orange-400 to-orange-500 text-white hover:from-orange-500 hover:to-orange-600 transition-all hover:shadow-md active:scale-95">×</button>
<button onClick={() => handleOperator('-')} className="p-4 rounded-lg font-semibold bg-gradient-to-br from-orange-400 to-orange-500 text-white hover:from-orange-500 hover:to-orange-600 transition-all hover:shadow-md active:scale-95"></button>
<button onClick={() => handleNumber('7')} className="p-4 rounded-lg font-semibold bg-white border border-slate-200 hover:bg-slate-50 transition-all hover:shadow-md active:scale-95">7</button>
<button onClick={() => handleNumber('8')} className="p-4 rounded-lg font-semibold bg-white border border-slate-200 hover:bg-slate-50 transition-all hover:shadow-md active:scale-95">8</button>
<button onClick={() => handleNumber('9')} className="p-4 rounded-lg font-semibold bg-white border border-slate-200 hover:bg-slate-50 transition-all hover:shadow-md active:scale-95">9</button>
<button onClick={() => handleOperator('+')} className="row-span-2 p-4 rounded-lg font-semibold bg-gradient-to-br from-orange-400 to-orange-500 text-white hover:from-orange-500 hover:to-orange-600 transition-all hover:shadow-md active:scale-95">+</button>
<button onClick={() => handleNumber('4')} className="p-4 rounded-lg font-semibold bg-white border border-slate-200 hover:bg-slate-50 transition-all hover:shadow-md active:scale-95">4</button>
<button onClick={() => handleNumber('5')} className="p-4 rounded-lg font-semibold bg-white border border-slate-200 hover:bg-slate-50 transition-all hover:shadow-md active:scale-95">5</button>
<button onClick={() => handleNumber('6')} className="p-4 rounded-lg font-semibold bg-white border border-slate-200 hover:bg-slate-50 transition-all hover:shadow-md active:scale-95">6</button>
<button onClick={() => handleNumber('1')} className="p-4 rounded-lg font-semibold bg-white border border-slate-200 hover:bg-slate-50 transition-all hover:shadow-md active:scale-95">1</button>
<button onClick={() => handleNumber('2')} className="p-4 rounded-lg font-semibold bg-white border border-slate-200 hover:bg-slate-50 transition-all hover:shadow-md active:scale-95">2</button>
<button onClick={() => handleNumber('3')} className="p-4 rounded-lg font-semibold bg-white border border-slate-200 hover:bg-slate-50 transition-all hover:shadow-md active:scale-95">3</button>
<button onClick={() => operator ? calculate() : null} className="row-span-2 p-4 rounded-lg font-semibold bg-gradient-to-br from-green-500 to-green-600 text-white hover:from-green-600 hover:to-green-700 transition-all hover:shadow-md active:scale-95">=</button>
<button onClick={() => handleNumber('0')} className="col-span-2 p-4 rounded-lg font-semibold bg-white border border-slate-200 hover:bg-slate-50 transition-all hover:shadow-md active:scale-95">0</button>
<button onClick={handleDecimal} className="p-4 rounded-lg font-semibold bg-white border border-slate-200 hover:bg-slate-50 transition-all hover:shadow-md active:scale-95">.</button>
{onUseResult && (
<button onClick={handleUse} className="col-span-4 mt-2 p-3 rounded-lg font-semibold text-sm bg-gradient-to-br from-indigo-600 to-indigo-500 text-white hover:from-indigo-700 hover:to-indigo-600 transition-all hover:shadow-md active:scale-95">
Utiliser le résultat
</button>
)}
</div>
</div>
</div>
);
}