import { useEffect, useState } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { categoryApi, expenseApi, inviteLinkApi } from '../api/client'; import { useStore } from '../store/useStore'; import type { Category, InviteLinkResponse, ExpenseHistoryResponse } from '../types'; import { Wallet, TrendingDown, Plus, Trash2, RotateCcw, Archive, Loader2, X, DollarSign, Tag, History, Calendar, MessageSquare, ShoppingCart, UserPlus, Copy, Check, User, } from 'lucide-react'; import ShoppingListModal from '../components/ShoppingListModal'; export default function FamilyView() { const { t } = useTranslation(); const { familyId } = useParams<{ familyId: string }>(); const navigate = useNavigate(); const { selectedFamily } = useStore(); const [categories, setCategories] = useState([]); const [remainingLimits, setRemainingLimits] = useState>(new Map()); const [loading, setLoading] = useState(true); const [error, setError] = useState(''); const [showAddCategory, setShowAddCategory] = useState(false); const [newCategoryName, setNewCategoryName] = useState(''); const [newCategoryLimit, setNewCategoryLimit] = useState(''); const [showAddExpense, setShowAddExpense] = useState(null); const [expenseAmount, setExpenseAmount] = useState(''); const [expenseDescription, setExpenseDescription] = useState(''); const [showHistory, setShowHistory] = useState(null); const [showArchive, setShowArchive] = useState(null); const [historyData, setHistoryData] = useState(null); const [archiveData, setArchiveData] = useState(null); const [showShoppingList, setShowShoppingList] = useState(false); const [showInviteModal, setShowInviteModal] = useState(false); const [inviteLink, setInviteLink] = useState(null); const [inviteLoading, setInviteLoading] = useState(false); const [copied, setCopied] = useState(false); useEffect(() => { if (!familyId) { navigate('/'); return; } loadCategories(); }, [familyId]); const loadCategories = async () => { if (!familyId) return; try { setLoading(true); setError(''); console.log('Loading categories for family:', familyId); const response = await categoryApi.getAllByFamily(parseInt(familyId)); console.log('Categories loaded:', response.data); setCategories(response.data); const limits = new Map(); for (const category of response.data) { const limitResponse = await expenseApi.getRemainingLimit( parseInt(familyId), category.id ); const limitValue = typeof limitResponse.data.remaining_limit === 'string' ? parseFloat(limitResponse.data.remaining_limit) : limitResponse.data.remaining_limit; limits.set(category.id, limitValue); } setRemainingLimits(limits); console.log('All data loaded successfully'); } catch (err: any) { const errorMsg = err.response?.data?.message || err.message || t('family.loadError'); setError(errorMsg); console.error('Error loading categories:', err); } finally { setLoading(false); } }; const handleAddCategory = async () => { if (!familyId || !newCategoryName || !newCategoryLimit) return; try { await categoryApi.create(parseInt(familyId), { name: newCategoryName, limit_amount: parseFloat(newCategoryLimit), }); setNewCategoryName(''); setNewCategoryLimit(''); setShowAddCategory(false); loadCategories(); } catch (err: any) { const errorMsg = err.response?.data?.message || err.response?.statusText || err.message || t('category.createError'); alert(`${t('category.createError')}: ${errorMsg}`); console.error('Full error:', err); } }; const handleDeleteCategory = async (categoryId: number) => { if (!familyId) return; if (!confirm(t('category.deleteConfirm'))) return; try { await categoryApi.delete(parseInt(familyId), categoryId); loadCategories(); } catch (err) { alert(t('category.deleteError')); console.error(err); } }; const handleResetLimit = async (categoryId: number) => { if (!familyId) return; if (!confirm(t('category.resetConfirm'))) return; try { const expensesResponse = await expenseApi.getAllByCategory( parseInt(familyId), categoryId ); for (const expense of expensesResponse.data) { await expenseApi.delete( parseInt(familyId), categoryId, expense.id ); } loadCategories(); } catch (err) { alert(t('category.resetError')); console.error(err); } }; const handleAddExpense = async (categoryId: number) => { if (!familyId || !expenseAmount) return; try { await expenseApi.create(parseInt(familyId), categoryId, { amount: parseFloat(expenseAmount), description: expenseDescription || undefined, }); setExpenseAmount(''); setExpenseDescription(''); setShowAddExpense(null); const limitResponse = await expenseApi.getRemainingLimit(parseInt(familyId), categoryId); const limitValue = typeof limitResponse.data.remaining_limit === 'string' ? parseFloat(limitResponse.data.remaining_limit) : limitResponse.data.remaining_limit; setRemainingLimits(prev => new Map(prev).set(categoryId, limitValue)); if (showHistory === categoryId) { const historyResponse = await expenseApi.getHistory(parseInt(familyId), categoryId, false); setHistoryData(historyResponse.data); } } catch (err) { alert(t('expense.addError')); console.error(err); } }; const handleShowHistory = async (categoryId: number) => { if (!familyId) return; if (showHistory === categoryId) { setShowHistory(null); return; } setShowArchive(null); try { const response = await expenseApi.getHistory( parseInt(familyId), categoryId, false ); setHistoryData(response.data); setShowHistory(categoryId); } catch (err) { alert(t('expense.historyError')); console.error(err); } }; const handleShowArchive = async (categoryId: number) => { if (!familyId) return; if (showArchive === categoryId) { setShowArchive(null); return; } setShowHistory(null); try { const response = await expenseApi.getHistory( parseInt(familyId), categoryId, true ); setArchiveData(response.data); setShowArchive(categoryId); } catch (err) { alert(t('expense.archiveError')); console.error(err); } }; const handleCreateInviteLink = async () => { try { setInviteLoading(true); const response = await inviteLinkApi.create({ expires_in_hours: 168 }); setInviteLink(response.data); } catch (err) { alert(t('invite.createError')); console.error(err); } finally { setInviteLoading(false); } }; const handleCopyInviteLink = async () => { if (!inviteLink) return; try { await navigator.clipboard.writeText(inviteLink.invite_url); setCopied(true); setTimeout(() => setCopied(false), 2000); } catch (err) { console.error('Failed to copy:', err); } }; const handleOpenInviteModal = () => { setShowInviteModal(true); setInviteLink(null); setCopied(false); }; if (loading) { return (
{t('common.loading')}
); } const getProgressColor = (remaining: number, limit: number) => { const percentage = (remaining / limit) * 100; if (percentage > 50) return 'bg-green-500'; if (percentage > 25) return 'bg-yellow-500'; return 'bg-red-500'; }; const getProgressPercentage = (remaining: number, limit: number) => { return Math.max(0, Math.min(100, (remaining / limit) * 100)); }; const getTotalLimit = () => { return categories.reduce((sum, cat) => sum + parseFloat(cat.limit_amount.toString()), 0); }; const getTotalRemaining = () => { return Array.from(remainingLimits.values()).reduce((sum, val) => sum + val, 0); }; const formatDate = (dateString: string) => { let dateStr = dateString; if (!dateStr.endsWith('Z') && !dateStr.includes('+')) { dateStr = dateStr + 'Z'; } const date = new Date(dateStr); return date.toLocaleString('ru-RU', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit', }); }; const getMonthName = (month: number) => { const months = [ t('months.1'), t('months.2'), t('months.3'), t('months.4'), t('months.5'), t('months.6'), t('months.7'), t('months.8'), t('months.9'), t('months.10'), t('months.11'), t('months.12') ]; return months[month - 1] || month; }; return (

{selectedFamily?.name || t('family.defaultName')}

{t('family.totalLimit')}

{getTotalLimit().toFixed(2)} ₽

{t('family.totalRemaining')}

{getTotalRemaining().toFixed(2)} ₽

{error && (
{error}
)}
{categories.map((category) => { const remaining = remainingLimits.get(category.id) || 0; const limit = parseFloat(category.limit_amount.toString()); const percentage = getProgressPercentage(remaining, limit); return (

{category.name}

{showAddExpense !== category.id && ( )}
{t('category.remaining')} {remaining.toFixed(2)} ₽
{t('category.limit')} {limit.toFixed(2)} ₽

{percentage.toFixed(0)}{t('category.percentRemaining')}

{showAddExpense === category.id && (

{t('expense.addTitle')}

setExpenseAmount(e.target.value)} className="w-full px-4 py-3 border-2 border-gray-300 rounded-2xl focus:border-purple-500 focus:ring-2 focus:ring-purple-200 transition-all text-center font-semibold text-lg" />
setExpenseDescription(e.target.value)} className="w-full px-4 py-3 border-2 border-gray-300 rounded-2xl focus:border-purple-500 focus:ring-2 focus:ring-purple-200 transition-all" />
)} {showHistory === category.id && historyData && (

{t('expense.historyTitle')}

{historyData.months.length === 0 ? (

{t('expense.noExpenses')}

) : (
{historyData.months.map((monthGroup) => (

{getMonthName(monthGroup.month)} {monthGroup.year}

{parseFloat(monthGroup.total_amount.toString()).toFixed(2)} ₽
{monthGroup.expenses.map((expense) => (
{parseFloat(expense.amount.toString()).toFixed(2)} ₽
{formatDate(expense.created_at)}
{expense.description && (
{expense.description}
)}
))}
))}
)}
)} {showArchive === category.id && archiveData && (

{t('expense.archiveTitle')}

{archiveData.months.length === 0 ? (

{t('expense.noArchive')}

) : (
{archiveData.months.map((monthGroup) => (

{getMonthName(monthGroup.month)} {monthGroup.year}

{parseFloat(monthGroup.total_amount.toString()).toFixed(2)} ₽
{monthGroup.expenses.map((expense) => (
{parseFloat(expense.amount.toString()).toFixed(2)} ₽ {t('expense.archived')}
{formatDate(expense.created_at)}
{expense.description && (
{expense.description}
)}
))}
))}
)}
)}
); })}

{t('category.management')}

{showAddCategory ? (

{t('category.newCategory')}

setNewCategoryName(e.target.value)} className="w-full px-5 py-4 border-2 border-gray-300 rounded-2xl focus:border-purple-500 focus:ring-2 focus:ring-purple-200 transition-all font-medium" /> setNewCategoryLimit(e.target.value)} className="w-full px-5 py-4 border-2 border-gray-300 rounded-2xl focus:border-purple-500 focus:ring-2 focus:ring-purple-200 transition-all font-medium text-center" />
) : ( )}
{showShoppingList && familyId && ( setShowShoppingList(false)} /> )} {showInviteModal && (

{t('invite.title')}

{!inviteLink ? (

{t('invite.description')}

) : (

{t('invite.sendLink')}

{inviteLink.invite_url}

)}
)}
); }