diff --git a/src/App.jsx b/src/App.jsx index 5f817ee..4168e33 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -13,6 +13,7 @@ import ForgotPassword from "./pages/ForgotPassword" import Stats from "./pages/Stats" import Settings from "./pages/Settings" import Finance from "./pages/Finance" +import Tracker from "./pages/Tracker" function ProtectedRoute({ children }) { const { isAuthenticated, isLoading } = useAuthStore() @@ -93,11 +94,20 @@ export default function App() { } /> + + + + } + /> + {/* Legacy routes redirect to tracker */} - + } /> @@ -105,7 +115,15 @@ export default function App() { path="/tasks" element={ - + + + } + /> + + } /> @@ -117,14 +135,6 @@ export default function App() { } /> - - - - } - /> clsx( - "flex flex-col items-center gap-0.5 px-1.5 py-1.5 rounded-xl transition-all", + "flex flex-col items-center gap-0.5 px-3 py-2 rounded-xl transition-all", isActive ? "text-primary-600 dark:text-primary-400 bg-primary-50 dark:bg-primary-900/30" : "text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300" ) } > - - {label} + + {label} ))} diff --git a/src/components/finance/AddTransactionModal.jsx b/src/components/finance/AddTransactionModal.jsx index 6ac3751..f281309 100644 --- a/src/components/finance/AddTransactionModal.jsx +++ b/src/components/finance/AddTransactionModal.jsx @@ -59,128 +59,138 @@ export default function AddTransactionModal({ onClose, onSaved }) { onClick={onClose} >
e.stopPropagation()} > -
-

- Новая запись -

+ {/* Header - fixed */} +
+
+

+ Новая запись +

+
-
- {/* Type toggle */} -
- {[ - ["expense", "Расход"], - ["income", "Доход"], - ].map(([k, l]) => ( - - ))} -
+ {/* Scrollable content */} +
+
+ {/* Type toggle */} +
+ {[ + ["expense", "Расход"], + ["income", "Доход"], + ].map(([k, l]) => ( + + ))} +
- {/* Quick templates */} - {type === "expense" && ( + {/* Quick templates */} + {type === "expense" && ( +
+

+ Быстрые шаблоны +

+
+ {quickTemplates.map((t, i) => ( + + ))} +
+
+ )} + + {/* Amount */}
-

- Быстрые шаблоны -

-
- {quickTemplates.map((t, i) => ( + +
+ setAmount(e.target.value)} + className="w-full px-4 py-3 rounded-xl bg-gray-100 dark:bg-gray-800 text-2xl font-bold text-gray-900 dark:text-white outline-none" + placeholder="0" + /> + + ₽ + +
+
+ + {/* Description */} +
+ + setDescription(e.target.value)} + className="w-full px-4 py-3 rounded-xl bg-gray-100 dark:bg-gray-800 text-sm text-gray-900 dark:text-white outline-none" + placeholder="Что купили?" + /> +
+ + {/* Category */} +
+ +
+ {cats.map((c) => ( ))}
- )} - {/* Amount */} -
- -
+ {/* Date */} +
+ setAmount(e.target.value)} - className="w-full px-4 py-3 rounded-xl bg-gray-100 dark:bg-gray-800 text-2xl font-bold text-gray-900 dark:text-white outline-none" - placeholder="0" + type="date" + value={date} + onChange={(e) => setDate(e.target.value)} + className="w-full px-4 py-3 rounded-xl bg-gray-100 dark:bg-gray-800 text-sm text-gray-900 dark:text-white outline-none" /> - - ₽ -
+
- {/* Description */} -
- - setDescription(e.target.value)} - className="w-full px-4 py-3 rounded-xl bg-gray-100 dark:bg-gray-800 text-sm text-gray-900 dark:text-white outline-none" - placeholder="Что купили?" - /> -
- - {/* Category */} -
- -
- {cats.map((c) => ( - - ))} -
-
- - {/* Date */} -
- - setDate(e.target.value)} - className="w-full px-4 py-3 rounded-xl bg-gray-100 dark:bg-gray-800 text-sm text-gray-900 dark:text-white outline-none" - /> -
- - {/* Submit */} + {/* Sticky submit button */} +
+ ))} +
+ +
+ + {/* Category list */} +
+ {cats.length === 0 ? ( +
+

Нет категорий

+ +
+ ) : cats.map(c => { + const pct = c.budget > 0 ? Math.min(Math.round((c.spent || 0) / c.budget * 100), 100) : 0 + const isOver = c.budget > 0 && (c.spent || 0) > c.budget * 0.9 + return ( +
+
+
+ {c.emoji} +
+
+
+ {c.name} + {c.budget > 0 && ( + + {fmt(c.spent || 0)} / {fmt(c.budget)} + + )} +
+ {c.budget > 0 && ( +
+
70 ? "bg-yellow-500" : "bg-primary-500"}`} + style={{ width: pct + "%" }} /> +
+ )} +
+
+ + +
+
+
+ ) + })} +
+ + {/* Add/Edit Modal */} + {showModal && ( +
setShowModal(false)}> +
e.stopPropagation()}> +
+

+ {editing ? "Редактировать категорию" : "Новая категория"} +

+
+ {/* Emoji + Name */} +
+ + setName(e.target.value)} + className="flex-1 px-4 py-3 rounded-xl bg-gray-100 dark:bg-gray-800 text-sm text-gray-900 dark:text-white outline-none" + placeholder="Название категории" /> +
+ + {/* Emoji picker */} + {showEmojiPicker && ( +
+ {EMOJI_OPTIONS.map(e => ( + + ))} +
+ )} + + {/* Type toggle */} +
+ {[["expense","Расход"],["income","Доход"]].map(([k,l]) => ( + + ))} +
+ + {/* Budget */} +
+ +
+ setBudget(e.target.value)} + className="w-full px-4 py-3 rounded-xl bg-gray-100 dark:bg-gray-800 text-sm text-gray-900 dark:text-white outline-none" + placeholder="0" /> + +
+
+ + {/* Save */} + +
+
+
+ )} +
+ ) +} diff --git a/src/pages/Finance.jsx b/src/pages/Finance.jsx index 8d849d3..e27dd04 100644 --- a/src/pages/Finance.jsx +++ b/src/pages/Finance.jsx @@ -3,12 +3,14 @@ import Navigation from "../components/Navigation" import FinanceDashboard from "../components/finance/FinanceDashboard" import TransactionList from "../components/finance/TransactionList" import FinanceAnalytics from "../components/finance/FinanceAnalytics" +import CategoriesManager from "../components/finance/CategoriesManager" import AddTransactionModal from "../components/finance/AddTransactionModal" const tabs = [ { key: "dashboard", label: "Обзор", icon: "📊" }, { key: "transactions", label: "Транзакции", icon: "📋" }, { key: "analytics", label: "Аналитика", icon: "📈" }, + { key: "categories", label: "Категории", icon: "🏷️" }, ] export default function Finance() { @@ -34,12 +36,12 @@ export default function Finance() { +
-
+
{tabs.map((t) => (
{showAdd && ( diff --git a/src/pages/Habits.jsx b/src/pages/Habits.jsx index ffe7486..4c3fd3d 100644 --- a/src/pages/Habits.jsx +++ b/src/pages/Habits.jsx @@ -10,7 +10,7 @@ import EditHabitModal from '../components/EditHabitModal' import Navigation from '../components/Navigation' import clsx from 'clsx' -export default function Habits() { +export default function Habits({ embedded = false }) { const [showCreateModal, setShowCreateModal] = useState(false) const [editingHabit, setEditingHabit] = useState(null) const [showArchived, setShowArchived] = useState(false) @@ -67,8 +67,8 @@ export default function Habits() { const archivedList = habits.filter(h => h.is_archived) return ( -
-
+
+ {!embedded &&

Мои привычки

@@ -79,7 +79,7 @@ export default function Habits() { Новая
-
+
}
{isLoading ? ( @@ -168,7 +168,7 @@ export default function Habits() { )}
- + {!embedded && } setShowCreateModal(false)} /> setEditingHabit(null)} habit={editingHabit} />
diff --git a/src/pages/Home.jsx b/src/pages/Home.jsx index 294b140..d623cd2 100644 --- a/src/pages/Home.jsx +++ b/src/pages/Home.jsx @@ -6,6 +6,7 @@ import { format, startOfWeek, differenceInDays, parseISO, isToday, isTomorrow, i import { ru } from 'date-fns/locale' import { habitsApi } from '../api/habits' import { tasksApi } from '../api/tasks' +import { financeApi } from '../api/finance' import { useAuthStore } from '../store/auth' import Navigation from '../components/Navigation' import CreateTaskModal from '../components/CreateTaskModal' @@ -96,6 +97,11 @@ export default function Home() { queryFn: tasksApi.today, }) + + const { data: financeSummary } = useQuery({ + queryKey: ["finance-summary"], + queryFn: () => financeApi.getSummary(), + }) useEffect(() => { if (habits.length > 0) { loadTodayLogs() @@ -301,6 +307,27 @@ export default function Home() { )} {/* Tasks */} + + {/* Finance Summary */} + {financeSummary && ( + +

💰 Баланс

+
+
+

+{(financeSummary.total_income || 0).toLocaleString("ru-RU")} ₽

+

Доходы

+
+
+

-{(financeSummary.total_expense || 0).toLocaleString("ru-RU")} ₽

+

Расходы

+
+
+

= 0 ? "text-primary-500" : "text-red-500")}>{(financeSummary.balance || 0).toLocaleString("ru-RU")} ₽

+

Баланс

+
+
+
+ )} {(activeTasks.length > 0 || !tasksLoading) && (
diff --git a/src/pages/Stats.jsx b/src/pages/Stats.jsx index a6baefe..2a0a47c 100644 --- a/src/pages/Stats.jsx +++ b/src/pages/Stats.jsx @@ -147,7 +147,7 @@ const SectionHeader = ({ icon: Icon, title, subtitle }) => (
) -export default function Stats() { +export default function Stats({ embedded = false }) { const [selectedHabitId, setSelectedHabitId] = useState(null) const [allHabitLogs, setAllHabitLogs] = useState({}) const [allHabitStats, setAllHabitStats] = useState({}) @@ -356,7 +356,7 @@ export default function Stats() {
-
+ {!embedded &&
@@ -371,7 +371,7 @@ export default function Stats() {
-
+
}
@@ -689,7 +689,7 @@ export default function Stats() { )}
- + {!embedded && }
) } diff --git a/src/pages/Tasks.jsx b/src/pages/Tasks.jsx index 2f2a4d8..59594cc 100644 --- a/src/pages/Tasks.jsx +++ b/src/pages/Tasks.jsx @@ -32,7 +32,7 @@ function formatDueDate(dateStr) { return format(date, 'd MMM', { locale: ru }) } -export default function Tasks() { +export default function Tasks({ embedded = false }) { const [showCreate, setShowCreate] = useState(false) const [editingTask, setEditingTask] = useState(null) const [filter, setFilter] = useState('active') @@ -68,8 +68,8 @@ export default function Tasks() { } return ( -
-
+
+ {!embedded &&

Задачи

@@ -99,7 +99,7 @@ export default function Tasks() { ))}
-
+
}
{isLoading ? ( @@ -145,7 +145,7 @@ export default function Tasks() { )}
- + {!embedded && } setShowCreate(false)} /> setEditingTask(null)} task={editingTask} />
diff --git a/src/pages/Tracker.jsx b/src/pages/Tracker.jsx new file mode 100644 index 0000000..81cbbea --- /dev/null +++ b/src/pages/Tracker.jsx @@ -0,0 +1,52 @@ +import { useState, lazy, Suspense } from "react" +import Navigation from "../components/Navigation" + +// Import pages as components (they render their own content but we strip their Navigation) +import HabitsContent from "./Habits" +import TasksContent from "./Tasks" +import StatsContent from "./Stats" + +const tabs = [ + { key: "habits", label: "Привычки", icon: "🎯" }, + { key: "tasks", label: "Задачи", icon: "✅" }, + { key: "stats", label: "Статистика", icon: "📊" }, +] + +export default function Tracker() { + const [activeTab, setActiveTab] = useState("habits") + + return ( +
+
+
+

+ 📊 Трекер +

+
+
+ {tabs.map((t) => ( + + ))} +
+
+ +
+ {activeTab === "habits" && } + {activeTab === "tasks" && } + {activeTab === "stats" && } +
+ + +
+ ) +}