init
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
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, Expense, InviteLinkResponse } from '../types';
|
||||
@@ -24,6 +25,7 @@ import {
|
||||
import ShoppingListModal from '../components/ShoppingListModal';
|
||||
|
||||
export default function FamilyView() {
|
||||
const { t } = useTranslation();
|
||||
const { familyId } = useParams<{ familyId: string }>();
|
||||
const navigate = useNavigate();
|
||||
const { selectedFamily } = useStore();
|
||||
@@ -82,7 +84,7 @@ export default function FamilyView() {
|
||||
setRemainingLimits(limits);
|
||||
console.log('All data loaded successfully');
|
||||
} catch (err: any) {
|
||||
const errorMsg = err.response?.data?.message || err.message || 'Ошибка загрузки категорий';
|
||||
const errorMsg = err.response?.data?.message || err.message || t('family.loadError');
|
||||
setError(errorMsg);
|
||||
console.error('Error loading categories:', err);
|
||||
} finally {
|
||||
@@ -103,28 +105,28 @@ export default function FamilyView() {
|
||||
setShowAddCategory(false);
|
||||
loadCategories();
|
||||
} catch (err: any) {
|
||||
const errorMsg = err.response?.data?.message || err.response?.statusText || err.message || 'Ошибка создания категории';
|
||||
alert(`Ошибка создания категории: ${errorMsg} (Статус: ${err.response?.status})`);
|
||||
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('Удалить категорию?')) return;
|
||||
if (!confirm(t('category.deleteConfirm'))) return;
|
||||
|
||||
try {
|
||||
await categoryApi.delete(parseInt(familyId), categoryId);
|
||||
loadCategories();
|
||||
} catch (err) {
|
||||
alert('Ошибка удаления категории');
|
||||
alert(t('category.deleteError'));
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleResetLimit = async (categoryId: number) => {
|
||||
if (!familyId) return;
|
||||
if (!confirm('Удалить все траты по этой категории?')) return;
|
||||
if (!confirm(t('category.resetConfirm'))) return;
|
||||
|
||||
try {
|
||||
const expensesResponse = await expenseApi.getAllByCategory(
|
||||
@@ -142,7 +144,7 @@ export default function FamilyView() {
|
||||
|
||||
loadCategories();
|
||||
} catch (err) {
|
||||
alert('Ошибка сброса трат');
|
||||
alert(t('category.resetError'));
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
@@ -160,7 +162,7 @@ export default function FamilyView() {
|
||||
setShowAddExpense(null);
|
||||
loadCategories();
|
||||
} catch (err) {
|
||||
alert('Ошибка добавления расхода');
|
||||
alert(t('expense.addError'));
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
@@ -181,7 +183,7 @@ export default function FamilyView() {
|
||||
setCategoryExpenses(response.data);
|
||||
setShowHistory(categoryId);
|
||||
} catch (err) {
|
||||
alert('Ошибка загрузки истории трат');
|
||||
alert(t('expense.historyError'));
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
@@ -192,7 +194,7 @@ export default function FamilyView() {
|
||||
const response = await inviteLinkApi.create({ expires_in_hours: 168 });
|
||||
setInviteLink(response.data);
|
||||
} catch (err) {
|
||||
alert('Ошибка создания ссылки-приглашения');
|
||||
alert(t('invite.createError'));
|
||||
console.error(err);
|
||||
} finally {
|
||||
setInviteLoading(false);
|
||||
@@ -221,7 +223,7 @@ export default function FamilyView() {
|
||||
<div className="min-h-screen flex items-center justify-center gradient-bg">
|
||||
<div className="flex items-center gap-3 text-white">
|
||||
<Loader2 className="w-8 h-8 animate-spin" />
|
||||
<span className="text-xl font-medium">Загрузка...</span>
|
||||
<span className="text-xl font-medium">{t('common.loading')}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -270,25 +272,25 @@ export default function FamilyView() {
|
||||
className="inline-flex items-center gap-2 px-4 py-2 bg-white/20 hover:bg-white/30 text-white rounded-2xl backdrop-blur-md mb-6 transition-all duration-300 group"
|
||||
>
|
||||
<UserPlus className="w-5 h-5 group-hover:scale-110 transition-transform" />
|
||||
<span className="font-medium">Пригласить участника</span>
|
||||
<span className="font-medium">{t('family.inviteMember')}</span>
|
||||
</button>
|
||||
<div className="text-center">
|
||||
<div className="inline-flex p-4 bg-white/20 backdrop-blur-md rounded-2xl mb-4">
|
||||
<Wallet className="w-12 h-12 text-white" />
|
||||
</div>
|
||||
<h1 className="text-4xl sm:text-5xl font-bold text-white mb-6">
|
||||
{selectedFamily?.name || 'Семья'}
|
||||
{selectedFamily?.name || t('family.defaultName')}
|
||||
</h1>
|
||||
<div className="max-w-2xl mx-auto glass-effect rounded-2xl shadow-lg p-5">
|
||||
<div className="grid grid-cols-2 gap-4 mb-4">
|
||||
<div className="text-center">
|
||||
<p className="text-gray-600 font-medium text-sm mb-2">Общий лимит</p>
|
||||
<p className="text-gray-600 font-medium text-sm mb-2">{t('family.totalLimit')}</p>
|
||||
<p className="text-2xl sm:text-3xl font-bold text-gray-900">
|
||||
{getTotalLimit().toFixed(2)} ₽
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-center border-l-2 border-gray-300">
|
||||
<p className="text-gray-600 font-medium text-sm mb-2">Общий остаток</p>
|
||||
<p className="text-gray-600 font-medium text-sm mb-2">{t('family.totalRemaining')}</p>
|
||||
<p className="text-2xl sm:text-3xl font-bold text-gray-900">
|
||||
{getTotalRemaining().toFixed(2)} ₽
|
||||
</p>
|
||||
@@ -299,7 +301,7 @@ export default function FamilyView() {
|
||||
className="w-full flex items-center justify-center gap-2 px-6 py-3 bg-gradient-to-r from-green-500 to-emerald-600 text-white rounded-2xl hover:shadow-xl transition-all duration-300 font-semibold"
|
||||
>
|
||||
<ShoppingCart className="w-5 h-5" />
|
||||
Список покупок
|
||||
{t('family.shoppingList')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -341,21 +343,21 @@ export default function FamilyView() {
|
||||
className="flex items-center gap-2 px-4 py-2 bg-linear-to-r from-red-500 to-pink-500 text-white rounded-xl hover:shadow-lg transition-all duration-300 font-semibold whitespace-nowrap text-sm"
|
||||
>
|
||||
<TrendingDown className="w-4 h-4" />
|
||||
<span className="hidden sm:inline">Добавить расход</span>
|
||||
<span className="sm:hidden">Расход</span>
|
||||
<span className="hidden sm:inline">{t('category.addExpense')}</span>
|
||||
<span className="sm:hidden">{t('category.expense')}</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 mb-4">
|
||||
<div className="flex justify-between items-baseline">
|
||||
<span className="text-gray-600 font-medium text-sm">Остаток:</span>
|
||||
<span className="text-gray-600 font-medium text-sm">{t('category.remaining')}</span>
|
||||
<span className="text-2xl sm:text-3xl font-bold text-gray-900">
|
||||
{remaining.toFixed(2)} ₽
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-baseline text-gray-500 text-sm">
|
||||
<span>Лимит:</span>
|
||||
<span>{t('category.limit')}</span>
|
||||
<span className="text-base font-semibold">{limit.toFixed(2)} ₽</span>
|
||||
</div>
|
||||
|
||||
@@ -366,7 +368,7 @@ export default function FamilyView() {
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 text-center font-medium">
|
||||
{percentage.toFixed(0)}% осталось
|
||||
{percentage.toFixed(0)}{t('category.percentRemaining')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -376,21 +378,21 @@ export default function FamilyView() {
|
||||
className="flex-1 flex items-center justify-center gap-1.5 px-3 py-2 bg-yellow-500 hover:bg-yellow-600 text-white rounded-xl transition-all font-semibold shadow-md hover:shadow-lg text-sm"
|
||||
>
|
||||
<RotateCcw className="w-4 h-4" />
|
||||
<span>Обнулить</span>
|
||||
<span>{t('category.reset')}</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleShowHistory(category.id)}
|
||||
className="flex-1 flex items-center justify-center gap-1.5 px-3 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-xl transition-all font-semibold shadow-md hover:shadow-lg text-sm"
|
||||
>
|
||||
<History className="w-4 h-4" />
|
||||
<span>История</span>
|
||||
<span>{t('category.history')}</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDeleteCategory(category.id)}
|
||||
className="flex-1 flex items-center justify-center gap-1.5 px-3 py-2 bg-red-500 hover:bg-red-600 text-white rounded-xl transition-all font-semibold shadow-md hover:shadow-lg text-sm"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
<span>Удалить</span>
|
||||
<span>{t('common.delete')}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -399,7 +401,7 @@ export default function FamilyView() {
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="font-semibold text-gray-800 text-lg flex items-center gap-2">
|
||||
<History className="w-5 h-5" />
|
||||
История трат
|
||||
{t('expense.historyTitle')}
|
||||
</h3>
|
||||
<button
|
||||
onClick={() => setShowHistory(null)}
|
||||
@@ -410,7 +412,7 @@ export default function FamilyView() {
|
||||
</div>
|
||||
|
||||
{categoryExpenses.length === 0 ? (
|
||||
<p className="text-center text-gray-500 py-4">Нет трат</p>
|
||||
<p className="text-center text-gray-500 py-4">{t('expense.noExpenses')}</p>
|
||||
) : (
|
||||
<div className="space-y-2 max-h-96 overflow-y-auto">
|
||||
{categoryExpenses.map((expense) => (
|
||||
@@ -448,12 +450,12 @@ export default function FamilyView() {
|
||||
{showAddExpense === category.id && (
|
||||
<div className="bg-linear-to-br from-purple-50 to-blue-50 p-6 rounded-2xl border-2 border-purple-200">
|
||||
<h3 className="font-semibold text-gray-800 mb-4 text-center">
|
||||
Добавить расход
|
||||
{t('expense.addTitle')}
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Сумма (₽)
|
||||
{t('expense.amount')}
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
@@ -465,11 +467,11 @@ export default function FamilyView() {
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Описание
|
||||
{t('expense.description')}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Опционально"
|
||||
placeholder={t('expense.descriptionPlaceholder')}
|
||||
value={expenseDescription}
|
||||
onChange={(e) => 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"
|
||||
@@ -481,7 +483,7 @@ export default function FamilyView() {
|
||||
className="flex-1 flex items-center justify-center gap-2 px-5 py-3 bg-linear-to-r from-green-500 to-green-600 text-white rounded-2xl hover:shadow-xl transition-all font-semibold"
|
||||
>
|
||||
<Plus className="w-5 h-5" />
|
||||
Добавить
|
||||
{t('common.add')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowAddExpense(null)}
|
||||
@@ -504,26 +506,26 @@ export default function FamilyView() {
|
||||
<DollarSign className="w-8 h-8 text-white" />
|
||||
</div>
|
||||
<h2 className="text-2xl sm:text-3xl font-bold text-gray-800">
|
||||
Управление категориями
|
||||
{t('category.management')}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
{showAddCategory ? (
|
||||
<div className="mb-8 p-6 bg-linear-to-br from-purple-50 to-blue-50 rounded-2xl border-2 border-purple-200">
|
||||
<h3 className="font-bold text-gray-800 mb-5 text-center text-lg">
|
||||
Новая категория
|
||||
{t('category.newCategory')}
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Название категории"
|
||||
placeholder={t('category.categoryName')}
|
||||
value={newCategoryName}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
placeholder="Лимит (₽)"
|
||||
placeholder={t('category.categoryLimit')}
|
||||
value={newCategoryLimit}
|
||||
onChange={(e) => 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"
|
||||
@@ -534,13 +536,13 @@ export default function FamilyView() {
|
||||
className="flex-1 flex items-center justify-center gap-2 px-6 py-4 bg-linear-to-r from-green-500 to-green-600 text-white rounded-2xl hover:shadow-xl transition-all font-semibold"
|
||||
>
|
||||
<Plus className="w-5 h-5" />
|
||||
Создать
|
||||
{t('common.create')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowAddCategory(false)}
|
||||
className="px-6 py-4 bg-gray-200 hover:bg-gray-300 text-gray-700 rounded-2xl transition-all font-medium"
|
||||
>
|
||||
Отмена
|
||||
{t('common.cancel')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -551,7 +553,7 @@ export default function FamilyView() {
|
||||
className="w-full flex items-center justify-center gap-2 px-6 py-4 bg-linear-to-r from-purple-600 to-blue-600 text-white rounded-2xl hover:shadow-xl transition-all duration-300 font-semibold"
|
||||
>
|
||||
<Plus className="w-5 h-5" />
|
||||
Добавить категорию
|
||||
{t('category.addCategory')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@@ -572,7 +574,7 @@ export default function FamilyView() {
|
||||
<div className="p-3 bg-gradient-to-br from-purple-500 to-blue-500 rounded-2xl">
|
||||
<UserPlus className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
<h2 className="text-xl font-bold text-gray-800">Пригласить участника</h2>
|
||||
<h2 className="text-xl font-bold text-gray-800">{t('invite.title')}</h2>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowInviteModal(false)}
|
||||
@@ -585,8 +587,7 @@ export default function FamilyView() {
|
||||
{!inviteLink ? (
|
||||
<div className="text-center">
|
||||
<p className="text-gray-600 mb-6">
|
||||
Создайте ссылку-приглашение, чтобы добавить нового участника в семью.
|
||||
Ссылка будет действительна 7 дней.
|
||||
{t('invite.description')}
|
||||
</p>
|
||||
<button
|
||||
onClick={handleCreateInviteLink}
|
||||
@@ -596,12 +597,12 @@ export default function FamilyView() {
|
||||
{inviteLoading ? (
|
||||
<>
|
||||
<Loader2 className="w-5 h-5 animate-spin" />
|
||||
Создание...
|
||||
{t('invite.creating')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<UserPlus className="w-5 h-5" />
|
||||
Создать ссылку
|
||||
{t('invite.createLink')}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
@@ -609,7 +610,7 @@ export default function FamilyView() {
|
||||
) : (
|
||||
<div>
|
||||
<p className="text-gray-600 mb-4 text-center">
|
||||
Отправьте эту ссылку участнику, которого хотите пригласить:
|
||||
{t('invite.sendLink')}
|
||||
</p>
|
||||
<div className="bg-gray-100 rounded-2xl p-4 mb-4">
|
||||
<p className="text-sm text-gray-800 break-all font-mono">
|
||||
@@ -627,12 +628,12 @@ export default function FamilyView() {
|
||||
{copied ? (
|
||||
<>
|
||||
<Check className="w-5 h-5" />
|
||||
Скопировано!
|
||||
{t('invite.copied')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Copy className="w-5 h-5" />
|
||||
Скопировать ссылку
|
||||
{t('invite.copyLink')}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user