Version : 2025.11.22
This commit is contained in:
131
components/BalanceCard.tsx
Normal file
131
components/BalanceCard.tsx
Normal 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;
|
||||
Reference in New Issue
Block a user