Version : 2025.11.22

This commit is contained in:
2025-11-23 16:34:11 +01:00
commit b399e803b3
31 changed files with 5810 additions and 0 deletions

131
components/BalanceCard.tsx Normal file
View File

@@ -0,0 +1,131 @@
import React, { useMemo } from 'react';
import { Balance, Operation, OperationType } from '../types';
interface BalanceCardProps {
balance: Balance;
operations: Operation[];
isSelected: boolean;
onClick: () => void;
onEdit: (balance: Balance) => void;
onDelete: (balanceId: string) => void;
}
const BalanceCard: React.FC<BalanceCardProps> = ({ balance, operations, isSelected, onClick, onEdit, onDelete }) => {
const { totalIncome, totalExpenses, currentBalance } = useMemo(() => {
const totalIncome = operations
.filter(op => op.type === OperationType.INCOME)
.reduce((sum, op) => sum + op.amount, 0);
const totalExpenses = operations
.filter(op => op.type === OperationType.EXPENSE)
.reduce((sum, op) => sum + op.amount, 0);
const currentBalance = balance.initialAmount + totalIncome - totalExpenses;
return { totalIncome, totalExpenses, currentBalance };
}, [balance, operations]);
const formatCurrency = (amount: number) => {
return new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(amount);
};
const [openMenu, setOpenMenu] = React.useState(false);
const menuRef = React.useRef<HTMLDivElement>(null);
const [menuPosition, setMenuPosition] = React.useState<{ top: number; left: number } | null>(null);
React.useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
setOpenMenu(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);
const toggleMenu = (e: React.MouseEvent) => {
e.stopPropagation();
if (openMenu) {
setOpenMenu(false);
return;
}
const rect = e.currentTarget.getBoundingClientRect();
setMenuPosition({
top: rect.bottom,
left: rect.right - 128,
});
setOpenMenu(true);
};
return (
<div
onClick={onClick}
className={`relative p-6 rounded-xl border transition-all duration-200 cursor-pointer group ${isSelected
? 'bg-gray-800 text-white shadow-lg'
: 'bg-white text-gray-800 shadow-md border-gray-200 hover:shadow-lg hover:border-gray-300'
}`}
>
<div className="flex justify-between items-start">
<p className={`text-lg font-semibold ${isSelected ? 'text-gray-300' : 'text-gray-600'}`}>
{balance.name}
</p>
<button
onClick={toggleMenu}
className={`p-1 rounded-full hover:bg-gray-200 transition-colors ${isSelected ? 'text-gray-400 hover:bg-gray-700' : 'text-gray-400'}`}
>
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path d="M10 6a2 2 0 110-4 2 2 0 010 4zM10 12a2 2 0 110-4 2 2 0 010 4zM10 18a2 2 0 110-4 2 2 0 010 4z" />
</svg>
</button>
</div>
<p className="text-4xl font-bold mt-2 truncate">{formatCurrency(currentBalance)}</p>
<div className="flex justify-between items-center mt-6">
<div className="text-center">
<p className={`text-sm ${isSelected ? 'text-gray-400' : 'text-gray-500'}`}>Income</p>
<p className={`font-semibold text-green-500 ${isSelected ? 'text-green-400' : 'text-green-600'}`}>{formatCurrency(totalIncome)}</p>
</div>
<div className="text-center">
<p className={`text-sm ${isSelected ? 'text-gray-400' : 'text-gray-500'}`}>Expenses</p>
<p className={`font-semibold text-red-500 ${isSelected ? 'text-red-400' : 'text-red-600'}`}>{formatCurrency(totalExpenses)}</p>
</div>
</div>
{openMenu && menuPosition && (
<div
ref={menuRef}
className="fixed z-50 w-32 origin-top-right rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none"
style={{ top: menuPosition.top, left: menuPosition.left }}
onClick={(e) => e.stopPropagation()}
>
<div className="py-1">
<button
onClick={() => {
onEdit(balance);
setOpenMenu(false);
}}
className="block w-full px-4 py-2 text-left text-sm text-gray-700 hover:bg-gray-100"
>
Edit
</button>
<button
onClick={() => {
onDelete(balance.id);
setOpenMenu(false);
}}
className="block w-full px-4 py-2 text-left text-sm text-red-600 hover:bg-gray-100"
>
Delete
</button>
</div>
</div>
)}
</div>
);
};
export default BalanceCard;