209 lines
7.7 KiB
TypeScript
209 lines
7.7 KiB
TypeScript
import { useState } from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { Tag, TrendingDown, Plus, Trash2, RotateCcw, History, X, DollarSign, MessageSquare, Calendar } from 'lucide-react';
|
|
import type { CategoryWithRemaining } from '../../services';
|
|
import { categoryService, expenseService } from '../../services';
|
|
import { useExpenses } from '../../hooks';
|
|
import { format } from '../../utils/format';
|
|
import { Button, Input } from '../ui';
|
|
|
|
interface CategoryCardProps {
|
|
category: CategoryWithRemaining;
|
|
familyId: number;
|
|
onDelete: (categoryId: number) => void;
|
|
onReset: (categoryId: number) => void;
|
|
onUpdate: () => void;
|
|
}
|
|
|
|
export function CategoryCard({ category, familyId, onDelete, onReset, onUpdate }: CategoryCardProps) {
|
|
const { t } = useTranslation();
|
|
const [showAddExpense, setShowAddExpense] = useState(false);
|
|
const [showHistory, setShowHistory] = useState(false);
|
|
const [expenseAmount, setExpenseAmount] = useState('');
|
|
const [expenseDescription, setExpenseDescription] = useState('');
|
|
|
|
const { expenses, loadExpenses, createExpense } = useExpenses(familyId, category.id);
|
|
|
|
const handleAddExpense = async () => {
|
|
if (!expenseAmount) return;
|
|
|
|
try {
|
|
await createExpense({
|
|
amount: parseFloat(expenseAmount),
|
|
description: expenseDescription || undefined,
|
|
});
|
|
setExpenseAmount('');
|
|
setExpenseDescription('');
|
|
setShowAddExpense(false);
|
|
onUpdate();
|
|
} catch (error) {
|
|
console.error(error);
|
|
}
|
|
};
|
|
|
|
const handleShowHistory = async () => {
|
|
if (!showHistory) {
|
|
await loadExpenses();
|
|
}
|
|
setShowHistory(!showHistory);
|
|
};
|
|
|
|
const progress = categoryService.calculateProgress(category.limit_amount, category.remaining_limit);
|
|
const progressColor = categoryService.getProgressColor(progress);
|
|
|
|
return (
|
|
<div className="glass-effect rounded-2xl shadow-lg overflow-hidden transition-all duration-300 hover:shadow-xl">
|
|
<div className="p-5">
|
|
<div className="flex items-start justify-between mb-4">
|
|
<div className="flex items-center gap-3">
|
|
<div className="p-3 bg-gradient-to-br from-blue-500 to-blue-600 rounded-xl">
|
|
<Tag className="w-6 h-6 text-white" />
|
|
</div>
|
|
<div>
|
|
<h3 className="text-xl font-bold text-gray-900">{category.name}</h3>
|
|
<p className="text-sm text-gray-600">
|
|
{t('category.limit')}: {format.currency(category.limit_amount)}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<button
|
|
onClick={() => onDelete(category.id)}
|
|
className="p-2 hover:bg-red-100 rounded-lg transition-colors group"
|
|
>
|
|
<Trash2 className="w-5 h-5 text-red-600 group-hover:scale-110 transition-transform" />
|
|
</button>
|
|
<button
|
|
onClick={() => onReset(category.id)}
|
|
className="p-2 hover:bg-blue-100 rounded-lg transition-colors group"
|
|
>
|
|
<RotateCcw className="w-5 h-5 text-blue-600 group-hover:scale-110 transition-transform" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mb-4">
|
|
<div className="flex justify-between items-center mb-2">
|
|
<span className="text-sm font-medium text-gray-700">{t('category.remaining')}</span>
|
|
<span className="text-lg font-bold text-green-600">
|
|
{format.currency(category.remaining_limit)}
|
|
</span>
|
|
</div>
|
|
<div className="w-full bg-gray-200 rounded-full h-3 overflow-hidden">
|
|
<div
|
|
className={`h-full transition-all duration-500 ${
|
|
progressColor === 'danger' ? 'bg-red-500' :
|
|
progressColor === 'warning' ? 'bg-yellow-500' :
|
|
'bg-green-500'
|
|
}`}
|
|
style={{ width: `${100 - progress}%` }}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex gap-2">
|
|
<Button
|
|
variant="primary"
|
|
size="sm"
|
|
fullWidth
|
|
onClick={() => setShowAddExpense(!showAddExpense)}
|
|
>
|
|
<Plus className="w-4 h-4 mr-2" />
|
|
{t('expense.add')}
|
|
</Button>
|
|
<Button
|
|
variant="secondary"
|
|
size="sm"
|
|
fullWidth
|
|
onClick={handleShowHistory}
|
|
>
|
|
<History className="w-4 h-4 mr-2" />
|
|
{t('expense.history')}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{showAddExpense && (
|
|
<div className="border-t border-gray-200 p-5 bg-gray-50">
|
|
<h4 className="font-semibold text-gray-900 mb-3 flex items-center gap-2">
|
|
<TrendingDown className="w-5 h-5 text-red-600" />
|
|
{t('expense.add')}
|
|
</h4>
|
|
<div className="space-y-3">
|
|
<Input
|
|
type="number"
|
|
placeholder={t('expense.amount')}
|
|
value={expenseAmount}
|
|
onChange={(e) => setExpenseAmount(e.target.value)}
|
|
fullWidth
|
|
/>
|
|
<Input
|
|
type="text"
|
|
placeholder={t('expense.description')}
|
|
value={expenseDescription}
|
|
onChange={(e) => setExpenseDescription(e.target.value)}
|
|
fullWidth
|
|
/>
|
|
<div className="flex gap-2">
|
|
<Button variant="success" fullWidth onClick={handleAddExpense}>
|
|
{t('common.save')}
|
|
</Button>
|
|
<Button variant="secondary" fullWidth onClick={() => setShowAddExpense(false)}>
|
|
{t('common.cancel')}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{showHistory && (
|
|
<div className="border-t border-gray-200 p-5 bg-gray-50 max-h-96 overflow-y-auto">
|
|
<div className="flex items-center justify-between mb-3">
|
|
<h4 className="font-semibold text-gray-900 flex items-center gap-2">
|
|
<History className="w-5 h-5 text-blue-600" />
|
|
{t('expense.history')}
|
|
</h4>
|
|
<button
|
|
onClick={() => setShowHistory(false)}
|
|
className="p-1 hover:bg-gray-200 rounded transition-colors"
|
|
>
|
|
<X className="w-5 h-5 text-gray-600" />
|
|
</button>
|
|
</div>
|
|
{expenses.length === 0 ? (
|
|
<p className="text-gray-600 text-center py-4">{t('expense.noHistory')}</p>
|
|
) : (
|
|
<div className="space-y-2">
|
|
{expenseService.sortByDate(expenses).map((expense) => (
|
|
<div
|
|
key={expense.id}
|
|
className="flex items-start justify-between p-3 bg-white rounded-lg border border-gray-200"
|
|
>
|
|
<div className="flex-1">
|
|
<div className="flex items-center gap-2 mb-1">
|
|
<DollarSign className="w-4 h-4 text-red-600" />
|
|
<span className="font-semibold text-gray-900">
|
|
{format.currency(expense.amount)}
|
|
</span>
|
|
</div>
|
|
{expense.description && (
|
|
<div className="flex items-start gap-2 text-sm text-gray-600">
|
|
<MessageSquare className="w-4 h-4 mt-0.5" />
|
|
<span>{expense.description}</span>
|
|
</div>
|
|
)}
|
|
<div className="flex items-center gap-2 text-xs text-gray-500 mt-1">
|
|
<Calendar className="w-3 h-3" />
|
|
<span>{format.date(expense.created_at)}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|