Merge pull request 'develop' (#1) from develop into main
Reviewed-on: https://gitea.home.com:3000/TolaMironcenko/wallet/pulls/1
This commit was merged in pull request #1.
This commit is contained in:
commit
7fac67f7f8
73 files changed
+2038
-805
No files matched your search
@@ -0,0 +1,49 @@
|
|||||||
|
name: Build and Test
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- develop
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- develop
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-test-backend:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: "1.24.5"
|
||||||
|
|
||||||
|
- name: Tidy and Vendor
|
||||||
|
run: cd backend && go mod tidy && go mod vendor
|
||||||
|
|
||||||
|
- name: build
|
||||||
|
run: cd backend && go build -v ./...
|
||||||
|
|
||||||
|
- name: test
|
||||||
|
run: cd backend && go test -v ./...
|
||||||
|
|
||||||
|
build-and-test-frontend:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-verion: "24.1.0"
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: cd frontend && npm ci
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: cd frontend && npm run build
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
go.sum
|
||||||
|
backend
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"backend/internal/category"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetAllCategories(c *fiber.Ctx) error {
|
||||||
|
categories, err := category.GetAllCategories()
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return c.JSON(categories)
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"backend/internal/operation"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetAllOperation(c *fiber.Ctx) error {
|
||||||
|
operations, err := operation.GetAllOperation()
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return c.JSON(operations)
|
||||||
|
}
|
||||||
@@ -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})
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
module backend
|
||||||
|
|
||||||
|
go 1.24.5
|
||||||
|
|
||||||
|
require (
|
||||||
|
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/golang-jwt/jwt/v5 v5.2.2 // indirect
|
||||||
|
github.com/google/uuid v1.6.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
|
||||||
|
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||||
|
github.com/rivo/uniseg v0.2.0 // indirect
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
|
github.com/valyala/fasthttp v1.51.0 // indirect
|
||||||
|
github.com/valyala/tcplisten v1.0.0 // indirect
|
||||||
|
golang.org/x/sys v0.28.0 // indirect
|
||||||
|
)
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package category
|
||||||
|
|
||||||
|
import "backend/model"
|
||||||
|
|
||||||
|
func GetAllCategories() ([]model.Category, error) {
|
||||||
|
return []model.Category{
|
||||||
|
{
|
||||||
|
ID: "1",
|
||||||
|
Title: "Electronics",
|
||||||
|
Icon: "😀",
|
||||||
|
Type: "expense",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "2",
|
||||||
|
Title: "Clothing",
|
||||||
|
Icon: "😁",
|
||||||
|
Type: "expense",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "3",
|
||||||
|
Title: "Books",
|
||||||
|
Icon: "😂",
|
||||||
|
Subcategories: []model.Subcategory{
|
||||||
|
{
|
||||||
|
ID: "1",
|
||||||
|
Title: "Fiction",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "2",
|
||||||
|
Title: "Non-Fiction",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Type: "expense",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "4",
|
||||||
|
Title: "Salary",
|
||||||
|
Icon: "😀",
|
||||||
|
Type: "income",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "5",
|
||||||
|
Title: "asdf",
|
||||||
|
Icon: "😀",
|
||||||
|
Type: "income",
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
package operation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"backend/model"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetAllOperation() ([]model.Operation, error) {
|
||||||
|
return []model.Operation{
|
||||||
|
{
|
||||||
|
ID: "1",
|
||||||
|
Title: "Operation 1",
|
||||||
|
Amount: 100,
|
||||||
|
Category: "Category 1",
|
||||||
|
SubCategory: model.Subcategory{
|
||||||
|
ID: "1",
|
||||||
|
Title: "subcategory",
|
||||||
|
},
|
||||||
|
Date: "2022-01-01T00:00:00Z",
|
||||||
|
Type: "income",
|
||||||
|
Icon: "😀",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "2",
|
||||||
|
Title: "Operation 2",
|
||||||
|
Amount: 200,
|
||||||
|
Category: "Category 2",
|
||||||
|
Date: "2022-01-02T00:00:00Z",
|
||||||
|
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
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"backend/server"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
app := fiber.New()
|
||||||
|
|
||||||
|
server.Configure(app)
|
||||||
|
|
||||||
|
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")),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
type Category struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Icon string `json:"icon"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Subcategories []Subcategory `json:"subcategories"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
type Operation struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Amount float64 `json:"amount"`
|
||||||
|
Category string `json:"category"`
|
||||||
|
SubCategory Subcategory `json:"subcategory,omitzero"`
|
||||||
|
Note string `json:"note,omitempty"`
|
||||||
|
Icon string `json:"icon"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Date string `json:"date"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
type Subcategory struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
}
|
||||||
@@ -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"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
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)
|
||||||
|
|
||||||
|
categoryGroup := apiGroup.Group("/category")
|
||||||
|
categoryGroup.Get("/", api.GetAllCategories)
|
||||||
|
}
|
||||||
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -23,3 +23,4 @@ yarn-debug.log*
|
|||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
bun.lock
|
bun.lock
|
||||||
bun.lockb
|
bun.lockb
|
||||||
|
deno.lock
|
||||||
File renamed without changes.
+559
-140
File diff suppressed because it is too large.
Load diff
@@ -7,8 +7,11 @@
|
|||||||
"@testing-library/jest-dom": "^6.6.3",
|
"@testing-library/jest-dom": "^6.6.3",
|
||||||
"@testing-library/react": "^16.3.0",
|
"@testing-library/react": "^16.3.0",
|
||||||
"@testing-library/user-event": "^13.5.0",
|
"@testing-library/user-event": "^13.5.0",
|
||||||
|
"axios": "^1.11.0",
|
||||||
"bootstrap": "^5.3.7",
|
"bootstrap": "^5.3.7",
|
||||||
"chart.js": "^4.5.0",
|
"chart.js": "^4.5.0",
|
||||||
|
"classnames": "^2.5.1",
|
||||||
|
"emoji-picker-react": "^4.13.2",
|
||||||
"faker": "^6.6.6",
|
"faker": "^6.6.6",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-bootstrap": "^2.10.10",
|
"react-bootstrap": "^2.10.10",
|
||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -3,6 +3,7 @@ import NavigationBar from "./components/NavigationBar/NavigationBar";
|
|||||||
import { useTheme } from "./contexts/ThemeContext";
|
import { useTheme } from "./contexts/ThemeContext";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import Home from "./pages/Home";
|
import Home from "./pages/Home";
|
||||||
|
import Login from "./pages/Login";
|
||||||
import {
|
import {
|
||||||
BrowserRouter as Router,
|
BrowserRouter as Router,
|
||||||
Routes,
|
Routes,
|
||||||
@@ -10,6 +11,7 @@ import {
|
|||||||
Navigate,
|
Navigate,
|
||||||
} from "react-router-dom";
|
} from "react-router-dom";
|
||||||
import NotFound from "./pages/NotFound";
|
import NotFound from "./pages/NotFound";
|
||||||
|
import PrivateRoute from "./components/PrivateRoute";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const { theme } = useTheme();
|
const { theme } = useTheme();
|
||||||
@@ -23,7 +25,15 @@ function App() {
|
|||||||
<NavigationBar />
|
<NavigationBar />
|
||||||
<>
|
<>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<Home />} />
|
<Route
|
||||||
|
path="/"
|
||||||
|
element={
|
||||||
|
<PrivateRoute>
|
||||||
|
<Home />
|
||||||
|
</PrivateRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route path="/login" element={<Login />} />
|
||||||
<Route path="/not-found" element={<NotFound />} />
|
<Route path="/not-found" element={<NotFound />} />
|
||||||
<Route path="*" element={<Navigate to="/not-found" replace />} />
|
<Route path="*" element={<Navigate to="/not-found" replace />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
File renamed without changes.
File renamed without changes.
@@ -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;
|
||||||
File renamed without changes.
File renamed without changes.
@@ -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;
|
||||||
+3
-3
@@ -4,14 +4,14 @@ import SettingsModal from "../../modals/SettingsModal/SettingsModal";
|
|||||||
|
|
||||||
const NavigationBar = () => {
|
const NavigationBar = () => {
|
||||||
return (
|
return (
|
||||||
<Navbar
|
<div
|
||||||
expand="lg"
|
expand="lg"
|
||||||
className="justify-content-between"
|
className="d-flex align-items-center justify-content-between"
|
||||||
style={{ padding: "1rem 1rem 0 1rem" }}
|
style={{ padding: "1rem 1rem 0 1rem" }}
|
||||||
>
|
>
|
||||||
<Navbar.Brand href="/">Wallet</Navbar.Brand>
|
<Navbar.Brand href="/">Wallet</Navbar.Brand>
|
||||||
<SettingsModal />
|
<SettingsModal />
|
||||||
</Navbar>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
File renamed without changes.
@@ -0,0 +1,7 @@
|
|||||||
|
import isAuthenticated from "../logic/isAuthenticated";
|
||||||
|
import Login from "../pages/Login";
|
||||||
|
|
||||||
|
const PrivateRoute = ({ children }) => {
|
||||||
|
return isAuthenticated() ? children : <Login />;
|
||||||
|
};
|
||||||
|
export default PrivateRoute;
|
||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -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,8 @@
|
|||||||
|
const getDateString = (date) => {
|
||||||
|
const dateData = new Date(date);
|
||||||
|
return [
|
||||||
|
dateData.getHours().toString().padStart(2, "0"),
|
||||||
|
dateData.getMinutes().toString().padStart(2, "0"),
|
||||||
|
].join(":");
|
||||||
|
};
|
||||||
|
export default getDateString;
|
||||||
@@ -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;
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { useTheme } from "../../contexts/ThemeContext";
|
||||||
|
import { Modal, Button, Card, Form } from "react-bootstrap";
|
||||||
|
import EmojiPickerButton from "./components/EmojiPickerButton/EmojiPickerButton";
|
||||||
|
|
||||||
|
const AddCategoryModal = ({ variant, ...props }) => {
|
||||||
|
const { theme } = useTheme();
|
||||||
|
const [show, setShow] = useState(false);
|
||||||
|
const handleClose = () => setShow(false);
|
||||||
|
const handleShow = () => setShow(true);
|
||||||
|
const [title, setTitle] = useState("");
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button variant={theme} onClick={handleShow} className="m-0" {...props}>
|
||||||
|
New Category
|
||||||
|
</Button>
|
||||||
|
<Modal show={show} onHide={handleClose} fullscreen={true}>
|
||||||
|
<Modal.Header className="d-flex align-items-center justify-content-between">
|
||||||
|
<spacer />
|
||||||
|
<Modal.Title>New Category</Modal.Title>
|
||||||
|
<Button
|
||||||
|
variant={theme}
|
||||||
|
type="button"
|
||||||
|
onClick={handleClose}
|
||||||
|
style={{
|
||||||
|
borderRadius: "50%",
|
||||||
|
width: "30px",
|
||||||
|
height: "30px",
|
||||||
|
padding: "0",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</Button>
|
||||||
|
</Modal.Header>
|
||||||
|
<Modal.Body className="p-3">
|
||||||
|
<div className="d-flex align-items-center justify-content-center">
|
||||||
|
<EmojiPickerButton />
|
||||||
|
</div>
|
||||||
|
<Form.Control
|
||||||
|
type="text"
|
||||||
|
placeholder="Title"
|
||||||
|
value={title}
|
||||||
|
onChange={(e) => setTitle(e.target.value)}
|
||||||
|
className="mb-3"
|
||||||
|
/>
|
||||||
|
<Card>
|
||||||
|
<Card.Body>
|
||||||
|
<div
|
||||||
|
className="text-muted small mb-1"
|
||||||
|
style={{ fontSize: "0.8rem" }}
|
||||||
|
>
|
||||||
|
SUBCATEGORIES
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant={"secondary"}
|
||||||
|
onClick={() => {}}
|
||||||
|
className="w-100 text-start mb-3"
|
||||||
|
>
|
||||||
|
Add subcategory
|
||||||
|
</Button>
|
||||||
|
<spacer />
|
||||||
|
<Button className="w-100">Add</Button>
|
||||||
|
</Card.Body>
|
||||||
|
</Card>
|
||||||
|
</Modal.Body>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default AddCategoryModal;
|
||||||
+58
@@ -0,0 +1,58 @@
|
|||||||
|
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";
|
||||||
|
|
||||||
|
const EmojiPickerButton = () => {
|
||||||
|
const { theme } = useTheme();
|
||||||
|
const [showPicker, setShowPicker] = useState(false);
|
||||||
|
const [inputValue, setInputValue] = useState("😀");
|
||||||
|
const target = useRef(null);
|
||||||
|
|
||||||
|
const handleEmojiSelect = (emoji) => {
|
||||||
|
setInputValue(emoji.emoji);
|
||||||
|
setShowPicker(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-3">
|
||||||
|
<InputGroup>
|
||||||
|
<Button
|
||||||
|
ref={target}
|
||||||
|
variant="secondary"
|
||||||
|
style={{
|
||||||
|
borderRadius: "50%",
|
||||||
|
width: "100px",
|
||||||
|
height: "100px",
|
||||||
|
fontSize: "45px",
|
||||||
|
}}
|
||||||
|
onClick={() => setShowPicker(!showPicker)}
|
||||||
|
>
|
||||||
|
{inputValue}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Overlay
|
||||||
|
show={showPicker}
|
||||||
|
target={target.current}
|
||||||
|
placement="bottom"
|
||||||
|
rootClose
|
||||||
|
onHide={() => setShowPicker(false)}
|
||||||
|
>
|
||||||
|
<Popover id="emoji-popover">
|
||||||
|
<Popover.Body className="p-0">
|
||||||
|
<EmojiPicker
|
||||||
|
onEmojiClick={handleEmojiSelect}
|
||||||
|
height={350}
|
||||||
|
width="100%"
|
||||||
|
previewConfig={{ showPreview: false }}
|
||||||
|
theme={theme}
|
||||||
|
/>
|
||||||
|
</Popover.Body>
|
||||||
|
</Popover>
|
||||||
|
</Overlay>
|
||||||
|
</InputGroup>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EmojiPickerButton;
|
||||||
File renamed without changes.
+1
-1
@@ -145,7 +145,7 @@ const AnalyticsModal = () => {
|
|||||||
<spacer />
|
<spacer />
|
||||||
</Modal.Header>
|
</Modal.Header>
|
||||||
<Modal.Body style={{ paddingLeft: "0", paddingRight: "0" }}>
|
<Modal.Body style={{ paddingLeft: "0", paddingRight: "0" }}>
|
||||||
<Tabs defaultActiveKey="Expenses" id="history-tabs">
|
<Tabs defaultActiveKey="Expenses" id="analytics-tabs">
|
||||||
<Tab eventKey="Incomes" title="Incomes"></Tab>
|
<Tab eventKey="Incomes" title="Incomes"></Tab>
|
||||||
<Tab eventKey="Expenses" title="Expenses" style={{ padding: "0" }}>
|
<Tab eventKey="Expenses" title="Expenses" style={{ padding: "0" }}>
|
||||||
<div className="d-flex mt-2 justify-content-center">
|
<div className="d-flex mt-2 justify-content-center">
|
||||||
File renamed without changes.
@@ -0,0 +1,148 @@
|
|||||||
|
import { useEffect, useState, useMemo, useCallback } from "react";
|
||||||
|
import { useTheme } from "../../contexts/ThemeContext";
|
||||||
|
import {
|
||||||
|
Modal,
|
||||||
|
Button,
|
||||||
|
Form,
|
||||||
|
InputGroup,
|
||||||
|
Tabs,
|
||||||
|
Tab,
|
||||||
|
Dropdown,
|
||||||
|
DropdownButton,
|
||||||
|
} from "react-bootstrap";
|
||||||
|
import styles from "./CategoriesModal.module.css";
|
||||||
|
import CategorySign from "./components/CategorySign/CategorySign";
|
||||||
|
import AddCategoryModal from "../../modals/AddCategoryModal/AddCategoryModal";
|
||||||
|
import fetchCategories from "../../logic/category/GetAllCategories";
|
||||||
|
|
||||||
|
const CategoriesModal = ({ variant }) => {
|
||||||
|
let [expenseCategories, setExpenseCategories] = useState([]);
|
||||||
|
const [incomeCategories, setIncomeCategories] = useState([]);
|
||||||
|
const { theme } = useTheme();
|
||||||
|
const [show, setShow] = useState(false);
|
||||||
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
|
const handleClose = () => setShow(false);
|
||||||
|
const handleShow = () => setShow(true);
|
||||||
|
useEffect(() => {
|
||||||
|
fetchCategories().then((data) => {
|
||||||
|
setExpenseCategories(
|
||||||
|
data.filter((category) => category.type === "expense"),
|
||||||
|
);
|
||||||
|
setIncomeCategories(
|
||||||
|
data.filter((category) => category.type === "income"),
|
||||||
|
);
|
||||||
|
console.log(data);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
const filteredExpenseCategories = useMemo(() => {
|
||||||
|
if (!searchTerm) return expenseCategories;
|
||||||
|
const term = searchTerm.toLowerCase();
|
||||||
|
return expenseCategories.filter((category) =>
|
||||||
|
category.title.toLowerCase().includes(term),
|
||||||
|
);
|
||||||
|
}, [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);
|
||||||
|
}, []);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
variant={theme}
|
||||||
|
onClick={handleShow}
|
||||||
|
className="w-100 d-flex justify-content-between"
|
||||||
|
>
|
||||||
|
<div>© Categories</div>
|
||||||
|
<div>❯</div>
|
||||||
|
</Button>
|
||||||
|
<Modal show={show} onHide={handleClose} fullscreen={true}>
|
||||||
|
<Modal.Header>
|
||||||
|
<div className="d-flex flex-column w-100 gap-2">
|
||||||
|
<div className="d-flex align-items-center justify-content-between">
|
||||||
|
<Button
|
||||||
|
variant={theme}
|
||||||
|
onClick={handleClose}
|
||||||
|
className="d-flex justify-content-between gap-2"
|
||||||
|
>
|
||||||
|
<div style={{ transform: "rotate(180deg)" }}>❯</div>
|
||||||
|
<div>Settings</div>
|
||||||
|
</Button>
|
||||||
|
<Modal.Title>Categories</Modal.Title>
|
||||||
|
<div></div>
|
||||||
|
<DropdownButton title="☰" variant={theme} drop="start">
|
||||||
|
<Dropdown.Item className="p-0">
|
||||||
|
<AddCategoryModal
|
||||||
|
className="w-100"
|
||||||
|
style={{ paddingLeft: "0" }}
|
||||||
|
/>
|
||||||
|
</Dropdown.Item>
|
||||||
|
<Dropdown.Item>Reorder</Dropdown.Item>
|
||||||
|
</DropdownButton>
|
||||||
|
</div>
|
||||||
|
<InputGroup className="mb-3">
|
||||||
|
<InputGroup.Text id="basic-addon1">
|
||||||
|
<div style={{ transform: "rotate(90deg)" }}>☌</div>
|
||||||
|
</InputGroup.Text>
|
||||||
|
<Form.Control
|
||||||
|
placeholder="Search"
|
||||||
|
aria-label="Search"
|
||||||
|
aria-describedby="basic-addon1"
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={handleSearch}
|
||||||
|
/>
|
||||||
|
</InputGroup>
|
||||||
|
</div>
|
||||||
|
</Modal.Header>
|
||||||
|
<Modal.Body className="p-0">
|
||||||
|
<Tabs
|
||||||
|
defaultActiveKey="Expenses"
|
||||||
|
id="categories-tabs"
|
||||||
|
className="nav nav-tabs"
|
||||||
|
>
|
||||||
|
<Tab eventKey="Incomes" title="Incomes">
|
||||||
|
<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" }}
|
||||||
|
>
|
||||||
|
{filteredExpenseCategories.map((category) => (
|
||||||
|
<CategorySign
|
||||||
|
key={`${category.id}/income`}
|
||||||
|
icon={category.icon}
|
||||||
|
title={category.title}
|
||||||
|
subcategories={
|
||||||
|
category.subcategories ? category.subcategories : []
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Tab>
|
||||||
|
</Tabs>
|
||||||
|
</Modal.Body>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default CategoriesModal;
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
.scrollableContent {
|
||||||
|
height: calc(100vh - 220px);
|
||||||
|
bottom: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.scrollableContent {
|
||||||
|
height: calc(100vh - 280px);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import { useTheme } from "../../../../contexts/ThemeContext";
|
||||||
|
import { Button } from "react-bootstrap";
|
||||||
|
import styles from "./CategorySign.module.css";
|
||||||
|
import classNames from "classnames";
|
||||||
|
|
||||||
|
const CategorySign = ({ icon, title, subcategories }) => {
|
||||||
|
const { theme } = useTheme();
|
||||||
|
return (
|
||||||
|
<Button variant={theme} className="w-100 mt-3">
|
||||||
|
<div>
|
||||||
|
<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>{icon}</div>
|
||||||
|
</div>
|
||||||
|
<div className="d-flex flex-column p-0 justify-content-center align-items-start w-100">
|
||||||
|
<h3
|
||||||
|
className="mb-0 fw-bolder"
|
||||||
|
style={{ fontSize: "1.1rem", fontWeight: 700 }}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</h3>
|
||||||
|
<div
|
||||||
|
className="text-muted small mb-1"
|
||||||
|
style={{ fontSize: "0.8rem" }}
|
||||||
|
>
|
||||||
|
{subcategories.length === 0
|
||||||
|
? "No subcategories"
|
||||||
|
: `Subcategories: ${subcategories.length}`}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{subcategories.length === 0 ? (
|
||||||
|
""
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className="d-flex align-items-center gap-3 p-0"
|
||||||
|
style={{ margin: "0 20px" }}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="p-2 justify-content-start align-items-start d-flex flex-column"
|
||||||
|
// style={{ fontSize: "30px" }}
|
||||||
|
>
|
||||||
|
<div>➞</div>
|
||||||
|
</div>
|
||||||
|
<div className={classNames("d-flex gap-3", styles.hideScrollbar)}>
|
||||||
|
{subcategories.map((subcategory) => (
|
||||||
|
<div
|
||||||
|
className="btn btn-secondary"
|
||||||
|
style={{ borderRadius: "25px", padding: "0 15px" }}
|
||||||
|
key={subcategory.id}
|
||||||
|
>
|
||||||
|
{subcategory.title}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CategorySign;
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
.hideScrollbar {
|
||||||
|
scrollbar-width: none;
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
}
|
||||||
|
.hideScrollbar::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
@@ -0,0 +1,186 @@
|
|||||||
|
import { useState, useEffect } from "react";
|
||||||
|
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 "../../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 [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);
|
||||||
|
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 (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
variant={theme}
|
||||||
|
className="shadow-sm p-2"
|
||||||
|
style={{ width: "100%", borderRadius: "15px" }}
|
||||||
|
onClick={handleShow}
|
||||||
|
>
|
||||||
|
History
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Modal show={show} onHide={handleClose} fullscreen={true}>
|
||||||
|
<Modal.Header className="d-flex align-items-center justify-content-between">
|
||||||
|
<Button variant={theme} type="button" onClick={handleClose}>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
<Modal.Title>History</Modal.Title>
|
||||||
|
<AddOperationModal variant={theme} />
|
||||||
|
{/* <Button variant={theme}>+</Button> */}
|
||||||
|
</Modal.Header>
|
||||||
|
<Modal.Body>
|
||||||
|
<Tabs defaultActiveKey="D" id="history-tabs">
|
||||||
|
<Tab eventKey="D" title="D" className="pt-2 pb-2">
|
||||||
|
<div className={styles.scrollableContent}>
|
||||||
|
{sortedGroupKeys.map((dateKey) => {
|
||||||
|
const group = grouped[dateKey];
|
||||||
|
return (
|
||||||
|
<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>
|
||||||
|
</Tab>
|
||||||
|
</Tabs>
|
||||||
|
</Modal.Body>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default HistoryModal;
|
||||||
File renamed without changes.
@@ -0,0 +1,60 @@
|
|||||||
|
import getDateString from "../../../../logic/getDateString";
|
||||||
|
// import { Button } from "react-bootstrap";
|
||||||
|
// import classNames from "classnames";
|
||||||
|
// import styles from "./OperationSign.module.css";
|
||||||
|
|
||||||
|
const OperationSign = ({ operation }) => {
|
||||||
|
return (
|
||||||
|
<div className="gap-2">
|
||||||
|
<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={
|
||||||
|
operation.type === "income"
|
||||||
|
? { fontSize: "1.1rem", fontWeight: 700, color: "#2DBB4D" }
|
||||||
|
: { fontSize: "1.1rem", fontWeight: 700 }
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{operation.type === "income" ? "" : "-"}
|
||||||
|
{operation.amount} ₽
|
||||||
|
</h3>
|
||||||
|
<div className="text-muted small mb-1" style={{ fontSize: "0.8rem" }}>
|
||||||
|
{operation.category}
|
||||||
|
</div>
|
||||||
|
{!operation.subcategory ? (
|
||||||
|
""
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className="text-muted small mb-1"
|
||||||
|
style={{ fontSize: "0.8rem" }}
|
||||||
|
>
|
||||||
|
{operation.subcategory.title}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="text-muted small mb-1" style={{ fontSize: "0.8rem" }}>
|
||||||
|
{operation.note && operation.note !== ""
|
||||||
|
? operation.note
|
||||||
|
: "No 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" }}
|
||||||
|
>
|
||||||
|
{getDateString(operation.date)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default OperationSign;
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
.hideScrollbar {
|
||||||
|
scrollbar-width: none;
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
}
|
||||||
|
.hideScrollbar::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
@@ -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 };
|
||||||
+27
@@ -2,6 +2,8 @@ import { useState } from "react";
|
|||||||
import { useTheme } from "../../contexts/ThemeContext";
|
import { useTheme } from "../../contexts/ThemeContext";
|
||||||
import { ListGroup, Modal, Button } from "react-bootstrap";
|
import { ListGroup, Modal, Button } from "react-bootstrap";
|
||||||
import ThemeToggle from "../../components/ThemeToggle";
|
import ThemeToggle from "../../components/ThemeToggle";
|
||||||
|
import CategoriesModal from "../../modals/CategoriesModal/CategoriesModal";
|
||||||
|
import logout from "../../logic/logout";
|
||||||
|
|
||||||
const SettingsModal = () => {
|
const SettingsModal = () => {
|
||||||
const { theme } = useTheme();
|
const { theme } = useTheme();
|
||||||
@@ -39,11 +41,36 @@ const SettingsModal = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</Modal.Header>
|
</Modal.Header>
|
||||||
<Modal.Body>
|
<Modal.Body>
|
||||||
|
<h3 className="text-muted small m-1" style={{ fontSize: "0.8rem" }}>
|
||||||
|
DATA
|
||||||
|
</h3>
|
||||||
|
<ListGroup>
|
||||||
|
<ListGroup.Item className="p-0">
|
||||||
|
<CategoriesModal />
|
||||||
|
</ListGroup.Item>
|
||||||
|
</ListGroup>
|
||||||
|
<h3 className="text-muted small m-1" style={{ fontSize: "0.8rem" }}>
|
||||||
|
APPLICATION
|
||||||
|
</h3>
|
||||||
<ListGroup>
|
<ListGroup>
|
||||||
<ListGroup.Item>
|
<ListGroup.Item>
|
||||||
<ThemeToggle />
|
<ThemeToggle />
|
||||||
</ListGroup.Item>
|
</ListGroup.Item>
|
||||||
</ListGroup>
|
</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.Body>
|
||||||
</Modal>
|
</Modal>
|
||||||
</>
|
</>
|
||||||
File renamed without changes.
File renamed without changes.
@@ -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;
|
||||||
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,8 @@
|
|||||||
|
const host = window.location.origin;
|
||||||
|
const prefix = "/api";
|
||||||
|
const routes = {
|
||||||
|
token: `${host}${prefix}/token`,
|
||||||
|
categories: `${host}${prefix}/category`,
|
||||||
|
operations: `${host}${prefix}/operation`,
|
||||||
|
};
|
||||||
|
export default routes;
|
||||||
File renamed without changes.
@@ -1,8 +0,0 @@
|
|||||||
import { render, screen } from '@testing-library/react';
|
|
||||||
import App from './App';
|
|
||||||
|
|
||||||
test('renders learn react link', () => {
|
|
||||||
render(<App />);
|
|
||||||
const linkElement = screen.getByText(/learn react/i);
|
|
||||||
expect(linkElement).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
@@ -1,652 +0,0 @@
|
|||||||
import { useState } from "react";
|
|
||||||
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";
|
|
||||||
|
|
||||||
const HistoryModal = () => {
|
|
||||||
const { theme } = useTheme();
|
|
||||||
const [show, setShow] = useState(false);
|
|
||||||
const handleClose = () => setShow(false);
|
|
||||||
const handleShow = () => setShow(true);
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Button
|
|
||||||
variant={theme}
|
|
||||||
className="shadow-sm p-2"
|
|
||||||
style={{ width: "100%", borderRadius: "15px" }}
|
|
||||||
onClick={handleShow}
|
|
||||||
>
|
|
||||||
History
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Modal show={show} onHide={handleClose} fullscreen={true}>
|
|
||||||
<Modal.Header className="d-flex align-items-center justify-content-between">
|
|
||||||
<Button variant={theme} type="button" onClick={handleClose}>
|
|
||||||
Close
|
|
||||||
</Button>
|
|
||||||
<Modal.Title>History</Modal.Title>
|
|
||||||
<AddOperationModal variant={theme} />
|
|
||||||
{/* <Button variant={theme}>+</Button> */}
|
|
||||||
</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" }}
|
|
||||||
>
|
|
||||||
<div className={styles.scrollableContent}>
|
|
||||||
<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>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
export default HistoryModal;
|
|
||||||
Reference in new issue
Block a user