feat(auth): implement JWT authentication flow
This commit implements a full-stack authentication system using JWT. On the backend, a new login endpoint `/api/token` is created. It validates credentials and issues a signed JWT. A JWT middleware is added to the server configuration to protect sensitive API endpoints like `/api/category` and `/api/operation`. Configuration is now managed via a `wallet.conf` file, loaded at startup. On the frontend, this enables a complete authentication lifecycle: - A new `/login` page allows users to enter their credentials. - A `PrivateRoute` component wraps protected pages, redirecting unauthenticated users to the login page. - API calls to protected resources now include the `Authorization: Bearer <token>` header. - A logout function is added to clear user session data. refactor(ui): enhance history and category views This commit also introduces major UI improvements. The History modal has been completely redesigned. Operations are no longer displayed as a flat list but are now grouped into tabs for Daily, Weekly, Monthly, and Yearly views, providing a much clearer overview of transactions. The Categories modal now separates categories into "Incomes" and "Expenses" tabs, reflecting the changes in the backend data model.
This commit is contained in:
1 parent
ca99cfcf6d
commit
1b6c7211ea
35 files changed
+789
-674
No files matched your search
+11
-1
@@ -3,6 +3,7 @@ import NavigationBar from "./components/NavigationBar/NavigationBar";
|
||||
import { useTheme } from "./contexts/ThemeContext";
|
||||
import { useEffect } from "react";
|
||||
import Home from "./pages/Home";
|
||||
import Login from "./pages/Login";
|
||||
import {
|
||||
BrowserRouter as Router,
|
||||
Routes,
|
||||
@@ -10,6 +11,7 @@ import {
|
||||
Navigate,
|
||||
} from "react-router-dom";
|
||||
import NotFound from "./pages/NotFound";
|
||||
import PrivateRoute from "./components/PrivateRoute";
|
||||
|
||||
function App() {
|
||||
const { theme } = useTheme();
|
||||
@@ -23,7 +25,15 @@ function App() {
|
||||
<NavigationBar />
|
||||
<>
|
||||
<Routes>
|
||||
<Route path="/" element={<Home />} />
|
||||
<Route
|
||||
path="/"
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<Home />
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route path="/not-found" element={<NotFound />} />
|
||||
<Route path="*" element={<Navigate to="/not-found" replace />} />
|
||||
</Routes>
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import axios from "axios";
|
||||
import routes from "../routes";
|
||||
|
||||
const fetchCategories = async () => {
|
||||
try {
|
||||
const response = await axios.get(routes.categories);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error("Error fetching categories:", error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export default fetchCategories;
|
||||
@@ -0,0 +1,29 @@
|
||||
const DayTitle = ({ date, income, expense, ...props }) => {
|
||||
return (
|
||||
<div
|
||||
className="d-flex flex-column p-0 justify-content-start align-items-start w-100"
|
||||
{...props}
|
||||
>
|
||||
<h3
|
||||
className="mb-0 fw-bolder"
|
||||
style={{ fontSize: "1.1rem", fontWeight: 700 }}
|
||||
>
|
||||
{/* 25 Jul 2025*/}
|
||||
{date}
|
||||
</h3>
|
||||
<div
|
||||
className="d-flex text-muted small mb-1 w-100"
|
||||
style={{ fontSize: "0.8rem" }}
|
||||
>
|
||||
<div className="w-100">
|
||||
Income: <b>{income} ₽</b>
|
||||
</div>
|
||||
<div className="w-100 text-end">
|
||||
Expense: <b>{expense} ₽</b>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DayTitle;
|
||||
@@ -0,0 +1,12 @@
|
||||
const InputTitle = ({ text, ...props }) => {
|
||||
return (
|
||||
<h3
|
||||
className="mb-0 fw-bolder text-start w-100"
|
||||
style={{ fontSize: "1.1rem", fontWeight: 700 }}
|
||||
{...props}
|
||||
>
|
||||
{text}
|
||||
</h3>
|
||||
);
|
||||
};
|
||||
export default InputTitle;
|
||||
@@ -0,0 +1,7 @@
|
||||
import isAuthenticated from "../logic/isAuthenticated";
|
||||
import Login from "../pages/Login";
|
||||
|
||||
const PrivateRoute = ({ children }) => {
|
||||
return isAuthenticated() ? children : <Login />;
|
||||
};
|
||||
export default PrivateRoute;
|
||||
@@ -0,0 +1,18 @@
|
||||
import routes from "../../routes";
|
||||
|
||||
const fetchCategories = async () => {
|
||||
try {
|
||||
const response = await fetch(routes.categories, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem("token")}`,
|
||||
},
|
||||
});
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error("Error fetching categories:", error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export default fetchCategories;
|
||||
@@ -0,0 +1,7 @@
|
||||
const isAuthenticated = () => {
|
||||
if (localStorage.getItem("token") === null) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
export default isAuthenticated;
|
||||
@@ -0,0 +1,33 @@
|
||||
import routes from "../routes";
|
||||
|
||||
const login = async (userdata, e) => {
|
||||
e.preventDefault();
|
||||
console.log({
|
||||
username: userdata.username,
|
||||
password: userdata.password,
|
||||
});
|
||||
const response = await fetch(routes.token, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
username: userdata.username,
|
||||
password: userdata.password,
|
||||
}),
|
||||
});
|
||||
if (response.status === 200) {
|
||||
const data = await response.json();
|
||||
localStorage.setItem("token", data.token);
|
||||
window.location.reload();
|
||||
return true;
|
||||
}
|
||||
if (response.status === 401) {
|
||||
return false;
|
||||
}
|
||||
if (response.status === 500) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export default login;
|
||||
@@ -0,0 +1,6 @@
|
||||
const logout = () => {
|
||||
localStorage.clear();
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
export default logout;
|
||||
@@ -0,0 +1,13 @@
|
||||
import routes from "../../routes";
|
||||
|
||||
const GetAllOperation = async () => {
|
||||
const response = await fetch(routes.operations, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem("token")}`,
|
||||
},
|
||||
});
|
||||
const data = await response.json();
|
||||
return data;
|
||||
};
|
||||
|
||||
export default GetAllOperation;
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState } from "react";
|
||||
import { useTheme } from "../../contexts/ThemeContext";
|
||||
import { Modal, Button, Card, Form } from "react-bootstrap";
|
||||
import EmojiPickerButton from "../../components/EmojiPickerButton/EmojiPickerButton";
|
||||
import EmojiPickerButton from "./components/EmojiPickerButton/EmojiPickerButton";
|
||||
|
||||
const AddCategoryModal = ({ variant, ...props }) => {
|
||||
const { theme } = useTheme();
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import React, { useState, useRef } from "react";
|
||||
import { Button, Overlay, Popover, InputGroup } from "react-bootstrap";
|
||||
import EmojiPicker from "emoji-picker-react";
|
||||
import { useTheme } from "../../contexts/ThemeContext";
|
||||
import { useTheme } from "../../../../contexts/ThemeContext";
|
||||
|
||||
const EmojiPickerButton = () => {
|
||||
const { theme } = useTheme();
|
||||
@@ -11,12 +11,13 @@ import {
|
||||
DropdownButton,
|
||||
} from "react-bootstrap";
|
||||
import styles from "./CategoriesModal.module.css";
|
||||
import CategorySign from "../../components/CategorySign/CategorySign";
|
||||
import CategorySign from "./components/CategorySign/CategorySign";
|
||||
import AddCategoryModal from "../../modals/AddCategoryModal/AddCategoryModal";
|
||||
import fetchCategories from "../../category/GetAllCategories";
|
||||
import fetchCategories from "../../logic/category/GetAllCategories";
|
||||
|
||||
const CategoriesModal = ({ variant }) => {
|
||||
let [categories, setCategories] = useState([]);
|
||||
let [expenseCategories, setExpenseCategories] = useState([]);
|
||||
const [incomeCategories, setIncomeCategories] = useState([]);
|
||||
const { theme } = useTheme();
|
||||
const [show, setShow] = useState(false);
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
@@ -24,17 +25,29 @@ const CategoriesModal = ({ variant }) => {
|
||||
const handleShow = () => setShow(true);
|
||||
useEffect(() => {
|
||||
fetchCategories().then((data) => {
|
||||
setCategories(data);
|
||||
setExpenseCategories(
|
||||
data.filter((category) => category.type === "expense"),
|
||||
);
|
||||
setIncomeCategories(
|
||||
data.filter((category) => category.type === "income"),
|
||||
);
|
||||
console.log(data);
|
||||
});
|
||||
}, []);
|
||||
const filteredCategories = useMemo(() => {
|
||||
if (!searchTerm) return categories;
|
||||
const filteredExpenseCategories = useMemo(() => {
|
||||
if (!searchTerm) return expenseCategories;
|
||||
const term = searchTerm.toLowerCase();
|
||||
return categories.filter((category) =>
|
||||
return expenseCategories.filter((category) =>
|
||||
category.title.toLowerCase().includes(term),
|
||||
);
|
||||
}, [categories, searchTerm]);
|
||||
}, [expenseCategories, searchTerm]);
|
||||
const filteredIncomeCategories = useMemo(() => {
|
||||
if (!searchTerm) return incomeCategories;
|
||||
const term = searchTerm.toLowerCase();
|
||||
return incomeCategories.filter((category) =>
|
||||
category.title.toLowerCase().includes(term),
|
||||
);
|
||||
}, [incomeCategories, searchTerm]);
|
||||
const handleSearch = useCallback((e) => {
|
||||
setSearchTerm(e.target.value);
|
||||
}, []);
|
||||
@@ -61,7 +74,7 @@ const CategoriesModal = ({ variant }) => {
|
||||
<div>Settings</div>
|
||||
</Button>
|
||||
<Modal.Title>Categories</Modal.Title>
|
||||
<spacer />
|
||||
<div></div>
|
||||
<DropdownButton title="☰" variant={theme} drop="start">
|
||||
<Dropdown.Item className="p-0">
|
||||
<AddCategoryModal
|
||||
@@ -93,16 +106,30 @@ const CategoriesModal = ({ variant }) => {
|
||||
className="nav nav-tabs"
|
||||
>
|
||||
<Tab eventKey="Incomes" title="Incomes">
|
||||
asdf
|
||||
<div
|
||||
className={styles.scrollableContent}
|
||||
style={{ padding: "10px" }}
|
||||
>
|
||||
{filteredIncomeCategories.map((category) => (
|
||||
<CategorySign
|
||||
key={`${category.id}/expense`}
|
||||
icon={category.icon}
|
||||
title={category.title}
|
||||
subcategories={
|
||||
category.subcategories ? category.subcategories : []
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Tab>
|
||||
<Tab eventKey="Expenses" title="Expenses">
|
||||
<div
|
||||
className={styles.scrollableContent}
|
||||
style={{ padding: "10px" }}
|
||||
>
|
||||
{filteredCategories.map((category) => (
|
||||
{filteredExpenseCategories.map((category) => (
|
||||
<CategorySign
|
||||
key={category.id}
|
||||
key={`${category.id}/income`}
|
||||
icon={category.icon}
|
||||
title={category.title}
|
||||
subcategories={
|
||||
|
||||
+5
-8
@@ -1,4 +1,4 @@
|
||||
import { useTheme } from "../../contexts/ThemeContext";
|
||||
import { useTheme } from "../../../../contexts/ThemeContext";
|
||||
import { Button } from "react-bootstrap";
|
||||
import styles from "./CategorySign.module.css";
|
||||
import classNames from "classnames";
|
||||
@@ -45,18 +45,15 @@ const CategorySign = ({ icon, title, subcategories }) => {
|
||||
>
|
||||
<div>➞</div>
|
||||
</div>
|
||||
<div
|
||||
className={classNames("d-flex gap-3", styles.hideScrollbar)}
|
||||
style={{ width: "100%", overflowX: "auto" }}
|
||||
>
|
||||
<div className={classNames("d-flex gap-3", styles.hideScrollbar)}>
|
||||
{subcategories.map((subcategory) => (
|
||||
<Button
|
||||
variant="secondary"
|
||||
<div
|
||||
className="btn btn-secondary"
|
||||
style={{ borderRadius: "25px", padding: "0 15px" }}
|
||||
key={subcategory.id}
|
||||
>
|
||||
{subcategory.title}
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
File renamed without changes.
@@ -3,19 +3,49 @@ import { useTheme } from "../../contexts/ThemeContext";
|
||||
import { ListGroup, Tabs, Tab, Modal, Button } from "react-bootstrap";
|
||||
import styles from "./HistoryModal.module.css";
|
||||
import AddOperationModal from "../AddOperationModal/AddOperationModal";
|
||||
import GetAllOperation from "../../operation/GetAllOperation";
|
||||
import OperationSign from "../../components/OperationSign/OperationSign";
|
||||
import GetAllOperation from "../../logic/operation/GetAllOperation";
|
||||
import OperationSign from "./components/OperationSign/OperationSign";
|
||||
import DayTitle from "../../components/DayTitle";
|
||||
import {
|
||||
processMonthlyOperations,
|
||||
processOperations,
|
||||
processWeeklyOperations,
|
||||
processYearlyOperations,
|
||||
} from "./logic/groupOperations";
|
||||
import { formatWeekRange } from "./logic/week";
|
||||
|
||||
const HistoryModal = () => {
|
||||
const { theme } = useTheme();
|
||||
const [show, setShow] = useState(false);
|
||||
const handleClose = () => setShow(false);
|
||||
const handleShow = () => setShow(true);
|
||||
const [operations, setOperations] = useState([]);
|
||||
const [grouped, setGrouped] = useState({});
|
||||
const [sortedGroupKeys, setSortedGroupKeys] = useState([]);
|
||||
const [groupedWeek, setGroupedWeek] = useState({});
|
||||
const [sortedGroupKeysWeek, setSortedGroupKeysWeek] = useState([]);
|
||||
const [groupedMonth, setGroupedMonth] = useState({});
|
||||
const [sortedGroupKeysMonth, setSortedGroupKeysMonth] = useState([]);
|
||||
const [groupedYear, setGroupedYear] = useState({});
|
||||
const [sortedGroupKeysYear, setSortedGroupKeysYear] = useState([]);
|
||||
useEffect(() => {
|
||||
GetAllOperation().then((data) => {
|
||||
console.log(data);
|
||||
setOperations(data);
|
||||
const processop = processOperations(data);
|
||||
setGrouped(processop.grouped);
|
||||
setSortedGroupKeys(processop.sortedGroupKeys);
|
||||
console.log(processop, processOperations(data));
|
||||
const processopWeek = processWeeklyOperations(data);
|
||||
setGroupedWeek(processopWeek.grouped);
|
||||
setSortedGroupKeysWeek(processopWeek.sortedKeys);
|
||||
console.log(processopWeek, processWeeklyOperations(data));
|
||||
const processopMonth = processMonthlyOperations(data);
|
||||
setGroupedMonth(processopMonth.grouped);
|
||||
setSortedGroupKeysMonth(processopMonth.sortedKeys);
|
||||
console.log(processopMonth, processMonthlyOperations(data));
|
||||
const processopYear = processYearlyOperations(data);
|
||||
setGroupedYear(processopYear.grouped);
|
||||
setSortedGroupKeysYear(processopYear.sortedKeys);
|
||||
console.log(processopYear, processYearlyOperations(data));
|
||||
});
|
||||
}, []);
|
||||
return (
|
||||
@@ -40,627 +70,113 @@ const HistoryModal = () => {
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<Tabs defaultActiveKey="D" id="history-tabs">
|
||||
<Tab
|
||||
eventKey="D"
|
||||
title="D"
|
||||
className="pt-2 pb-2"
|
||||
// style={{ maxHeight: "calc(100vh - 200px)", overflowY: "auto" }}
|
||||
>
|
||||
<Tab eventKey="D" title="D" className="pt-2 pb-2">
|
||||
<div className={styles.scrollableContent}>
|
||||
{operations.map((operation) => {
|
||||
{sortedGroupKeys.map((dateKey) => {
|
||||
const group = grouped[dateKey];
|
||||
return (
|
||||
<ListGroup className="mb-3 mt-0">
|
||||
<ListGroup.Item className="p-0">
|
||||
<OperationSign operation={operation} />
|
||||
</ListGroup.Item>
|
||||
</ListGroup>
|
||||
<div key={dateKey}>
|
||||
<DayTitle
|
||||
date={group.dateObj.toDateString()}
|
||||
income={group.income}
|
||||
expense={group.expense}
|
||||
/>
|
||||
<ListGroup className="mb-3 mt-0">
|
||||
{group.operations.map((operation) => (
|
||||
<ListGroup.Item
|
||||
className="p-0"
|
||||
key={`${operation.id}/${operation.date}/day`}
|
||||
>
|
||||
<OperationSign operation={operation} />
|
||||
</ListGroup.Item>
|
||||
))}
|
||||
</ListGroup>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Tab>
|
||||
<Tab eventKey="W" title="W" className="pt-2 pb-2">
|
||||
<div className={styles.scrollableContent}>
|
||||
{sortedGroupKeysWeek.map((weekKey) => {
|
||||
const group = groupedWeek[weekKey];
|
||||
return (
|
||||
<div key={weekKey}>
|
||||
<DayTitle
|
||||
date={formatWeekRange(group.monday)}
|
||||
income={group.income}
|
||||
expense={group.expense}
|
||||
/>
|
||||
<ListGroup className="mb-3 mt-0">
|
||||
{group.operations.map((operation) => (
|
||||
<ListGroup.Item
|
||||
className="p-0"
|
||||
key={`${operation.id}/${operation.date}/week`}
|
||||
>
|
||||
<OperationSign operation={operation} />
|
||||
</ListGroup.Item>
|
||||
))}
|
||||
</ListGroup>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Tab>
|
||||
<Tab eventKey="M" title="M" className="pt-2 pb-2">
|
||||
<div className={styles.scrollableContent}>
|
||||
{sortedGroupKeysMonth.map((monthKey) => {
|
||||
const group = groupedMonth[monthKey];
|
||||
return (
|
||||
<div key={monthKey}>
|
||||
<DayTitle
|
||||
date={group.monthDate.toLocaleDateString("ru-RU", {
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
})}
|
||||
income={group.income}
|
||||
expense={group.expense}
|
||||
/>
|
||||
<ListGroup className="mb-3 mt-0">
|
||||
{group.operations.map((operation) => (
|
||||
<ListGroup.Item
|
||||
className="p-0"
|
||||
key={`${operation.id}/${operation.date}/month`}
|
||||
>
|
||||
<OperationSign operation={operation} />
|
||||
</ListGroup.Item>
|
||||
))}
|
||||
</ListGroup>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Tab>
|
||||
<Tab eventKey="Y" title="Y" className="pt-2 pb-2">
|
||||
<div className={styles.scrollableContent}>
|
||||
{sortedGroupKeysYear.map((yearKey) => {
|
||||
const group = groupedYear[yearKey];
|
||||
return (
|
||||
<div key={yearKey}>
|
||||
<DayTitle
|
||||
date={group.year}
|
||||
income={group.income}
|
||||
expense={group.expense}
|
||||
/>
|
||||
<ListGroup className="mb-3 mt-0">
|
||||
{group.operations.map((operation) => (
|
||||
<ListGroup.Item
|
||||
className="p-0"
|
||||
key={`${operation.id}/${operation.date}/year`}
|
||||
>
|
||||
<OperationSign operation={operation} />
|
||||
</ListGroup.Item>
|
||||
))}
|
||||
</ListGroup>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{/* <div className="d-flex m-1">
|
||||
<div className="d-flex flex-column p-0 justify-content-start align-items-start w-100">
|
||||
<h3
|
||||
className="mb-0 fw-bolder"
|
||||
style={{ fontSize: "1.1rem", fontWeight: 700 }}
|
||||
>
|
||||
25 Jul 2025
|
||||
</h3>
|
||||
<div
|
||||
className="d-flex text-muted small mb-1 w-100"
|
||||
style={{ fontSize: "0.8rem" }}
|
||||
>
|
||||
<div className="w-100">
|
||||
Income: <b>0,00 ₽</b>
|
||||
</div>
|
||||
<div className="w-100 text-end">
|
||||
Expense: <b>0,00 ₽</b>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ListGroup className="mb-3 mt-0">
|
||||
<ListGroup.Item className="p-0">
|
||||
<div className="d-flex flex-row gap-3 p-2">
|
||||
<div
|
||||
className="p-2 justify-content-start align-items-start d-flex flex-column"
|
||||
style={{ fontSize: "30px" }}
|
||||
>
|
||||
<div>😀</div>
|
||||
</div>
|
||||
<div className="d-flex flex-column p-0 justify-content-start align-items-start w-100">
|
||||
<h3
|
||||
className="mb-0 fw-bolder"
|
||||
style={{ fontSize: "1.1rem", fontWeight: 700 }}
|
||||
>
|
||||
-63.00 ₽
|
||||
</h3>
|
||||
<div
|
||||
className="text-muted small mb-1"
|
||||
style={{ fontSize: "0.8rem" }}
|
||||
>
|
||||
Transport
|
||||
</div>
|
||||
<div
|
||||
className="text-muted small mb-1"
|
||||
style={{ fontSize: "0.8rem" }}
|
||||
>
|
||||
Note
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="p-0 m-0"
|
||||
style={{ width: "100%", height: "100%" }}
|
||||
>
|
||||
<div
|
||||
className="text-muted small mb-1 text-end"
|
||||
style={{ fontSize: "0.8rem" }}
|
||||
>
|
||||
09:14
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ListGroup.Item>
|
||||
<ListGroup.Item className="p-0">
|
||||
<div className="d-flex flex-row gap-3 p-2">
|
||||
<div
|
||||
className="p-2 justify-content-start align-items-start d-flex flex-column"
|
||||
style={{ fontSize: "30px" }}
|
||||
>
|
||||
<div>🗼</div>
|
||||
</div>
|
||||
<div className="d-flex flex-column p-0 justify-content-start align-items-start w-100">
|
||||
<h3
|
||||
className="mb-0 fw-bolder"
|
||||
style={{
|
||||
fontSize: "1.1rem",
|
||||
fontWeight: 700,
|
||||
color: "#2DBB4D",
|
||||
}}
|
||||
>
|
||||
18000 ₽
|
||||
</h3>
|
||||
<div
|
||||
className="text-muted small mb-1"
|
||||
style={{ fontSize: "0.8rem" }}
|
||||
>
|
||||
Transport
|
||||
</div>
|
||||
<div
|
||||
className="text-muted small mb-1"
|
||||
style={{ fontSize: "0.8rem" }}
|
||||
>
|
||||
Note
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="p-0 m-0"
|
||||
style={{ width: "100%", height: "100%" }}
|
||||
>
|
||||
<div
|
||||
className="text-muted small mb-1 text-end"
|
||||
style={{ fontSize: "0.8rem" }}
|
||||
>
|
||||
09:14
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ListGroup.Item>
|
||||
<ListGroup.Item className="p-0">
|
||||
<div className="d-flex flex-row gap-3 p-2">
|
||||
<div
|
||||
className="p-2 justify-content-start align-items-start d-flex flex-column"
|
||||
style={{ fontSize: "30px" }}
|
||||
>
|
||||
<div>😀</div>
|
||||
</div>
|
||||
<div className="d-flex flex-column p-0 justify-content-start align-items-start w-100">
|
||||
<h3
|
||||
className="mb-0 fw-bolder"
|
||||
style={{ fontSize: "1.1rem", fontWeight: 700 }}
|
||||
>
|
||||
-63.00 ₽
|
||||
</h3>
|
||||
<div
|
||||
className="text-muted small mb-1"
|
||||
style={{ fontSize: "0.8rem" }}
|
||||
>
|
||||
Transport
|
||||
</div>
|
||||
<div
|
||||
className="text-muted small mb-1"
|
||||
style={{ fontSize: "0.8rem" }}
|
||||
>
|
||||
Note
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="p-0 m-0"
|
||||
style={{ width: "100%", height: "100%" }}
|
||||
>
|
||||
<div
|
||||
className="text-muted small mb-1 text-end"
|
||||
style={{ fontSize: "0.8rem" }}
|
||||
>
|
||||
09:14
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ListGroup.Item>
|
||||
</ListGroup>
|
||||
<div className="d-flex m-1">
|
||||
<div className="d-flex flex-column p-0 justify-content-start align-items-start w-100">
|
||||
<h3
|
||||
className="mb-0 fw-bolder"
|
||||
style={{ fontSize: "1.1rem", fontWeight: 700 }}
|
||||
>
|
||||
25 Jul 2025
|
||||
</h3>
|
||||
<div
|
||||
className="d-flex text-muted small mb-1 w-100"
|
||||
style={{ fontSize: "0.8rem" }}
|
||||
>
|
||||
<div className="w-100">
|
||||
Income: <b>0,00 ₽</b>
|
||||
</div>
|
||||
<div className="w-100 text-end">
|
||||
Expense: <b>0,00 ₽</b>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ListGroup className="mb-3 mt-0">
|
||||
<ListGroup.Item className="p-0">
|
||||
<div className="d-flex flex-row gap-3 p-2">
|
||||
<div
|
||||
className="p-2 justify-content-start align-items-start d-flex flex-column"
|
||||
style={{ fontSize: "30px" }}
|
||||
>
|
||||
<div>😀</div>
|
||||
</div>
|
||||
<div className="d-flex flex-column p-0 justify-content-start align-items-start w-100">
|
||||
<h3
|
||||
className="mb-0 fw-bolder"
|
||||
style={{ fontSize: "1.1rem", fontWeight: 700 }}
|
||||
>
|
||||
-63.00 ₽
|
||||
</h3>
|
||||
<div
|
||||
className="text-muted small mb-1"
|
||||
style={{ fontSize: "0.8rem" }}
|
||||
>
|
||||
Transport
|
||||
</div>
|
||||
<div
|
||||
className="text-muted small mb-1"
|
||||
style={{ fontSize: "0.8rem" }}
|
||||
>
|
||||
Note
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="p-0 m-0"
|
||||
style={{ width: "100%", height: "100%" }}
|
||||
>
|
||||
<div
|
||||
className="text-muted small mb-1 text-end"
|
||||
style={{ fontSize: "0.8rem" }}
|
||||
>
|
||||
09:14
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ListGroup.Item>
|
||||
<ListGroup.Item className="p-0">
|
||||
<div className="d-flex flex-row gap-3 p-2">
|
||||
<div
|
||||
className="p-2 justify-content-start align-items-start d-flex flex-column"
|
||||
style={{ fontSize: "30px" }}
|
||||
>
|
||||
<div>🗼</div>
|
||||
</div>
|
||||
<div className="d-flex flex-column p-0 justify-content-start align-items-start w-100">
|
||||
<h3
|
||||
className="mb-0 fw-bolder"
|
||||
style={{
|
||||
fontSize: "1.1rem",
|
||||
fontWeight: 700,
|
||||
color: "#2DBB4D",
|
||||
}}
|
||||
>
|
||||
18000 ₽
|
||||
</h3>
|
||||
<div
|
||||
className="text-muted small mb-1"
|
||||
style={{ fontSize: "0.8rem" }}
|
||||
>
|
||||
Transport
|
||||
</div>
|
||||
<div
|
||||
className="text-muted small mb-1"
|
||||
style={{ fontSize: "0.8rem" }}
|
||||
>
|
||||
Note
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="p-0 m-0"
|
||||
style={{ width: "100%", height: "100%" }}
|
||||
>
|
||||
<div
|
||||
className="text-muted small mb-1 text-end"
|
||||
style={{ fontSize: "0.8rem" }}
|
||||
>
|
||||
09:14
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ListGroup.Item>
|
||||
<ListGroup.Item className="p-0">
|
||||
<div className="d-flex flex-row gap-3 p-2">
|
||||
<div
|
||||
className="p-2 justify-content-start align-items-start d-flex flex-column"
|
||||
style={{ fontSize: "30px" }}
|
||||
>
|
||||
<div>😀</div>
|
||||
</div>
|
||||
<div className="d-flex flex-column p-0 justify-content-start align-items-start w-100">
|
||||
<h3
|
||||
className="mb-0 fw-bolder"
|
||||
style={{ fontSize: "1.1rem", fontWeight: 700 }}
|
||||
>
|
||||
-63.00 ₽
|
||||
</h3>
|
||||
<div
|
||||
className="text-muted small mb-1"
|
||||
style={{ fontSize: "0.8rem" }}
|
||||
>
|
||||
Transport
|
||||
</div>
|
||||
<div
|
||||
className="text-muted small mb-1"
|
||||
style={{ fontSize: "0.8rem" }}
|
||||
>
|
||||
Note
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="p-0 m-0"
|
||||
style={{ width: "100%", height: "100%" }}
|
||||
>
|
||||
<div
|
||||
className="text-muted small mb-1 text-end"
|
||||
style={{ fontSize: "0.8rem" }}
|
||||
>
|
||||
09:14
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ListGroup.Item>
|
||||
</ListGroup>
|
||||
<div className="d-flex m-1">
|
||||
<div className="d-flex flex-column p-0 justify-content-start align-items-start w-100">
|
||||
<h3
|
||||
className="mb-0 fw-bolder"
|
||||
style={{ fontSize: "1.1rem", fontWeight: 700 }}
|
||||
>
|
||||
25 Jul 2025
|
||||
</h3>
|
||||
<div
|
||||
className="d-flex text-muted small mb-1 w-100"
|
||||
style={{ fontSize: "0.8rem" }}
|
||||
>
|
||||
<div className="w-100">
|
||||
Income: <b>0,00 ₽</b>
|
||||
</div>
|
||||
<div className="w-100 text-end">
|
||||
Expense: <b>0,00 ₽</b>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ListGroup className="mb-3 mt-0">
|
||||
<ListGroup.Item className="p-0">
|
||||
<div className="d-flex flex-row gap-3 p-2">
|
||||
<div
|
||||
className="p-2 justify-content-start align-items-start d-flex flex-column"
|
||||
style={{ fontSize: "30px" }}
|
||||
>
|
||||
<div>😀</div>
|
||||
</div>
|
||||
<div className="d-flex flex-column p-0 justify-content-start align-items-start w-100">
|
||||
<h3
|
||||
className="mb-0 fw-bolder"
|
||||
style={{ fontSize: "1.1rem", fontWeight: 700 }}
|
||||
>
|
||||
-63.00 ₽
|
||||
</h3>
|
||||
<div
|
||||
className="text-muted small mb-1"
|
||||
style={{ fontSize: "0.8rem" }}
|
||||
>
|
||||
Transport
|
||||
</div>
|
||||
<div
|
||||
className="text-muted small mb-1"
|
||||
style={{ fontSize: "0.8rem" }}
|
||||
>
|
||||
Note
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="p-0 m-0"
|
||||
style={{ width: "100%", height: "100%" }}
|
||||
>
|
||||
<div
|
||||
className="text-muted small mb-1 text-end"
|
||||
style={{ fontSize: "0.8rem" }}
|
||||
>
|
||||
09:14
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ListGroup.Item>
|
||||
<ListGroup.Item className="p-0">
|
||||
<div className="d-flex flex-row gap-3 p-2">
|
||||
<div
|
||||
className="p-2 justify-content-start align-items-start d-flex flex-column"
|
||||
style={{ fontSize: "30px" }}
|
||||
>
|
||||
<div>🗼</div>
|
||||
</div>
|
||||
<div className="d-flex flex-column p-0 justify-content-start align-items-start w-100">
|
||||
<h3
|
||||
className="mb-0 fw-bolder"
|
||||
style={{
|
||||
fontSize: "1.1rem",
|
||||
fontWeight: 700,
|
||||
color: "#2DBB4D",
|
||||
}}
|
||||
>
|
||||
18000 ₽
|
||||
</h3>
|
||||
<div
|
||||
className="text-muted small mb-1"
|
||||
style={{ fontSize: "0.8rem" }}
|
||||
>
|
||||
Transport
|
||||
</div>
|
||||
<div
|
||||
className="text-muted small mb-1"
|
||||
style={{ fontSize: "0.8rem" }}
|
||||
>
|
||||
Note
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="p-0 m-0"
|
||||
style={{ width: "100%", height: "100%" }}
|
||||
>
|
||||
<div
|
||||
className="text-muted small mb-1 text-end"
|
||||
style={{ fontSize: "0.8rem" }}
|
||||
>
|
||||
09:14
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ListGroup.Item>
|
||||
<ListGroup.Item className="p-0">
|
||||
<div className="d-flex flex-row gap-3 p-2">
|
||||
<div
|
||||
className="p-2 justify-content-start align-items-start d-flex flex-column"
|
||||
style={{ fontSize: "30px" }}
|
||||
>
|
||||
<div>😀</div>
|
||||
</div>
|
||||
<div className="d-flex flex-column p-0 justify-content-start align-items-start w-100">
|
||||
<h3
|
||||
className="mb-0 fw-bolder"
|
||||
style={{ fontSize: "1.1rem", fontWeight: 700 }}
|
||||
>
|
||||
-63.00 ₽
|
||||
</h3>
|
||||
<div
|
||||
className="text-muted small mb-1"
|
||||
style={{ fontSize: "0.8rem" }}
|
||||
>
|
||||
Transport
|
||||
</div>
|
||||
<div
|
||||
className="text-muted small mb-1"
|
||||
style={{ fontSize: "0.8rem" }}
|
||||
>
|
||||
Note
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="p-0 m-0"
|
||||
style={{ width: "100%", height: "100%" }}
|
||||
>
|
||||
<div
|
||||
className="text-muted small mb-1 text-end"
|
||||
style={{ fontSize: "0.8rem" }}
|
||||
>
|
||||
09:14
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ListGroup.Item>
|
||||
</ListGroup>
|
||||
<div className="d-flex m-1">
|
||||
<div className="d-flex flex-column p-0 justify-content-start align-items-start w-100">
|
||||
<h3
|
||||
className="mb-0 fw-bolder"
|
||||
style={{ fontSize: "1.1rem", fontWeight: 700 }}
|
||||
>
|
||||
25 Jul 2025
|
||||
</h3>
|
||||
<div
|
||||
className="d-flex text-muted small mb-1 w-100"
|
||||
style={{ fontSize: "0.8rem" }}
|
||||
>
|
||||
<div className="w-100">
|
||||
Income: <b>0,00 ₽</b>
|
||||
</div>
|
||||
<div className="w-100 text-end">
|
||||
Expense: <b>0,00 ₽</b>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ListGroup className="mb-3 mt-0">
|
||||
<ListGroup.Item className="p-0">
|
||||
<div className="d-flex flex-row gap-3 p-2">
|
||||
<div
|
||||
className="p-2 justify-content-start align-items-start d-flex flex-column"
|
||||
style={{ fontSize: "30px" }}
|
||||
>
|
||||
<div>😀</div>
|
||||
</div>
|
||||
<div className="d-flex flex-column p-0 justify-content-start align-items-start w-100">
|
||||
<h3
|
||||
className="mb-0 fw-bolder"
|
||||
style={{ fontSize: "1.1rem", fontWeight: 700 }}
|
||||
>
|
||||
-63.00 ₽
|
||||
</h3>
|
||||
<div
|
||||
className="text-muted small mb-1"
|
||||
style={{ fontSize: "0.8rem" }}
|
||||
>
|
||||
Transport
|
||||
</div>
|
||||
<div
|
||||
className="text-muted small mb-1"
|
||||
style={{ fontSize: "0.8rem" }}
|
||||
>
|
||||
Note
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="p-0 m-0"
|
||||
style={{ width: "100%", height: "100%" }}
|
||||
>
|
||||
<div
|
||||
className="text-muted small mb-1 text-end"
|
||||
style={{ fontSize: "0.8rem" }}
|
||||
>
|
||||
09:14
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ListGroup.Item>
|
||||
<ListGroup.Item className="p-0">
|
||||
<div className="d-flex flex-row gap-3 p-2">
|
||||
<div
|
||||
className="p-2 justify-content-start align-items-start d-flex flex-column"
|
||||
style={{ fontSize: "30px" }}
|
||||
>
|
||||
<div>🗼</div>
|
||||
</div>
|
||||
<div className="d-flex flex-column p-0 justify-content-start align-items-start w-100">
|
||||
<h3
|
||||
className="mb-0 fw-bolder"
|
||||
style={{
|
||||
fontSize: "1.1rem",
|
||||
fontWeight: 700,
|
||||
color: "#2DBB4D",
|
||||
}}
|
||||
>
|
||||
18000 ₽
|
||||
</h3>
|
||||
<div
|
||||
className="text-muted small mb-1"
|
||||
style={{ fontSize: "0.8rem" }}
|
||||
>
|
||||
Transport
|
||||
</div>
|
||||
<div
|
||||
className="text-muted small mb-1"
|
||||
style={{ fontSize: "0.8rem" }}
|
||||
>
|
||||
Note
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="p-0 m-0"
|
||||
style={{ width: "100%", height: "100%" }}
|
||||
>
|
||||
<div
|
||||
className="text-muted small mb-1 text-end"
|
||||
style={{ fontSize: "0.8rem" }}
|
||||
>
|
||||
09:14
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ListGroup.Item>
|
||||
<ListGroup.Item className="p-0">
|
||||
<div className="d-flex flex-row gap-3 p-2">
|
||||
<div
|
||||
className="p-2 justify-content-start align-items-start d-flex flex-column"
|
||||
style={{ fontSize: "30px" }}
|
||||
>
|
||||
<div>😀</div>
|
||||
</div>
|
||||
<div className="d-flex flex-column p-0 justify-content-start align-items-start w-100">
|
||||
<h3
|
||||
className="mb-0 fw-bolder"
|
||||
style={{ fontSize: "1.1rem", fontWeight: 700 }}
|
||||
>
|
||||
-63.00 ₽
|
||||
</h3>
|
||||
<div
|
||||
className="text-muted small mb-1"
|
||||
style={{ fontSize: "0.8rem" }}
|
||||
>
|
||||
Transport
|
||||
</div>
|
||||
<div
|
||||
className="text-muted small mb-1"
|
||||
style={{ fontSize: "0.8rem" }}
|
||||
>
|
||||
Note
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="p-0 m-0"
|
||||
style={{ width: "100%", height: "100%" }}
|
||||
>
|
||||
<div
|
||||
className="text-muted small mb-1 text-end"
|
||||
style={{ fontSize: "0.8rem" }}
|
||||
>
|
||||
09:14
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ListGroup.Item> */}
|
||||
{/* </ListGroup> */}
|
||||
</div>
|
||||
</Tab>
|
||||
<Tab eventKey="W" title="W" className="pt-2 pb-2"></Tab>
|
||||
<Tab eventKey="M" title="M" className="pt-2 pb-2"></Tab>
|
||||
<Tab eventKey="Y" title="Y" className="pt-2 pb-2"></Tab>
|
||||
</Tabs>
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
|
||||
+4
-4
@@ -1,7 +1,7 @@
|
||||
import getDateString from "../../logic/getDateString";
|
||||
import { Button } from "react-bootstrap";
|
||||
import classNames from "classnames";
|
||||
import styles from "./OperationSign.module.css";
|
||||
import getDateString from "../../../../logic/getDateString";
|
||||
// import { Button } from "react-bootstrap";
|
||||
// import classNames from "classnames";
|
||||
// import styles from "./OperationSign.module.css";
|
||||
|
||||
const OperationSign = ({ operation }) => {
|
||||
return (
|
||||
File renamed without changes.
@@ -0,0 +1,175 @@
|
||||
import { getMonday } from "./week";
|
||||
|
||||
const groupOperationsByDate = (operations) => {
|
||||
const groups = {};
|
||||
operations.forEach((operation) => {
|
||||
const date = new Date(operation.date);
|
||||
|
||||
const dateKey = [
|
||||
date.getFullYear(),
|
||||
String(date.getMonth() + 1).padStart(2, "0"),
|
||||
String(date.getDate()).padStart(2, "0"),
|
||||
].join("-");
|
||||
|
||||
if (!groups[dateKey]) {
|
||||
groups[dateKey] = {
|
||||
dateObj: new Date(date.getFullYear(), date.getMonth(), date.getDate()),
|
||||
operations: [],
|
||||
income: 0,
|
||||
expense: 0,
|
||||
};
|
||||
}
|
||||
groups[dateKey].operations.push(operation);
|
||||
operation.type === "income"
|
||||
? (groups[dateKey].income += operation.amount)
|
||||
: (groups[dateKey].expense += operation.amount);
|
||||
});
|
||||
return groups;
|
||||
};
|
||||
|
||||
const processOperations = (operations) => {
|
||||
const grouped = groupOperationsByDate(operations);
|
||||
const sortedGroupKeys = Object.keys(grouped).sort(
|
||||
(a, b) => new Date(b) - new Date(a),
|
||||
);
|
||||
|
||||
sortedGroupKeys.forEach((key) => {
|
||||
grouped[key].operations.sort((a, b) => new Date(b.date) - new Date(a.date));
|
||||
});
|
||||
return { grouped, sortedGroupKeys };
|
||||
};
|
||||
|
||||
const groupOperationsByWeek = (operations) => {
|
||||
const groups = {};
|
||||
|
||||
operations.forEach((operation) => {
|
||||
const date = new Date(operation.date);
|
||||
const monday = getMonday(date);
|
||||
const weekKey = monday.toISOString().split("T")[0]; // YYYY-MM-DD
|
||||
|
||||
if (!groups[weekKey]) {
|
||||
groups[weekKey] = {
|
||||
monday: new Date(monday), // Сохраняем объект Date
|
||||
operations: [],
|
||||
income: 0,
|
||||
expense: 0,
|
||||
};
|
||||
}
|
||||
|
||||
groups[weekKey].operations.push(operation);
|
||||
operation.type === "income"
|
||||
? (groups[weekKey].income += operation.amount)
|
||||
: (groups[weekKey].expense += operation.amount);
|
||||
});
|
||||
|
||||
return groups;
|
||||
};
|
||||
|
||||
const processWeeklyOperations = (operations) => {
|
||||
const grouped = groupOperationsByWeek(operations);
|
||||
|
||||
// Сортировка недель (от новых к старым)
|
||||
const sortedKeys = Object.keys(grouped).sort(
|
||||
(a, b) => new Date(b) - new Date(a),
|
||||
);
|
||||
|
||||
// Сортировка транзакций внутри недель (от новых к старым)
|
||||
sortedKeys.forEach((key) => {
|
||||
grouped[key].operations.sort((a, b) => new Date(b.date) - new Date(a.date));
|
||||
});
|
||||
|
||||
return { grouped, sortedKeys };
|
||||
};
|
||||
|
||||
// Группировка транзакций по месяцу и году
|
||||
const groupOperationsByMonth = (operations) => {
|
||||
const groups = {};
|
||||
|
||||
operations.forEach((operation) => {
|
||||
const date = new Date(operation.date);
|
||||
// Формируем ключ в формате "Год-Месяц" (например: "2025-08")
|
||||
const monthKey = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}`;
|
||||
|
||||
if (!groups[monthKey]) {
|
||||
groups[monthKey] = {
|
||||
monthDate: new Date(date.getFullYear(), date.getMonth(), 1), // Первый день месяца
|
||||
operations: [],
|
||||
income: 0,
|
||||
expense: 0,
|
||||
};
|
||||
}
|
||||
|
||||
groups[monthKey].operations.push(operation);
|
||||
operation.type === "income"
|
||||
? (groups[monthKey].income += operation.amount)
|
||||
: (groups[monthKey].expense += operation.amount);
|
||||
});
|
||||
|
||||
return groups;
|
||||
};
|
||||
|
||||
// Обработка и сортировка транзакций
|
||||
const processMonthlyOperations = (operations) => {
|
||||
const grouped = groupOperationsByMonth(operations);
|
||||
|
||||
// Сортировка месяцев (от новых к старым)
|
||||
const sortedKeys = Object.keys(grouped).sort(
|
||||
(a, b) => new Date(b) - new Date(a),
|
||||
);
|
||||
|
||||
// Сортировка транзакций внутри месяцев (от новых к старым)
|
||||
sortedKeys.forEach((key) => {
|
||||
grouped[key].operations.sort((a, b) => new Date(b.date) - new Date(a.date));
|
||||
});
|
||||
|
||||
return { grouped, sortedKeys };
|
||||
};
|
||||
|
||||
// Группировка транзакций по году
|
||||
const groupOperationsByYear = (operations) => {
|
||||
const groups = {};
|
||||
|
||||
operations.forEach((operation) => {
|
||||
const date = new Date(operation.date);
|
||||
const year = date.getFullYear();
|
||||
const yearKey = String(year);
|
||||
|
||||
if (!groups[yearKey]) {
|
||||
groups[yearKey] = {
|
||||
year,
|
||||
operations: [],
|
||||
income: 0,
|
||||
expense: 0,
|
||||
};
|
||||
}
|
||||
|
||||
groups[yearKey].operations.push(operation);
|
||||
operation.type === "income"
|
||||
? (groups[yearKey].income += operation.amount)
|
||||
: (groups[yearKey].expense += operation.amount);
|
||||
});
|
||||
|
||||
return groups;
|
||||
};
|
||||
|
||||
// Обработка и сортировка транзакций
|
||||
const processYearlyOperations = (operations) => {
|
||||
const grouped = groupOperationsByYear(operations);
|
||||
|
||||
// Сортировка годов (от новых к старым)
|
||||
const sortedKeys = Object.keys(grouped).sort((a, b) => b - a);
|
||||
|
||||
// Сортировка транзакций внутри годов (от новых к старым)
|
||||
sortedKeys.forEach((key) => {
|
||||
grouped[key].operations.sort((a, b) => new Date(b.date) - new Date(a.date));
|
||||
});
|
||||
|
||||
return { grouped, sortedKeys };
|
||||
};
|
||||
|
||||
export {
|
||||
processOperations,
|
||||
processWeeklyOperations,
|
||||
processMonthlyOperations,
|
||||
processYearlyOperations,
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
// Получение даты понедельника для любой даты
|
||||
const getMonday = (date) => {
|
||||
const d = new Date(date);
|
||||
const day = d.getDay(); // 0 (воскресенье) - 6 (суббота)
|
||||
const diff = d.getDate() - day + (day === 0 ? -6 : 1); // Коррекция для воскресенья
|
||||
return new Date(d.setDate(diff));
|
||||
};
|
||||
|
||||
// Форматирование диапазона недели
|
||||
const formatWeekRange = (monday) => {
|
||||
const start = new Date(monday);
|
||||
const end = new Date(monday);
|
||||
end.setDate(start.getDate() + 6);
|
||||
|
||||
const startStr = start.toLocaleDateString("ru-RU", {
|
||||
day: "numeric",
|
||||
month: "short",
|
||||
});
|
||||
|
||||
const endStr = end.toLocaleDateString("ru-RU", {
|
||||
day: "numeric",
|
||||
month: "short",
|
||||
year: "numeric",
|
||||
});
|
||||
|
||||
return `${startStr} - ${endStr}`;
|
||||
};
|
||||
|
||||
export { getMonday, formatWeekRange };
|
||||
@@ -3,6 +3,7 @@ import { useTheme } from "../../contexts/ThemeContext";
|
||||
import { ListGroup, Modal, Button } from "react-bootstrap";
|
||||
import ThemeToggle from "../../components/ThemeToggle";
|
||||
import CategoriesModal from "../../modals/CategoriesModal/CategoriesModal";
|
||||
import logout from "../../logic/logout";
|
||||
|
||||
const SettingsModal = () => {
|
||||
const { theme } = useTheme();
|
||||
@@ -56,6 +57,20 @@ const SettingsModal = () => {
|
||||
<ThemeToggle />
|
||||
</ListGroup.Item>
|
||||
</ListGroup>
|
||||
<h3 className="text-muted small m-1" style={{ fontSize: "0.8rem" }}>
|
||||
ACCOUNT
|
||||
</h3>
|
||||
<ListGroup>
|
||||
<ListGroup.Item className="p-0">
|
||||
<Button
|
||||
variant={theme}
|
||||
className="w-100 p-1 m-0"
|
||||
onClick={logout}
|
||||
>
|
||||
Logout
|
||||
</Button>
|
||||
</ListGroup.Item>
|
||||
</ListGroup>
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
</>
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
import routes from "../routes";
|
||||
|
||||
const GetAllOperation = async () => {
|
||||
const response = await fetch(routes.operations);
|
||||
const data = await response.json();
|
||||
return data;
|
||||
};
|
||||
|
||||
export default GetAllOperation;
|
||||
@@ -0,0 +1,58 @@
|
||||
import { useState } from "react";
|
||||
import { Modal, Card, Button, Form } from "react-bootstrap";
|
||||
import InputTitle from "../components/InputTitle";
|
||||
import login from "../logic/login";
|
||||
|
||||
const Login = () => {
|
||||
const [show, setShow] = useState(true);
|
||||
// const handleClose = () => setShow(false);
|
||||
const [userdata, setUserdata] = useState({ username: "", password: "" });
|
||||
return (
|
||||
<Modal show={show} fullscreen={true}>
|
||||
<Modal.Header className="d-flex align-items-center justify-content-between">
|
||||
<Modal.Title className="w-100 text-center">Login</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body className="p-3">
|
||||
<Card>
|
||||
<Card.Body>
|
||||
<Form
|
||||
onSubmit={async (e) => {
|
||||
if (await login(userdata, e)) {
|
||||
setShow(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<InputTitle text="Username" />
|
||||
<Form.Control
|
||||
type="text"
|
||||
placeholder="Title"
|
||||
value={userdata.username}
|
||||
onChange={(e) =>
|
||||
setUserdata({ ...userdata, username: e.target.value })
|
||||
}
|
||||
className="mb-3"
|
||||
required
|
||||
/>
|
||||
<InputTitle text="Password" />
|
||||
<Form.Control
|
||||
type="password"
|
||||
placeholder="password"
|
||||
value={userdata.password}
|
||||
onChange={(e) =>
|
||||
setUserdata({ ...userdata, password: e.target.value })
|
||||
}
|
||||
className="mb-3"
|
||||
required
|
||||
/>
|
||||
<Button type="submit" className="w-100">
|
||||
Login
|
||||
</Button>
|
||||
</Form>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default Login;
|
||||
@@ -1,6 +1,7 @@
|
||||
const host = window.location.origin;
|
||||
const prefix = "/api";
|
||||
const routes = {
|
||||
token: `${host}${prefix}/token`,
|
||||
categories: `${host}${prefix}/category`,
|
||||
operations: `${host}${prefix}/operation`,
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user