espace-paie-odentas/components/Calculator.tsx

275 lines
11 KiB
TypeScript
Raw 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 });
// 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);
};
const handleNumber = (num: string) => {
if (waitingForSecond) {
setDisplay(num);
setWaitingForSecond(false);
} 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;
}
setDisplay(result.toString());
setOperator('');
setFirstValue('');
setWaitingForSecond(false);
};
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) {
onUseResult(result);
onClose();
}
};
// Gestion du clavier
useEffect(() => {
if (!isOpen) return;
const handleKeyDown = (e: KeyboardEvent) => {
// 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-[50px] 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>
)}
{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>
);
}