Compare commits

..

No commits in common. "b40b2e4430595b2a927164de946957ae3021e6cd" and "0cec8be4cd4dfef1dc7367429fb8ce8d137596b4" have entirely different histories.

13 changed files with 209 additions and 42 deletions

View File

@ -44,15 +44,14 @@ export class ProjectsController {
return this.projectsService.findAll();
}
@ApiOperation({ summary: 'Get all projects where I participate' })
@ApiOperation({ summary: 'Get my projects' })
@ApiResponse({ status: 200, description: 'List of projects', type: Project, isArray: true })
@UseGuards(JwtAuthGuard)
@Get('my')
async findUserProjects(@Request() req): Promise<Project[]> {
return this.projectsService.findUserProjects(req.user.sub);
async findMyProjects(@Request() req): Promise<Project[]> {
return this.projectsService.findByOwner(req.user.sub);
}
@ApiOperation({ summary: 'Get single project by ID' })
@ApiParam({ name: 'id', type: 'number' })
@ApiResponse({ status: 200, description: 'Found project', type: Project })

View File

@ -29,14 +29,6 @@ export class ProjectsService {
return this.projectsRepository.find({ where: { ownerId } });
}
async findUserProjects(userId: number): Promise<Project[]> {
return this.projectsRepository
.createQueryBuilder('project')
.leftJoin('project_members', 'member', 'member.projectId = project.id')
.where('project.ownerId = :userId OR member.userId = :userId', { userId })
.getMany();
}
async findOneById(id: number): Promise<Project | null> {
return this.projectsRepository.findOne({ where: { id } });
}

View File

@ -46,7 +46,7 @@ export class AuthController {
description: 'User created successfully',
})
@ApiResponse({
status: 401,
status: 409,
description: 'Username already exists',
})
@Post('register')

View File

@ -1,8 +1,11 @@
import { Theme } from '@radix-ui/themes';
import '@radix-ui/themes/styles.css';
import { QueryClientProvider } from '@tanstack/react-query';
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import queryClient from './api/queryClient';
import './App.css';
import MyRoutes from './routes/routes';
import React from 'react';
import { appStore } from './stores/AppStore';
import { Ctx } from './context';

View File

@ -9,6 +9,6 @@ export const axiosBase = axios.create({
export const axiosAuth = axios.create({
baseURL: BASE_URL,
headers: {
Authorization: `Bearer ${localStorage.getItem('token')}`
Authorization: `Bearer ${localStorage.getItem('token')}` // Maybe we will use cookies
}
});

View File

@ -0,0 +1,11 @@
import { QueryClient } from "@tanstack/react-query";
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: 2,
},
},
})
export default queryClient;

View File

@ -9,6 +9,7 @@ import AddUserToProjectDialog from '../dialogs/AddUserToProjectDialog/AddUserToP
import CreateTaskDialog from '../dialogs/CreateTaskDialog/CreateTaskDialog';
import DeleteProjectDialog from '../dialogs/DeleteProjectDialog/CreateTaskDialog';
// Импортируем наш mobx-store
import { useStore } from '../../hooks/useStore';
type TCardGroup = {
@ -18,15 +19,18 @@ type TCardGroup = {
function CardGroup(props: TCardGroup) {
// При монтировании (и при изменении `props.id`) грузим задачи для проекта
const store = useStore();
useEffect(() => {
store.mainStore.fetchTasksForGroup(props.id).catch(console.error);
}, [props.id, store.mainStore]);
// Получаем задачи для данного проекта из стора (или пустой массив, если их нет)
const tasks = store.mainStore.tasksByProject.get(props.id) ?? [];
const isLoading = store.mainStore.isLoading;
const isLoading = store.mainStore.isLoading; // общее isLoading из стора
// Создать задачу
const createTask = (taskText: string, date: string) => {
store.mainStore.createTask({
title: taskText,
@ -36,6 +40,7 @@ function CardGroup(props: TCardGroup) {
});
};
// Удалить проект (аналог deleteProjectMutation)
const deleteGroup = () => {
store.mainStore.deleteProject(props.id);
};

View File

@ -7,17 +7,22 @@ import CardGroup from '../CardGroup/CardGroup';
import CreateProjectDialog from '../dialogs/CreateProjectDialog/CreateProjectDialog';
import { useStore } from '../../hooks/useStore';
type TDragResult = any; // или определите, если надо
function MainBoard() {
const store = useStore();
// Загрузка проектов при монтировании
useEffect(() => {
store.mainStore.fetchProjects().catch(console.error);
}, []);
const dragEndHandle = (result: TDragResult) => {
// Логика перетаскивания, если нужно
console.log(result);
};
// Для кнопки "Create project"
const createProject = (newProjectData: any) => {
store.mainStore.createProject(newProjectData);
};

View File

@ -2,6 +2,7 @@ import { Draggable } from '@hello-pangea/dnd';
import { Badge, Button, Card, Flex, Text } from '@radix-ui/themes';
import { observer } from 'mobx-react-lite';
import { useStore } from '../../hooks/useStore';
import { useEffect } from 'react';
type TTaskCard = {
title?: string;
@ -35,6 +36,10 @@ const TaskCard = observer((props: TTaskCard) => {
// @ts-ignore
console.log(`rerendered with`, store.mainStore.tasksByProject.get(2) );
useEffect(() => {
console.log(store.mainStore.fakeData)
}, [store.mainStore.fakeData])
return (
<Draggable draggableId={props.id || '123'} index={props.index || 0}>
@ -45,6 +50,7 @@ const TaskCard = observer((props: TTaskCard) => {
{...provided.draggableProps}
{...provided.dragHandleProps}
>
<h1>{store.mainStore.fakeData.toString()}</h1>
<Flex direction="column" gap="2">
<Flex justify={'between'}>
<Text wrap="pretty">{props.title}</Text>
@ -77,6 +83,11 @@ const TaskCard = observer((props: TTaskCard) => {
Completed
</Button>
)}
<Button onClick={() => {
store.mainStore.fetchPosts()
}}>
posts
</Button>
</Flex>
</Card>
)}

View File

@ -2,6 +2,7 @@ import { PropsWithChildren, useEffect, useState } from 'react';
import { observer } from 'mobx-react-lite';
import { Button, Dialog, Flex, Select } from '@radix-ui/themes';
// Импортируем сторы
import { useStore } from '../../../hooks/useStore';
type TAddUserToProjectDialog = {
@ -14,7 +15,8 @@ function AddUserToProjectDialog(props: TAddUserToProjectDialog) {
const store = useStore();
// При открытии диалога (или при ререндере) можно обновлять список пользователей
// (В RTK Query был refetch. Тут можно просто вызвать fetchAllUsers(force=true) если надо всегда свежий список)
useEffect(() => {
store.authStore.fetchAllUsers();
}, []);
@ -22,6 +24,7 @@ function AddUserToProjectDialog(props: TAddUserToProjectDialog) {
const handleAddUser = async () => {
if (selectedUserId) {
try {
// В store.mainStore метод addProjectMember(projectId: string, memberId: string/number)
await store.mainStore.addProjectMember(projectId.toString(), selectedUserId.toString());
setSelectedUserId(null);
} catch (err) {

View File

@ -1,4 +1,4 @@
import { makeAutoObservable } from 'mobx';
import { makeAutoObservable, runInAction } from 'mobx';
import { MainStore } from './MainStore';
import { AuthStore } from './AuthStore';

View File

@ -2,13 +2,18 @@ import { makeAutoObservable, runInAction } from 'mobx';
import axios from 'axios';
import { AppStore } from './AppStore';
/**
* AuthStore
* Хранит логику для авторизации, регистрации и получения всех пользователей.
*/
export class AuthStore {
appStore: AppStore
// Можно также хранить здесь данные о текущем пользователе, ошибках и т.д.
isLoading = false;
error: string | null = null;
// Список всех пользователей (если вам нужно его кэшировать)
allUsers: any[] = [];
constructor(appStore:any) {
@ -20,6 +25,9 @@ export class AuthStore {
return localStorage.getItem('token') || '';
}
/**
* Логин: отправляет запрос на сервер и сохраняет токен в localStorage
*/
async login(credentials: { username: string; password: string }) {
this.isLoading = true;
this.error = null;
@ -32,6 +40,7 @@ export class AuthStore {
runInAction(() => {
if (response.data.access_token) {
localStorage.setItem('token', response.data.access_token);
// Если нужно перенаправить после логина:
window.location.href = '/';
}
this.isLoading = false;
@ -45,6 +54,9 @@ export class AuthStore {
}
}
/**
* Регистрация: отправляет запрос на сервер
*/
async register(credentials: { username: string; password: string }) {
this.isLoading = true;
this.error = null;
@ -55,6 +67,7 @@ export class AuthStore {
);
runInAction(() => {
// Если регистрация успешна, например, статус 201 перенаправляем на /login
if (response.status === 201) {
window.location.href = '/login';
}
@ -69,7 +82,11 @@ export class AuthStore {
}
}
/**
* Получить всех пользователей
*/
async fetchAllUsers(force = false) {
// Если уже есть пользователи и не запрошен force, просто возвращаем
if (this.allUsers.length && !force) {
return this.allUsers;
}

View File

@ -2,18 +2,40 @@ import { makeAutoObservable, runInAction } from 'mobx';
import axios from 'axios';
import { AppStore } from './AppStore';
export type Posts = Post[]
export interface Post {
userId: number
id: number
title: string
body: string
}
/**
* MainStore
* Хранит логику для задач, проектов и участников проектов
*/
export class MainStore {
appStore: AppStore
// ----- Общие поля -----
isLoading = false;
error: string | null = null;
// ----- Задачи -----
tasks: any[] = [];
// Кэшированный список задач по ID проекта
tasksByProject = new Map<string, any[]>();
// ----- Проекты -----
projects: any[] = [];
// Детальные данные по конкретному проекту (если нужно кэшировать)
projectById = new Map<string, any>();
fakeData: Posts = []
// ----- Участники проекта -----
// Если нужно кэшировать участников по проекту
projectMembersById = new Map<string, any[]>();
constructor(appStore:any) {
@ -21,18 +43,75 @@ export class MainStore {
makeAutoObservable(this);
}
/**
* Получить токен из localStorage
*/
get token() {
return localStorage.getItem('token') || '';
}
/**
* Заготовка для заголовков (проставляем Bearer токен)
*/
get headers() {
return {
Authorization: `Bearer ${this.token}`,
};
}
/**
* --------------------------------------------------------------------------
* ЗАДАЧИ
* --------------------------------------------------------------------------
*/
/**
* Получить все задачи (кэширование: если уже загружены, второй раз не грузим)
*/
async fetchPosts(){
fetch('https://jsonplaceholder.typicode.com/posts?limit=10')
.then(response => response.json())
.then(json => {
this.fakeData = json
console.log(this.fakeData);
})
}
async fetchTasks(force = false) {
console.error(`Aboba`);
if (this.tasks.length && !force) {
return this.tasks;
}
this.isLoading = true;
this.error = null;
try {
const response = await axios.get('http://109.107.166.17:5000/api/tasks', {
headers: this.headers,
});
runInAction(() => {
console.log(`This is all tasks`,response.data);
this.tasks = response.data;
this.isLoading = false;
});
return this.tasks;
} catch (error: any) {
runInAction(() => {
this.error = error?.response?.data?.message || 'Ошибка при загрузке задач';
this.isLoading = false;
});
throw error;
}
}
/**
* Получить задачи для конкретного проекта (группы)
*/
async fetchTasksForGroup(projectId: string, force = false) {
if (this.tasksByProject.has(projectId) && !force) {
return this.tasksByProject.get(projectId);
@ -50,6 +129,8 @@ export class MainStore {
},
);
runInAction(() => {
console.log(`This is tasks for ${projectId}`,response.data);
this.tasksByProject.set(projectId, response.data);
this.isLoading = false;
});
@ -63,6 +144,9 @@ export class MainStore {
}
}
/**
* Получить одну задачу по ID
*/
async fetchTask(taskId: string) {
this.isLoading = true;
this.error = null;
@ -85,17 +169,18 @@ export class MainStore {
}
}
/**
* Создать задачу
*/
async createTask(newTask: any) {
try {
const response = await axios.post(
await axios.post(
'http://109.107.166.17:5000/api/tasks/create',
newTask,
{ headers: this.headers },
);
const createdTask = response.data;
this.tasks.push(createdTask);
const oldTasks = this.tasksByProject.get(newTask.projectId) ?? [];
this.tasksByProject.set(newTask.projectId, [...oldTasks, createdTask]);
// Инвалидируем кэш (чтобы при следующем getTasks() данные были актуальны)
this.tasks = [];
} catch (error: any) {
runInAction(() => {
this.error = error?.response?.data?.message || 'Ошибка при создании задачи';
@ -104,25 +189,22 @@ export class MainStore {
}
}
/**
* Обновить задачу
*/
async updateTask(task: { id: string; status: string }) {
try {
const response = await axios.patch(
await axios.patch(
`http://109.107.166.17:5000/api/tasks/${task.id}`,
{ status: task.status },
{ headers: this.headers },
);
// Локально обновим статус в массиве tasks
runInAction(() => {
const index = this.tasks.findIndex((t) => t.id === task.id);
if (index !== -1) {
this.tasks[index].status = task.status;
}
const projectId = response.data.project.id;
const oldArray = this.tasksByProject.get(projectId) || [];
const indexInMap = oldArray.findIndex((t) => t.id === +task.id);
if (indexInMap !== -1) {
oldArray[indexInMap].status = task.status;
this.tasksByProject.set(projectId, [...oldArray]);
}
});
} catch (error: any) {
runInAction(() => {
@ -132,7 +214,9 @@ export class MainStore {
}
}
/**
* Удалить задачу
*/
async deleteTask(taskId: string) {
try {
await axios.delete(`http://109.107.166.17:5000/api/tasks/${taskId}`, {
@ -150,7 +234,15 @@ export class MainStore {
}
}
/**
* --------------------------------------------------------------------------
* ПРОЕКТЫ
* --------------------------------------------------------------------------
*/
/**
* Получить все проекты текущего пользователя
*/
async fetchProjects(force = false) {
if (this.projects.length && !force) {
return this.projects;
@ -177,7 +269,11 @@ export class MainStore {
}
}
/**
* Получить данные одного проекта по ID
*/
async fetchProject(projectId: string) {
// Если хотим кэшировать отдельно проекты по id
if (this.projectById.has(projectId)) {
return this.projectById.get(projectId);
}
@ -203,17 +299,18 @@ export class MainStore {
}
}
/**
* Создать проект
*/
async createProject(newProject: any) {
try {
const response = await axios.post(
await axios.post(
'http://109.107.166.17:5000/api/projects/create',
newProject,
{ headers: this.headers },
);
const created = response.data;
runInAction(() => {
this.projects.push(created);
});
// Инвалидируем кэш
this.projects = [];
} catch (error: any) {
runInAction(() => {
this.error = error?.response?.data?.message || 'Ошибка при создании проекта';
@ -222,6 +319,9 @@ export class MainStore {
}
}
/**
* Обновить проект
*/
async updateProject(id: string, projectData: any) {
try {
await axios.patch(
@ -229,6 +329,7 @@ export class MainStore {
projectData,
{ headers: this.headers },
);
// Можно либо перезагрузить список проектов, либо локально обновить
this.fetchProjects(true);
} catch (error: any) {
runInAction(() => {
@ -238,6 +339,9 @@ export class MainStore {
}
}
/**
* Удалить проект
*/
async deleteProject(id: string) {
try {
await axios.delete(`http://109.107.166.17:5000/api/projects/${id}`, {
@ -255,6 +359,15 @@ export class MainStore {
}
}
/**
* --------------------------------------------------------------------------
* УЧАСТНИКИ ПРОЕКТА
* --------------------------------------------------------------------------
*/
/**
* Добавить участника в проект
*/
async addProjectMember(projectId: string, memberId: string) {
try {
await axios.post(
@ -265,6 +378,7 @@ export class MainStore {
},
{ headers: this.headers },
);
// Инвалидируем список участников
this.projectMembersById.delete(projectId);
} catch (error: any) {
runInAction(() => {
@ -273,6 +387,10 @@ export class MainStore {
throw error;
}
}
/**
* Получить участников проекта
*/
async fetchProjectMembers(projectId: string, force = false) {
if (this.projectMembersById.has(projectId) && !force) {
return this.projectMembersById.get(projectId);
@ -300,6 +418,9 @@ export class MainStore {
}
}
/**
* Удалить участника из проекта
*/
async removeProjectMember(projectId: string, memberId: string) {
try {
await axios.delete(
@ -309,7 +430,7 @@ export class MainStore {
headers: this.headers,
},
);
// Инвалидируем список участников
this.projectMembersById.delete(projectId);
} catch (error: any) {
runInAction(() => {