diff --git a/backend/api/Accessible.go b/backend/api/Accessible.go
new file mode 100644
index 0000000..a3f8e86
--- /dev/null
+++ b/backend/api/Accessible.go
@@ -0,0 +1,17 @@
+package api
+
+import (
+ "github.com/gofiber/fiber/v2"
+ "github.com/golang-jwt/jwt"
+)
+
+func Accessible(c *fiber.Ctx) error {
+ return c.SendString("Accessible")
+}
+
+func Restricted(c *fiber.Ctx) error {
+ user := c.Locals("user").(*jwt.Token)
+ claims := user.Claims.(jwt.MapClaims)
+ name := claims["name"].(string)
+ return c.SendString("Welcome " + name)
+}
diff --git a/backend/api/Login.go b/backend/api/Login.go
new file mode 100644
index 0000000..e5e20d5
--- /dev/null
+++ b/backend/api/Login.go
@@ -0,0 +1,36 @@
+package api
+
+import (
+ "backend/model"
+ "fmt"
+ "os"
+ "time"
+
+ "github.com/gofiber/fiber/v2"
+ "github.com/golang-jwt/jwt"
+)
+
+func Login(c *fiber.Ctx) error {
+ var requestdata model.AuthRequest
+ err := c.BodyParser(&requestdata)
+ if err != nil {
+ return fmt.Errorf("Error parse body, %s", err)
+ }
+ if requestdata.Username != "tola" || requestdata.Password != "2808" {
+ return c.SendStatus(fiber.StatusUnauthorized)
+ }
+
+ claims := jwt.MapClaims{
+ "username": "tola",
+ "is_superuser": true,
+ "exp": time.Now().Add(time.Hour * 72).Unix(),
+ }
+
+ token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
+
+ t, err := token.SignedString([]byte(os.Getenv("JWT_SECRET_KEY")))
+ if err != nil {
+ return c.SendStatus(fiber.StatusInternalServerError)
+ }
+ return c.JSON(fiber.Map{"token": t})
+}
diff --git a/backend/go.mod b/backend/go.mod
index a2b0a9e..1ca3944 100644
--- a/backend/go.mod
+++ b/backend/go.mod
@@ -3,15 +3,17 @@ module backend
go 1.24.5
require (
- github.com/gofiber/fiber v1.14.6
+ github.com/gofiber/contrib/jwt v1.1.2
github.com/gofiber/fiber/v2 v2.52.9
+ github.com/golang-jwt/jwt v3.2.2+incompatible
+ github.com/joho/godotenv v1.5.1
)
require (
+ github.com/MicahParks/keyfunc/v2 v2.1.0 // indirect
github.com/andybalholm/brotli v1.1.0 // indirect
- github.com/gofiber/utils v0.0.10 // indirect
+ github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
github.com/google/uuid v1.6.0 // indirect
- github.com/gorilla/schema v1.1.0 // indirect
github.com/klauspost/compress v1.17.9 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
diff --git a/backend/internal/category/GetAllCategories.go b/backend/internal/category/GetAllCategories.go
index 1d5127b..e58bff1 100644
--- a/backend/internal/category/GetAllCategories.go
+++ b/backend/internal/category/GetAllCategories.go
@@ -8,11 +8,13 @@ func GetAllCategories() ([]model.Category, error) {
ID: "1",
Title: "Electronics",
Icon: "😀",
+ Type: "expense",
},
{
ID: "2",
Title: "Clothing",
Icon: "😁",
+ Type: "expense",
},
{
ID: "3",
@@ -28,6 +30,19 @@ func GetAllCategories() ([]model.Category, error) {
Title: "Non-Fiction",
},
},
+ Type: "expense",
+ },
+ {
+ ID: "4",
+ Title: "Salary",
+ Icon: "😀",
+ Type: "income",
+ },
+ {
+ ID: "5",
+ Title: "asdf",
+ Icon: "😀",
+ Type: "income",
},
}, nil
}
diff --git a/backend/internal/operation/GetAllOperation.go b/backend/internal/operation/GetAllOperation.go
index 423d68e..152f404 100644
--- a/backend/internal/operation/GetAllOperation.go
+++ b/backend/internal/operation/GetAllOperation.go
@@ -1,6 +1,9 @@
package operation
-import "backend/model"
+import (
+ "backend/model"
+ "time"
+)
func GetAllOperation() ([]model.Operation, error) {
return []model.Operation{
@@ -26,5 +29,45 @@ func GetAllOperation() ([]model.Operation, error) {
Type: "expense",
Icon: "😂",
},
+ {
+ ID: "2",
+ Title: "Operation 2",
+ Amount: 200,
+ Category: "Category 2",
+ Date: time.Now().Format(time.RFC3339),
+ Type: "expense",
+ Icon: "😂",
+ },
+ {
+ ID: "1",
+ Title: "Operation 1",
+ Amount: 100,
+ Category: "Category 1",
+ SubCategory: model.Subcategory{
+ ID: "1",
+ Title: "subcategory",
+ },
+ Date: "2022-01-02T00:00:00Z",
+ Type: "income",
+ Icon: "😀",
+ },
+ {
+ ID: "2",
+ Title: "Operation 2",
+ Amount: 200,
+ Category: "Category 2",
+ Date: "2022-06-02T00:00:00Z",
+ Type: "expense",
+ Icon: "😂",
+ },
+ {
+ ID: "2",
+ Title: "Operation 2",
+ Amount: 200,
+ Category: "Category 2",
+ Date: "2022-06-22T00:00:00Z",
+ Type: "expense",
+ Icon: "😂",
+ },
}, nil
}
diff --git a/backend/main.go b/backend/main.go
index 0bfabba..1c8cffc 100644
--- a/backend/main.go
+++ b/backend/main.go
@@ -2,6 +2,9 @@ package main
import (
"backend/server"
+ "log"
+ "os"
+ "strconv"
"github.com/gofiber/fiber/v2"
)
@@ -11,5 +14,21 @@ func main() {
server.Configure(app)
- app.Listen(":3000")
+ enableTls, err := strconv.ParseBool(os.Getenv("ENABLE_TLS"))
+ if err != nil {
+ log.Fatalf("error read tls configuration, %v", err)
+ }
+ if enableTls {
+ log.Fatal(
+ app.ListenTLS(
+ os.Getenv("HTTP_ADDRESS"),
+ os.Getenv("CERT_FILE"),
+ os.Getenv("KEY_FILE"),
+ ),
+ )
+ } else {
+ log.Fatal(
+ app.Listen(os.Getenv("HTTP_ADDRESS")),
+ )
+ }
}
diff --git a/backend/model/category.go b/backend/model/category.go
index d468b0d..52abbfa 100644
--- a/backend/model/category.go
+++ b/backend/model/category.go
@@ -5,4 +5,5 @@ type Category struct {
Icon string `json:"icon"`
Title string `json:"title"`
Subcategories []Subcategory `json:"subcategories"`
+ Type string `json:"type"`
}
diff --git a/backend/model/user.go b/backend/model/user.go
new file mode 100644
index 0000000..bedcc61
--- /dev/null
+++ b/backend/model/user.go
@@ -0,0 +1,14 @@
+package model
+
+type AuthRequest struct {
+ Username string `json:"username"`
+ Password string `json:"password"`
+}
+
+type User struct {
+ Username string `json:"username"`
+ Password string `json:"password"`
+ Email string `json:"email"`
+ Group string `json:"group"`
+ IsSuperuser bool `json:"is_superuser"`
+}
diff --git a/backend/server/configure.go b/backend/server/configure.go
index ab5c4c7..d4b462a 100644
--- a/backend/server/configure.go
+++ b/backend/server/configure.go
@@ -2,14 +2,33 @@ package server
import (
"backend/api"
+ "backend/utils"
+ "os"
+ jwtware "github.com/gofiber/contrib/jwt"
"github.com/gofiber/fiber/v2"
+ "github.com/gofiber/fiber/v2/log"
+ "github.com/gofiber/fiber/v2/middleware/cors"
)
func Configure(app *fiber.App) {
+ utils.LoadEnv("wallet.conf")
+ log.SetLevel(log.LevelDebug)
+ app.Use(cors.New(cors.Config{
+ AllowOrigins: "http://localhost:3000",
+ AllowHeaders: "*",
+ }))
app.Static("/", "../frontend/build")
apiGroup := app.Group("/api")
+ apiGroup.Post("/token", api.Login)
+ apiGroup.Get("/accessible", api.Accessible)
+ app.Use(jwtware.New(jwtware.Config{
+ SigningKey: jwtware.SigningKey{
+ Key: []byte(os.Getenv("JWT_SECRET_KEY")),
+ },
+ }))
+ apiGroup.Get("/rescticted", api.Restricted)
operationGroup := apiGroup.Group("/operation")
operationGroup.Get("/", api.GetAllOperation)
diff --git a/backend/utils/LoadEnv.go b/backend/utils/LoadEnv.go
new file mode 100644
index 0000000..8a3820a
--- /dev/null
+++ b/backend/utils/LoadEnv.go
@@ -0,0 +1,14 @@
+package utils
+
+import (
+ "log"
+
+ "github.com/joho/godotenv"
+)
+
+func LoadEnv(filename string) {
+ err := godotenv.Load(filename)
+ if err != nil {
+ log.Fatal("Error loading .env file")
+ }
+}
diff --git a/backend/wallet.conf b/backend/wallet.conf
new file mode 100644
index 0000000..c9dd73b
--- /dev/null
+++ b/backend/wallet.conf
@@ -0,0 +1,5 @@
+JWT_SECRET_KEY=secret
+HTTP_ADDRESS=0.0.0.0:43243
+ENABLE_TLS=true
+CERT_FILE=/home/tola/certs/localhost/localhost.crt
+KEY_FILE=/home/tola/certs/localhost/localhost.key
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index 8aece72..59886a1 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -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() {
<>
- } />
+
+
+
+ }
+ />
+ } />
} />
} />
diff --git a/frontend/src/category/GetAllCategories.js b/frontend/src/category/GetAllCategories.js
deleted file mode 100644
index b58a726..0000000
--- a/frontend/src/category/GetAllCategories.js
+++ /dev/null
@@ -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;
diff --git a/frontend/src/components/DayTitle.jsx b/frontend/src/components/DayTitle.jsx
new file mode 100644
index 0000000..11e37e7
--- /dev/null
+++ b/frontend/src/components/DayTitle.jsx
@@ -0,0 +1,29 @@
+const DayTitle = ({ date, income, expense, ...props }) => {
+ return (
+
+
+ {/* 25 Jul 2025*/}
+ {date}
+
+
+
+ Income: {income} ₽
+
+
+ Expense: {expense} ₽
+
+
+
+ );
+};
+
+export default DayTitle;
diff --git a/frontend/src/components/InputTitle.jsx b/frontend/src/components/InputTitle.jsx
new file mode 100644
index 0000000..dc59e50
--- /dev/null
+++ b/frontend/src/components/InputTitle.jsx
@@ -0,0 +1,12 @@
+const InputTitle = ({ text, ...props }) => {
+ return (
+
+ {text}
+
+ );
+};
+export default InputTitle;
diff --git a/frontend/src/components/PrivateRoute.jsx b/frontend/src/components/PrivateRoute.jsx
new file mode 100644
index 0000000..db0fd2a
--- /dev/null
+++ b/frontend/src/components/PrivateRoute.jsx
@@ -0,0 +1,7 @@
+import isAuthenticated from "../logic/isAuthenticated";
+import Login from "../pages/Login";
+
+const PrivateRoute = ({ children }) => {
+ return isAuthenticated() ? children : ;
+};
+export default PrivateRoute;
diff --git a/frontend/src/logic/category/GetAllCategories.js b/frontend/src/logic/category/GetAllCategories.js
new file mode 100644
index 0000000..f960ced
--- /dev/null
+++ b/frontend/src/logic/category/GetAllCategories.js
@@ -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;
diff --git a/frontend/src/logic/isAuthenticated.js b/frontend/src/logic/isAuthenticated.js
new file mode 100644
index 0000000..9fcb178
--- /dev/null
+++ b/frontend/src/logic/isAuthenticated.js
@@ -0,0 +1,7 @@
+const isAuthenticated = () => {
+ if (localStorage.getItem("token") === null) {
+ return false;
+ }
+ return true;
+};
+export default isAuthenticated;
diff --git a/frontend/src/logic/login.js b/frontend/src/logic/login.js
new file mode 100644
index 0000000..997c809
--- /dev/null
+++ b/frontend/src/logic/login.js
@@ -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;
diff --git a/frontend/src/logic/logout.js b/frontend/src/logic/logout.js
new file mode 100644
index 0000000..43872e2
--- /dev/null
+++ b/frontend/src/logic/logout.js
@@ -0,0 +1,6 @@
+const logout = () => {
+ localStorage.clear();
+ window.location.reload();
+};
+
+export default logout;
diff --git a/frontend/src/logic/operation/GetAllOperation.js b/frontend/src/logic/operation/GetAllOperation.js
new file mode 100644
index 0000000..41b44b5
--- /dev/null
+++ b/frontend/src/logic/operation/GetAllOperation.js
@@ -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;
diff --git a/frontend/src/modals/AddCategoryModal/AddCategoryModal.jsx b/frontend/src/modals/AddCategoryModal/AddCategoryModal.jsx
index 5932bf8..439cd2b 100644
--- a/frontend/src/modals/AddCategoryModal/AddCategoryModal.jsx
+++ b/frontend/src/modals/AddCategoryModal/AddCategoryModal.jsx
@@ -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();
diff --git a/frontend/src/components/EmojiPickerButton/EmojiPickerButton.jsx b/frontend/src/modals/AddCategoryModal/components/EmojiPickerButton/EmojiPickerButton.jsx
similarity index 96%
rename from frontend/src/components/EmojiPickerButton/EmojiPickerButton.jsx
rename to frontend/src/modals/AddCategoryModal/components/EmojiPickerButton/EmojiPickerButton.jsx
index cf7b193..7e5c241 100644
--- a/frontend/src/components/EmojiPickerButton/EmojiPickerButton.jsx
+++ b/frontend/src/modals/AddCategoryModal/components/EmojiPickerButton/EmojiPickerButton.jsx
@@ -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();
diff --git a/frontend/src/modals/CategoriesModal/CategoriesModal.jsx b/frontend/src/modals/CategoriesModal/CategoriesModal.jsx
index 9cbf164..1e3ef48 100644
--- a/frontend/src/modals/CategoriesModal/CategoriesModal.jsx
+++ b/frontend/src/modals/CategoriesModal/CategoriesModal.jsx
@@ -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 }) => {
Settings
Categories
-
+
{
className="nav nav-tabs"
>
- asdf
+
+ {filteredIncomeCategories.map((category) => (
+
+ ))}
+
- {filteredCategories.map((category) => (
+ {filteredExpenseCategories.map((category) => (
{
>
➞
-
+
{subcategories.map((subcategory) => (
-
+
))}
diff --git a/frontend/src/components/CategorySign/CategorySign.module.css b/frontend/src/modals/CategoriesModal/components/CategorySign/CategorySign.module.css
similarity index 100%
rename from frontend/src/components/CategorySign/CategorySign.module.css
rename to frontend/src/modals/CategoriesModal/components/CategorySign/CategorySign.module.css
diff --git a/frontend/src/modals/HistoryModal/HistoryModal.jsx b/frontend/src/modals/HistoryModal/HistoryModal.jsx
index 2d8a3ef..c2a7a85 100644
--- a/frontend/src/modals/HistoryModal/HistoryModal.jsx
+++ b/frontend/src/modals/HistoryModal/HistoryModal.jsx
@@ -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 = () => {
-
+
- {operations.map((operation) => {
+ {sortedGroupKeys.map((dateKey) => {
+ const group = grouped[dateKey];
return (
-
-
-
-
-
+
+
+
+ {group.operations.map((operation) => (
+
+
+
+ ))}
+
+
+ );
+ })}
+
+
+
+
+ {sortedGroupKeysWeek.map((weekKey) => {
+ const group = groupedWeek[weekKey];
+ return (
+
+
+
+ {group.operations.map((operation) => (
+
+
+
+ ))}
+
+
+ );
+ })}
+
+
+
+
+ {sortedGroupKeysMonth.map((monthKey) => {
+ const group = groupedMonth[monthKey];
+ return (
+
+
+
+ {group.operations.map((operation) => (
+
+
+
+ ))}
+
+
+ );
+ })}
+
+
+
+
+ {sortedGroupKeysYear.map((yearKey) => {
+ const group = groupedYear[yearKey];
+ return (
+
+
+
+ {group.operations.map((operation) => (
+
+
+
+ ))}
+
+
);
})}
- {/*
-
-
- 25 Jul 2025
-
-
-
- Income: 0,00 ₽
-
-
- Expense: 0,00 ₽
-
-
-
-
-
-
-
-
-
-
- -63.00 ₽
-
-
- Transport
-
-
- Note
-
-
-
-
-
-
-
-
-
-
- 18000 ₽
-
-
- Transport
-
-
- Note
-
-
-
-
-
-
-
-
-
-
- -63.00 ₽
-
-
- Transport
-
-
- Note
-
-
-
-
-
-
-
-
-
- 25 Jul 2025
-
-
-
- Income: 0,00 ₽
-
-
- Expense: 0,00 ₽
-
-
-
-
-
-
-
-
-
-
- -63.00 ₽
-
-
- Transport
-
-
- Note
-
-
-
-
-
-
-
-
-
-
- 18000 ₽
-
-
- Transport
-
-
- Note
-
-
-
-
-
-
-
-
-
-
- -63.00 ₽
-
-
- Transport
-
-
- Note
-
-
-
-
-
-
-
-
-
- 25 Jul 2025
-
-
-
- Income: 0,00 ₽
-
-
- Expense: 0,00 ₽
-
-
-
-
-
-
-
-
-
-
- -63.00 ₽
-
-
- Transport
-
-
- Note
-
-
-
-
-
-
-
-
-
-
- 18000 ₽
-
-
- Transport
-
-
- Note
-
-
-
-
-
-
-
-
-
-
- -63.00 ₽
-
-
- Transport
-
-
- Note
-
-
-
-
-
-
-
-
-
- 25 Jul 2025
-
-
-
- Income: 0,00 ₽
-
-
- Expense: 0,00 ₽
-
-
-
-
-
-
-
-
-
-
- -63.00 ₽
-
-
- Transport
-
-
- Note
-
-
-
-
-
-
-
-
-
-
- 18000 ₽
-
-
- Transport
-
-
- Note
-
-
-
-
-
-
-
-
-
-
- -63.00 ₽
-
-
- Transport
-
-
- Note
-
-
-
-
- */}
- {/* */}
-
-
-
diff --git a/frontend/src/components/OperationSign/OperationSign.jsx b/frontend/src/modals/HistoryModal/components/OperationSign/OperationSign.jsx
similarity index 89%
rename from frontend/src/components/OperationSign/OperationSign.jsx
rename to frontend/src/modals/HistoryModal/components/OperationSign/OperationSign.jsx
index 20ab8a2..330ad3f 100644
--- a/frontend/src/components/OperationSign/OperationSign.jsx
+++ b/frontend/src/modals/HistoryModal/components/OperationSign/OperationSign.jsx
@@ -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 (
diff --git a/frontend/src/components/OperationSign/OperationSign.module.css b/frontend/src/modals/HistoryModal/components/OperationSign/OperationSign.module.css
similarity index 100%
rename from frontend/src/components/OperationSign/OperationSign.module.css
rename to frontend/src/modals/HistoryModal/components/OperationSign/OperationSign.module.css
diff --git a/frontend/src/modals/HistoryModal/logic/groupOperations.js b/frontend/src/modals/HistoryModal/logic/groupOperations.js
new file mode 100644
index 0000000..28bdc5d
--- /dev/null
+++ b/frontend/src/modals/HistoryModal/logic/groupOperations.js
@@ -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,
+};
diff --git a/frontend/src/modals/HistoryModal/logic/week.js b/frontend/src/modals/HistoryModal/logic/week.js
new file mode 100644
index 0000000..da67bbd
--- /dev/null
+++ b/frontend/src/modals/HistoryModal/logic/week.js
@@ -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 };
diff --git a/frontend/src/modals/SettingsModal/SettingsModal.jsx b/frontend/src/modals/SettingsModal/SettingsModal.jsx
index 78aea82..00f346a 100644
--- a/frontend/src/modals/SettingsModal/SettingsModal.jsx
+++ b/frontend/src/modals/SettingsModal/SettingsModal.jsx
@@ -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 = () => {
+
+ ACCOUNT
+
+
+
+
+
+
>
diff --git a/frontend/src/operation/GetAllOperation.js b/frontend/src/operation/GetAllOperation.js
deleted file mode 100644
index b06f6fc..0000000
--- a/frontend/src/operation/GetAllOperation.js
+++ /dev/null
@@ -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;
diff --git a/frontend/src/pages/Login.jsx b/frontend/src/pages/Login.jsx
new file mode 100644
index 0000000..caf73ac
--- /dev/null
+++ b/frontend/src/pages/Login.jsx
@@ -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 (
+
+
+ Login
+
+
+
+
+
+ setUserdata({ ...userdata, username: e.target.value })
+ }
+ className="mb-3"
+ required
+ />
+
+
+ setUserdata({ ...userdata, password: e.target.value })
+ }
+ className="mb-3"
+ required
+ />
+
+
+
+
+
+
+ );
+};
+
+export default Login;
diff --git a/frontend/src/routes.js b/frontend/src/routes.js
index f7855f2..b68cdac 100644
--- a/frontend/src/routes.js
+++ b/frontend/src/routes.js
@@ -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`,
};