try to do better

This commit is contained in:
arrelin
2026-01-29 15:17:54 +03:00
parent f00ddc7d10
commit 24f04a7e82
60 changed files with 5335 additions and 1254 deletions

View File

@@ -0,0 +1,208 @@
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Tag, TrendingDown, Plus, Trash2, RotateCcw, History, X, DollarSign, MessageSquare, Calendar } from 'lucide-react';
import { CategoryWithRemaining } from '../../services';
import { categoryService, expenseService } from '../../services';
import { useExpenses } from '../../hooks';
import { format } from '../../utils/format';
import { Button, Input, Badge } 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>
);
}