ci/cd + https + front
This commit is contained in:
@@ -2,7 +2,7 @@ import { useEffect, useState } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { categoryApi, expenseApi } from '../api/client';
|
||||
import { useStore } from '../store/useStore';
|
||||
import type { Category } from '../types';
|
||||
import type { Category, Expense } from '../types';
|
||||
import {
|
||||
ArrowLeft,
|
||||
Wallet,
|
||||
@@ -14,6 +14,9 @@ import {
|
||||
X,
|
||||
DollarSign,
|
||||
Tag,
|
||||
History,
|
||||
Calendar,
|
||||
MessageSquare,
|
||||
} from 'lucide-react';
|
||||
|
||||
export default function FamilyView() {
|
||||
@@ -34,6 +37,9 @@ export default function FamilyView() {
|
||||
const [expenseAmount, setExpenseAmount] = useState('');
|
||||
const [expenseDescription, setExpenseDescription] = useState('');
|
||||
|
||||
const [showHistory, setShowHistory] = useState<number | null>(null);
|
||||
const [categoryExpenses, setCategoryExpenses] = useState<Expense[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!familyId) {
|
||||
navigate('/');
|
||||
@@ -150,6 +156,27 @@ export default function FamilyView() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleShowHistory = async (categoryId: number) => {
|
||||
if (!familyId) return;
|
||||
|
||||
if (showHistory === categoryId) {
|
||||
setShowHistory(null);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await expenseApi.getAllByCategory(
|
||||
parseInt(familyId),
|
||||
categoryId
|
||||
);
|
||||
setCategoryExpenses(response.data);
|
||||
setShowHistory(categoryId);
|
||||
} catch (err) {
|
||||
alert('Ошибка загрузки истории трат');
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center gradient-bg">
|
||||
@@ -172,6 +199,21 @@ export default function FamilyView() {
|
||||
return Math.max(0, Math.min(100, (remaining / limit) * 100));
|
||||
};
|
||||
|
||||
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',
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen gradient-bg py-8 sm:py-12 px-4">
|
||||
<div className="max-w-5xl mx-auto">
|
||||
@@ -214,14 +256,14 @@ export default function FamilyView() {
|
||||
return (
|
||||
<div
|
||||
key={category.id}
|
||||
className="glass-effect rounded-3xl shadow-xl p-6 sm:p-8 card-hover"
|
||||
className="glass-effect rounded-2xl shadow-lg p-4 sm:p-5 card-hover"
|
||||
>
|
||||
<div className="flex items-center justify-between gap-4 mb-6">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="p-3 bg-linear-to-br from-purple-500 to-blue-500 text-white rounded-2xl shadow-lg">
|
||||
<Tag className="w-8 h-8" />
|
||||
<div className="flex items-center justify-between gap-3 mb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 bg-linear-to-br from-purple-500 to-blue-500 text-white rounded-xl shadow-lg">
|
||||
<Tag className="w-6 h-6" />
|
||||
</div>
|
||||
<h2 className="text-2xl sm:text-3xl font-bold text-gray-900">
|
||||
<h2 className="text-xl sm:text-2xl font-bold text-gray-900">
|
||||
{category.name}
|
||||
</h2>
|
||||
</div>
|
||||
@@ -229,38 +271,113 @@ export default function FamilyView() {
|
||||
{showAddExpense !== category.id && (
|
||||
<button
|
||||
onClick={() => setShowAddExpense(category.id)}
|
||||
className="flex items-center gap-2 px-5 py-3 bg-linear-to-r from-red-500 to-pink-500 text-white rounded-2xl hover:shadow-xl transition-all duration-300 font-semibold whitespace-nowrap"
|
||||
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-5 h-5" />
|
||||
<TrendingDown className="w-4 h-4" />
|
||||
<span className="hidden sm:inline">Добавить расход</span>
|
||||
<span className="sm:hidden">Расход</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 mb-6">
|
||||
<div className="space-y-3 mb-4">
|
||||
<div className="flex justify-between items-baseline">
|
||||
<span className="text-gray-600 font-medium">Остаток:</span>
|
||||
<span className="text-3xl sm:text-4xl font-bold text-gray-900">
|
||||
<span className="text-gray-600 font-medium text-sm">Остаток:</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">
|
||||
<div className="flex justify-between items-baseline text-gray-500 text-sm">
|
||||
<span>Лимит:</span>
|
||||
<span className="text-lg font-semibold">{limit.toFixed(2)} ₽</span>
|
||||
<span className="text-base font-semibold">{limit.toFixed(2)} ₽</span>
|
||||
</div>
|
||||
|
||||
<div className="relative h-4 bg-gray-200 rounded-full overflow-hidden">
|
||||
<div className="relative h-3 bg-gray-200 rounded-full overflow-hidden">
|
||||
<div
|
||||
className={`h-full ${getProgressColor(remaining, limit)} transition-all duration-500 rounded-full shadow-inner`}
|
||||
style={{ width: `${percentage}%` }}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-sm text-gray-500 text-center font-medium">
|
||||
<p className="text-xs text-gray-500 text-center font-medium">
|
||||
{percentage.toFixed(0)}% осталось
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 justify-between">
|
||||
<button
|
||||
onClick={() => handleResetLimit(category.id)}
|
||||
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>
|
||||
</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>
|
||||
</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>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{showHistory === category.id && (
|
||||
<div className="mt-4 bg-linear-to-br from-blue-50 to-purple-50 p-4 rounded-2xl border-2 border-blue-200">
|
||||
<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" />
|
||||
История трат
|
||||
</h3>
|
||||
<button
|
||||
onClick={() => setShowHistory(null)}
|
||||
className="p-2 hover:bg-white/50 rounded-xl transition-all"
|
||||
>
|
||||
<X className="w-5 h-5 text-gray-600" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{categoryExpenses.length === 0 ? (
|
||||
<p className="text-center text-gray-500 py-4">Нет трат</p>
|
||||
) : (
|
||||
<div className="space-y-2 max-h-96 overflow-y-auto">
|
||||
{categoryExpenses.map((expense) => (
|
||||
<div
|
||||
key={expense.id}
|
||||
className="bg-white p-3 rounded-xl shadow-sm border border-gray-200"
|
||||
>
|
||||
<div className="flex justify-between items-start mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="p-1.5 bg-red-100 rounded-lg">
|
||||
<TrendingDown className="w-4 h-4 text-red-600" />
|
||||
</div>
|
||||
<span className="font-bold text-gray-900 text-lg">
|
||||
{parseFloat(expense.amount.toString()).toFixed(2)} ₽
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 text-xs text-gray-500">
|
||||
<Calendar className="w-3 h-3" />
|
||||
<span>{formatDate(expense.created_at)}</span>
|
||||
</div>
|
||||
</div>
|
||||
{expense.description && (
|
||||
<div className="flex items-start gap-2 text-sm text-gray-600 bg-gray-50 p-2 rounded-lg">
|
||||
<MessageSquare className="w-4 h-4 mt-0.5 flex-shrink-0" />
|
||||
<span>{expense.description}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{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">
|
||||
@@ -364,48 +481,12 @@ export default function FamilyView() {
|
||||
) : (
|
||||
<button
|
||||
onClick={() => setShowAddCategory(true)}
|
||||
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 mb-8"
|
||||
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" />
|
||||
Добавить категорию
|
||||
</button>
|
||||
)}
|
||||
|
||||
<div className="space-y-3">
|
||||
{categories.map((category) => (
|
||||
<div
|
||||
key={category.id}
|
||||
className="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-3 p-5 bg-linear-to-r from-purple-50 to-blue-50 rounded-2xl border-2 border-purple-200 card-hover"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 bg-purple-500 text-white rounded-xl">
|
||||
<Tag className="w-5 h-5" />
|
||||
</div>
|
||||
<span className="font-bold text-gray-800 text-lg">
|
||||
{category.name}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => handleResetLimit(category.id)}
|
||||
className="flex-1 sm:flex-none flex items-center justify-center gap-2 px-4 py-2.5 bg-yellow-500 hover:bg-yellow-600 text-white rounded-xl transition-all font-medium shadow-md hover:shadow-lg"
|
||||
>
|
||||
<RotateCcw className="w-4 h-4" />
|
||||
<span className="hidden sm:inline">Обнулить</span>
|
||||
<span className="sm:hidden">Сброс</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDeleteCategory(category.id)}
|
||||
className="flex-1 sm:flex-none flex items-center justify-center gap-2 px-4 py-2.5 bg-red-500 hover:bg-red-600 text-white rounded-xl transition-all font-medium shadow-md hover:shadow-lg"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
<span className="hidden sm:inline">Удалить</span>
|
||||
<span className="sm:hidden">X</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user