diff --git a/README.md b/README.md index 1052cb5..e81e943 100644 --- a/README.md +++ b/README.md @@ -22,31 +22,56 @@ - Администратор ```json { - "email": "ivan.petrov@example.com", - "password": "admin123" + "email": "admin1@example.com", + "password": "AdminPass123" +} +``` + +- Заказчик +```json +{ + "email": "customer1@example.com", + "password": "CustPass123" } ``` - Прораб ```json { - "email": "maria.smirnova@example.com", - "password": "securepassword1" + "email": "foreman1@example.com", + "password": "ForemanPass123" } ``` -- Работник +- Работник 1 ```json { - "email": "dmitry.kuznetsov@example.com", - "password": "securepassword2" + "email": "worker1@example.com", + "password": "WorkerPass123" } ``` -- Заказчик +- Работник 2 ```json { - "email": "alex.sidorov@example.com", - "password": "securepassword3" + "email": "worker2@example.com:", + "password": "WorkerPass456" } -``` \ No newline at end of file +``` + +- Работник 3 +```json +{ + "email": "worker3@example.com:", + "password": "WorkerPass789" +} +``` + +- Работник 4 +```json +{ + "email": "worker4@example.com", + "password": "MysticWolf2024!" +} +``` + diff --git a/backend/.env b/backend/.env index bc7a6f0..e342677 100644 --- a/backend/.env +++ b/backend/.env @@ -4,12 +4,14 @@ MONGO_INITDB_DATABASE=fastapi DATABASE_URL=mongodb://admin:password123@mongo_db:27017/?authSource=admin -ACCESS_TOKEN_EXPIRES_IN=15 +ACCESS_TOKEN_EXPIRES_IN=60 REFRESH_TOKEN_EXPIRES_IN=60 JWT_ALGORITHM=HS256 JWT_SECRET_KEY=09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7 CLIENT_ORIGIN=http://app-frontend:8080 +DEFAULT_DATA_FILE=./dump/dump.json + JWT_PRIVATE_KEY=LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcEFJQkFBS0NBUUVBMExNZXQvc296T1RneVliTitZbXczTnlUYkFRemU4eDRQVHk2TmN4K3BVc1RTemh1CmVab0RnRU0wZmtVVHVjd2kyUERaWXdYckhrTmFDTVdPbHZEV3Uzc1RoS3VUOG8zVEYyUy9hRmJ5eE5jenpuNG0KUHIwU3k2VUZKM29XZHBGakNpNnFFMjNlZm9MZDhIanJ1Rlc0eXRyZkxJQU1uT0RQdVk5R3VadkduT1B2Z2VoKwp5aHo5K0NWd2s2bGRaSzZzYVZLZDBPbm9MU1g5U1REWmJrU3R1d0VqVG9ld0ZqMjNwdW14b3hLNCtLM2czUFpjCnJEcHZFdFkxeE1hbEJyVjVIRDYwQlc1bmo4bGgrbW5JQUlrWTJBSkRRYzV5MFJpRlZjcy9JVmxPQk5HdXJBZ3gKYlJGQ1VXOVBRdjVCVGxwODZlNlRqNVhPL2thR3ZhdXRVSS9yVVFJREFRQUJBb0lCQUhxaTl3Y242S2JXVEIxQQpRT05FL1JBYjhlbEVZcmg1dzZKQWdDL0M5aHpOakEza29FNkdxVTRDcitNUFZuTVV1Tm1BVmszeEdXT1VNbUQ3Ckxqb1dWaWlmUHkzejRTRmtJOG9ZWXIyK2NqUW5QWU0yNytSb0dKWmdaekgyZFNMQmRsQnljWEN2WEZJOU5vdnIKa3FDa2hzMTFaalZ4SFhoR1J1cUVmZ3Z0dFAxVmlBVUkxTkVSUjBzbzBrMzlCUUo3NVpTRkVsckhKSW0ybTRQVQpBMEpQaHFZNDNhLzRsNU5FVTMrdGlvanNzTUpnNzkzclZ5aEFLQVFaMnNUbWwwcG1vQURUNXYyYk1aZDFYSk9ZCjlxSElJNUwrQW85QkJvdktxcjNtdWNzQWpnbm9zR3hzU0FvdzdLbjA0Qm5idm1hSU9uMVdTQXN3dS92NGRmM1oKc25qUzNBRUNnWUVBN3ppWlJqMWJaeVMrYkwrNkFiTktKeW9DMjIzOGt0QzlRcFJ3WFVRdzZTUnoyWDlDVkhZWgpmNUwzY0FVOTZkSTh4STlpbjVoY2ZUUnpDVVBoWnBtNnZadHhpaHBNcTNEVlFkOUcxTHJMc1JhQ0thaGkvMzdQCldTSWcxbzV6SEVDWnZGVHVsNjRETlkvbkR2aTV1R3h6T2NYUVBJNHUwN1BQSld6RElsK1JIUkVDZ1lFQTMxWjgKVnBCUmtGeU8rZkhDakRkSyt3WXFiWlg5VDlNWkx6VFdqNFVFQ2xKUCtRdFF4Sml0VXdVZlNwZ1poUE5yQTJINApXT1dHaDI5RSsyTVpaZ0xtWTNmSndUc2xXenlUTVNuajZUVzZTUWZNeWJqRm1ROXE3bmhGV1cwVVhGVnJybm5FCnQxZ2g5QWp2Z1cvWXh4MHh4N0cwUUNzWXVGSGNxdWh0QVIzRzZrRUNnWUVBbWloV0ZiNktmWEJmU29OUEliTmgKTU5YUTI0a0lQN0JHbG5aRDVzWi80bTQ4UGNmVmZjcFJhalhTUUowUUpmTDJlQkNTbEpoQjJlbUh6RXV6SUVRbQo0L01jK3NzeDV6VWlLSDN6RGptRjlBdTJPNVFvbjg4ZlhhZ3hrekpmR2JERG9XcjJDa2I0Q0hkQWhoUmcwbWtJCjVBMEd3VTg2Ky9BZXFGWnJkV1l5aEpFQ2dZQW5NWGsrZzdNY24zR2o0VTVmNXZBc24wZGcxZHFQWUo5aHptYjgKNXIzdnhjUXRFMVJJTy9ibXc5WmE4OWcrb2EwYytkdG9WbGRHZXp0aTFtQkZxNnFjdUEvYTdqTS9FS0ZRRm1iZApyVVVVdmQ2dFk5U2hhTGcrUXpNQVg0a2NMdzFub0F6cWsvZlphSndIWGdadjR1cXlmYmdCTHM3MndiNzA2emI5CjVDamRRUUtCZ1FEbk5SaUFHZXBBOW9PRURiejIwSExrQmd2VGIxTUtmbWVxQmthUnRuNGFURlpxcGJPWFNYci8KL005UHhCMklFSm5kd2FHWFRvVWdsYm5QNWhGNTdCOEprOFFFWDUvWVJtZGVYT2FYSnBxZGo3WlFxdUhscnBzdgpuRE4xZzRDMkRmdHlaMG1vT3BPdEhZeVRlbkpjbmlPTjhPTnVKTHpDOVN5NDJOWUFDVkY2T3c9PQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQ== JWT_PUBLIC_KEY=LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUEwTE1ldC9zb3pPVGd5WWJOK1ltdwozTnlUYkFRemU4eDRQVHk2TmN4K3BVc1RTemh1ZVpvRGdFTTBma1VUdWN3aTJQRFpZd1hySGtOYUNNV09sdkRXCnUzc1RoS3VUOG8zVEYyUy9hRmJ5eE5jenpuNG1QcjBTeTZVRkozb1dkcEZqQ2k2cUUyM2Vmb0xkOEhqcnVGVzQKeXRyZkxJQU1uT0RQdVk5R3VadkduT1B2Z2VoK3loejkrQ1Z3azZsZFpLNnNhVktkME9ub0xTWDlTVERaYmtTdAp1d0VqVG9ld0ZqMjNwdW14b3hLNCtLM2czUFpjckRwdkV0WTF4TWFsQnJWNUhENjBCVzVuajhsaCttbklBSWtZCjJBSkRRYzV5MFJpRlZjcy9JVmxPQk5HdXJBZ3hiUkZDVVc5UFF2NUJUbHA4NmU2VGo1WE8va2FHdmF1dFVJL3IKVVFJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0t \ No newline at end of file diff --git a/backend/config.py b/backend/config.py index 98d1363..545fe66 100644 --- a/backend/config.py +++ b/backend/config.py @@ -17,6 +17,8 @@ class Settings(BaseSettings): JWT_SECRET_KEY: str CLIENT_ORIGIN: str + + DEFAULT_DATA_FILE: str class Config: env_file = "../.env" diff --git a/backend/dao/base.py b/backend/dao/base.py new file mode 100644 index 0000000..a2c5452 --- /dev/null +++ b/backend/dao/base.py @@ -0,0 +1,65 @@ +from typing import Any + +from bson import ObjectId +from motor.motor_asyncio import AsyncIOMotorCollection + +from schemas.utils import get_date_now, object_id_to_str + + +class BaseDao: + collection: AsyncIOMotorCollection = None + + @classmethod + async def _create(cls, data: dict) -> str: + if cls.collection is not None: + data['updated_at'] = get_date_now() + data['created_at'] = get_date_now() + result = await cls.collection.insert_one(data) + return str(result.inserted_id) + + @classmethod + async def _get_one_by_id(cls, id: str) -> dict: + if cls.collection is not None: + data = await cls.collection.find_one({"_id": ObjectId(id)}) + return object_id_to_str(data) + + @classmethod + async def _get_one_by_field(cls, field: str, data: str) -> dict: + if cls.collection is not None: + data = await cls.collection.find_one({field: data}) + return object_id_to_str(data) + + @classmethod + async def _update(cls, id: str, data: dict) -> str | None: + if cls.collection is not None: + data['updated_at'] = get_date_now() + result = await cls.collection.update_one( + {"_id": ObjectId(id)}, + {"$set": data} + ) + + if result.modified_count == 0: + return None + return id + + @classmethod + async def _update_with_query(cls, query: dict[str, Any], data: dict[str, Any]) -> str | None: + if cls.collection is not None: + result = await cls.collection.update_one(query, data) + if result.modified_count == 0: + return None + return str(query["_id"]) + + # @classmethod + # async def _find_with_filters(cls, query: dict) -> dict: + # if cls.collection: + # + + @classmethod + async def _delete_by_id(cls, id: str) -> str | None: + if cls.collection is not None: + result = await cls.collection.delete_one({"_id": ObjectId(id)}) + + if result.deleted_count == 0: + return None + return id diff --git a/backend/dao/messager.py b/backend/dao/messager.py index d443507..df7c398 100644 --- a/backend/dao/messager.py +++ b/backend/dao/messager.py @@ -1,14 +1,13 @@ -from datetime import datetime, timezone from bson import ObjectId -from fastapi.params import Depends from database import db -from schemas.messager import Participant, LastMessage, Chat, Message, CreateChatResponse, ChatResponse, CreateMessage, \ +from schemas.messager import Participant, LastMessage, Chat, Message, ChatResponse, CreateMessage, \ StatusMsg, MessageResponse -from schemas.user import UserDao +from schemas.user import User +from schemas.utils import get_date_now -async def create_chat(user_sender: UserDao, user_receiver: UserDao, content: str) -> ChatResponse | None: +async def create_chat(user_sender: User, user_receiver: User, content: str) -> ChatResponse | None: chat_collection = db.get_collection('chat') message_collection = db.get_collection('message') @@ -138,7 +137,7 @@ async def add_message_to_chat(user_id: str, message_data: CreateMessage) -> str { "$set": { "lastMessage": last_message.model_dump(), - "updated_at": datetime.now(timezone.utc) + "updated_at": get_date_now() } }, return_document=True @@ -159,7 +158,7 @@ async def create_message(user_id: str, message_data: CreateMessage) -> MessageRe sender=user_id, content=message_data.content, status=StatusMsg.unread, - timestamp=datetime.now(timezone.utc) + timestamp=get_date_now() ) insert_result = await message_collection.insert_one(message.model_dump()) diff --git a/backend/dao/project.py b/backend/dao/project.py index 315acb1..e31ee32 100644 --- a/backend/dao/project.py +++ b/backend/dao/project.py @@ -1,216 +1,371 @@ from bson import ObjectId + +from dao.base import BaseDao from database import db -from schemas.project import ProjectCreate, Project, Procurement, Stage, Risk, ProcurementResponse, RiskResponse, \ - StageResponse -from schemas.user import UserDao, Contact, ContactResponse -from schemas.utils import object_id_to_str, generate_id - - -async def create_project(project_crate: ProjectCreate, user: UserDao) -> Project: - project_collection = db.get_collection('project') - contact = Contact( - username=f'{user.lastname} {user.name} {user.middlename}', - role=user.role - ) - project_crate.add_contact(user.id, contact) - result = await project_collection.insert_one(project_crate.model_dump()) - return await get_project_by_id(result.inserted_id) - - -async def get_project_by_id(project_id: str) -> Project | None: - project_collection = db.get_collection('project') - project = await project_collection.find_one({'_id': ObjectId(project_id)}) - if project is None: +from schemas.project import ProjectCreate, ProjectResponse, Procurement, Stage, Risk, ProcurementResponse, \ + RiskResponse, \ + StageResponse, ProjectUpdate, RiskUpdate, ProcurementUpdate, StageUpdate +from schemas.user import User, Contact, ContactResponse +from schemas.utils import object_id_to_str, generate_id, get_date_now + + +class ProjectDao(BaseDao): + collection = db.get_collection('project') + + @classmethod + async def create_project(cls, project_create: ProjectCreate, user: User) -> ProjectResponse | None: + contact = Contact( + username=f'{user.lastname} {user.name} {user.middlename}', + role=user.role + ) + project_create.add_contact(user.id, contact) + created_id = await cls._create(project_create.model_dump()) + return await cls.get_project_by_id(created_id) + + @classmethod + async def delete_project(cls, project_id: str) -> str: + return await cls._delete_by_id(project_id) + + @classmethod + async def get_project_by_id(cls, project_id: str) -> ProjectResponse | None: + project = await cls._get_one_by_id(project_id) + if project is None: + return None + return ProjectResponse(**project) + + @classmethod + async def update_project_by_id(cls, project_id: str, project_update: ProjectUpdate) -> ProjectResponse | None: + update_data = project_update.model_dump() + updated_id = await cls._update(project_id, update_data) + return await cls.get_project_by_id(updated_id) + + @classmethod + async def get_project_by_name(cls, project_name: str) -> ProjectResponse | None: + project = await cls._get_one_by_field('name', project_name) + if project is None: + return None + return ProjectResponse(**project) + + @classmethod + async def get_all_projects(cls) -> list[ProjectResponse] | list[None]: + cursor = cls.collection.find() + projects_list = await cursor.to_list() + result = [] + for project in projects_list: + project = object_id_to_str(project) + result.append(ProjectResponse(**project)) + return result + + @classmethod + async def get_projects_by_user(cls, user_id: str) -> list[ProjectResponse] | list[None]: + cursor = cls.collection.find( + {f"contacts.{user_id}": {"$exists": True}} + ) + projects_list = await cursor.to_list() + result = [] + for project in projects_list: + project = object_id_to_str(project) + result.append(ProjectResponse(**project)) + return result + + @classmethod + async def add_contact_to_project(cls, project_id: str, user: User) -> ProjectResponse | None: + updated_id = await cls._update_with_query( + {"_id": ObjectId(project_id)}, + { + "$set": { + f"contacts.{user.id}": { + "username": f"{user.lastname} {user.name} {user.middlename}", + "role": user.role, + "updated_at": get_date_now(), + "created_at": user.created_at + } + } + } + ) + return await cls.get_project_by_id(updated_id) + + @classmethod + async def get_contacts_by_project_id(cls, project_id: str) -> list[ContactResponse] | list[None] | None: + project = await cls.collection.find_one({"_id": ObjectId(project_id)}, {"contacts": 1}) + if project and "contacts" in project: + contacts = [ContactResponse(id=contact_id, **contact_data) for contact_id, contact_data in + project["contacts"].items()] + return contacts + elif project is None: + return None + return [] + + @classmethod + async def get_contact_by_id(cls, project_id: str, contact_id: str) -> ContactResponse | None: + project = await cls._get_one_by_id(project_id) + if not project: + return None + + contact = project.get("contacts", {}).get(contact_id) + if not contact or contact.get("user_id") != ObjectId(contact_id): + return None + return ContactResponse(**contact) + + @classmethod + async def delete_contact(cls, project_id, contact_id: str) -> ProjectResponse | None: + project = await cls._get_one_by_id(project_id) + + if not project: + return None + + contacts = project.get("contacts", {}) + if contact_id in contacts: + del contacts[contact_id] + else: + return None + + stages = project.get("stages", {}) + + for stage_id, stage in stages.items(): + tasks = stage.get("tasks", {}) + for task_id, task in tasks.items(): + workers = task.get("workers", {}) + if contact_id in workers: + del workers[contact_id] + + update_result = await cls.collection.replace_one( + {"_id": ObjectId(project_id)}, + project + ) + + if update_result.modified_count == 0: + return None + + return await cls.get_project_by_id(project_id) + + @classmethod + async def add_procurement_to_project(cls, project_id: str, + procurement_create: Procurement) -> ProjectResponse | None: + procurement = procurement_create.model_dump() + procurement['quantity'] = str(procurement['quantity']) + procurement['price'] = str(procurement['price']) + result = await cls.collection.update_one( + {"_id": ObjectId(project_id)}, + { + "$set": {f"procurements.{generate_id()}": procurement} + } + ) + return await cls.get_project_by_id(project_id) + + @classmethod + async def delete_procurement(cls, project_id: str, procurement_id: str) -> str | None: + return await cls._update_with_query( + { + "_id": ObjectId(project_id), + f"procurements.{procurement_id}": {"$exists": True} + }, + { + "$unset": {f"procurements.{procurement_id}": ""} + } + ) + + @classmethod + async def get_procurements_by_project_id(cls, project_id: str) -> list[ProcurementResponse] | list[None] | None: + project = await cls.collection.find_one({"_id": ObjectId(project_id)}, {"procurements": 1}) + if project and "procurements" in project: + procurements = [ProcurementResponse(id=procurement_id, **procurement_data) for + procurement_id, procurement_data + in + project["procurements"].items()] + return procurements + elif project is None: + return None + return [] + + @classmethod + async def get_procurement_by_id(cls, project_id: str, procurement_id: str) -> ProcurementResponse | None: + project = await cls.collection.find_one( + { + "_id": ObjectId(project_id), + f"procurements.{procurement_id}": {"$exists": True} + }, + { + f"procurements.{procurement_id}": 1 + } + ) + + if project: + procurement = project["procurements"][procurement_id] + return ProcurementResponse(id=procurement_id, **procurement) return None - project = object_id_to_str(project) - return Project(**project) + @classmethod + async def update_procurement_by_id(cls, project_id: str, procurement_id: str, + procurement_update: ProcurementUpdate) -> ProcurementResponse | None: + + update_data = procurement_update.model_dump() + + updated_id = await cls._update_with_query( + { + "_id": ObjectId(project_id), + f"procurements.{procurement_id}": {"$exists": True} + }, + { + "$set": { + f"procurements.{procurement_id}.item_name": update_data["item_name"], + f"procurements.{procurement_id}.quantity": update_data["quantity"], + f"procurements.{procurement_id}.price": update_data["price"], + f"procurements.{procurement_id}.inStock": update_data["inStock"], + f"procurements.{procurement_id}.units": update_data["units"], + f"procurements.{procurement_id}.delivery_date": update_data["delivery_date"], + f"procurements.{procurement_id}.created_by": update_data["created_by"], + f"procurements.{procurement_id}.updated_at": get_date_now(), + } + } + ) + + return await cls.get_procurement_by_id(updated_id, procurement_id) + + @classmethod + async def add_stage_to_project(cls, project_id: str, stage_create: Stage): + updated_id = await cls._update_with_query( + {'_id': ObjectId(project_id)}, + { + "$set": {f"stages.{generate_id()}": stage_create.model_dump()} + } + ) + return await cls.get_project_by_id(updated_id) + + @classmethod + async def delete_stage(cls, project_id: str, stage_id: str) -> str | None: + updated_id = await cls._update_with_query( + { + "_id": ObjectId(project_id), + f"stages.{stage_id}": {"$exists": True} + }, + { + "$unset": {f"stages.{stage_id}": ""} + } + ) + + return stage_id + + @classmethod + async def get_stages_by_project_id(cls, project_id: str) -> list[StageResponse] | list[None] | None: + project = await cls.collection.find_one({"_id": ObjectId(project_id)}, {"stages": 1}) + if project and "stages" in project: + stages = [StageResponse(id=stage_id, **stage_data) for stage_id, stage_data in + project["stages"].items()] + return stages + elif project is None: + return None + return [] + + @classmethod + async def get_stage_by_id(cls, project_id: str, stage_id: str) -> StageResponse | None: + project = await cls.collection.find_one( + { + "_id": ObjectId(project_id), + f"stages.{stage_id}": {"$exists": True} + }, + { + f"stages.{stage_id}": 1 + } + ) -async def get_project_by_name(project_name: str) -> Project | None: - project_collection = db.get_collection('project') - project = await project_collection.find_one({'name': project_name}) - if project is None: + if project: + stage = project["stages"][stage_id] + return StageResponse(id=stage_id, **stage) return None - project = object_id_to_str(project) - return Project(**project) - - -async def get_projects_by_user(user_id: str) -> list[Project] | None: - project_collection = db.get_collection('project') - cursor = project_collection.find( - {f"contacts.{user_id}": {"$exists": True}} - ) - projects_list = await cursor.to_list() - result = [] - for project in projects_list: - project = object_id_to_str(project) - result.append(Project(**project)) - return result - - -async def add_contact_to_project(project_id: str, user: UserDao) -> Project | None: - project_collection = db.get_collection('project') - result = await project_collection.update_one( - {"_id": ObjectId(project_id)}, - { - "$set": { - f"contacts.{user.id}": { - "username": f"{user.lastname} {user.name} {user.middlename}", - "role": user.role, + + @classmethod + async def update_stage_by_id(cls, project_id: str, stage_id: str, + stage_update: StageUpdate) -> StageResponse | None: + update_data = stage_update.model_dump() + update_data["updated_at"] = get_date_now() + updated_id = await cls._update_with_query( + { + "_id": ObjectId(project_id), + f"stages.{stage_id}": {"$exists": True} + }, + { + "$set": { + f"stages.{stage_id}.name": update_data["name"], + f"stages.{stage_id}.start_date": update_data["start_date"], + f"stages.{stage_id}.end_date": update_data["end_date"], + f"stages.{stage_id}.updated_at": update_data["updated_at"], } } - }, - ) - return await get_project_by_id(project_id) - - -async def get_contacts_by_project_id(project_id: str) -> list[ContactResponse] | list[None] | None: - project_collection = db.get_collection('project') - project = await project_collection.find_one({"_id": ObjectId(project_id)}, {"contacts": 1}) - if project and "contacts" in project: - contacts = [ContactResponse(id=contact_id, **contact_data) for contact_id, contact_data in - project["contacts"].items()] - return contacts - elif project is None: + ) + return await cls.get_stage_by_id(updated_id, stage_id) + + @classmethod + async def add_risk_to_project(cls, project_id: str, risk_create: Risk): + updated_id = await cls._update_with_query( + {"_id": ObjectId(project_id)}, + { + "$set": {f"risks.{generate_id()}": risk_create.model_dump()} + } + ) + return await cls.get_project_by_id(updated_id) + + @classmethod + async def delete_risk(cls, project_id: str, risk_id: str) -> str | None: + updated_id = await cls._update_with_query( + { + "_id": ObjectId(project_id), + f"risks.{risk_id}": {"$exists": True} + }, + { + "$unset": {f"risks.{risk_id}": ""} + } + ) + return risk_id + + @classmethod + async def get_risks_by_project_id(cls, project_id: str) -> list[RiskResponse] | list[None] | None: + project = await cls.collection.find_one({"_id": ObjectId(project_id)}, {"risks": 1}) + if project and "risks" in project: + risks = [RiskResponse(id=risk_id, **risk_data) for risk_id, risk_data in + project["risks"].items()] + return risks + elif project is None: + return None + return [] + + @classmethod + async def get_risk_by_id(cls, project_id: str, risk_id: str) -> RiskResponse | None: + project = await cls.collection.find_one( + { + "_id": ObjectId(project_id), + f"risks.{risk_id}": {"$exists": True} + }, + { + f"risks.{risk_id}": 1 + } + ) + + if project: + risk = project["risks"][risk_id] + return RiskResponse(id=risk_id, **risk) return None - return [] + @classmethod + async def update_risk_by_id(cls, project_id: str, risk_id: str, risk_update: RiskUpdate) -> RiskResponse | None: + update_data = risk_update.model_dump() + update_data["updated_at"] = get_date_now() + + result = await cls.collection.update_one( + { + "_id": ObjectId(project_id), + f"risks.{risk_id}": {"$exists": True} + }, + { + "$set": { + f"risks.{risk_id}.name": update_data["name"], + f"risks.{risk_id}.description": update_data["description"], + f"risks.{risk_id}.updated_at": update_data["updated_at"] + } + } + ) -async def get_contact_by_id(project_id: str, contact_id: str) -> ContactResponse | None: - project_collection = db.get_collection('project') - project = await project_collection.find_one({"_id": ObjectId(project_id)}) - if not project: - return None + if result.modified_count == 0: + return None - contact = project.get("contacts", {}).get(contact_id) - if not contact or contact.get("user_id") != ObjectId(contact_id): - return None - return ContactResponse(**contact) - - -async def add_procurement_to_project(project_id: str, procurement_create: Procurement) -> Project | None: - project_collection = db.get_collection('project') - procurement = procurement_create.model_dump() - procurement['quantity'] = str(procurement['quantity']) - procurement['price'] = str(procurement['price']) - result = await project_collection.update_one( - {"_id": ObjectId(project_id)}, - { - "$set": {f"procurements.{generate_id()}": procurement} - } - ) - return await get_project_by_id(project_id) - - -async def get_procurements_by_project_id(project_id: str) -> list[ProcurementResponse] | list[None] | None: - project_collection = db.get_collection('project') - project = await project_collection.find_one({"_id": ObjectId(project_id)}, {"procurements": 1}) - if project and "procurements" in project: - procurements = [ProcurementResponse(id=procurement_id, **procurement_data) for procurement_id, procurement_data - in - project["procurements"].items()] - return procurements - elif project is None: - return None - return [] - - -async def get_procurement_by_id(project_id: str, procurement_id: str) -> ProcurementResponse | None: - project_collection = db.get_collection('project') - project = await project_collection.find_one( - { - "_id": ObjectId(project_id), - f"procurements.{procurement_id}": {"$exists": True} - }, - { - f"procurements.{procurement_id}": 1 - } - ) - - if project: - procurement = project["procurements"][procurement_id] - return ProcurementResponse(id=procurement_id, **procurement) - return None - - -async def add_stage_to_project(project_id: str, stage_create: Stage): - project_collection = db.get_collection('project') - result = await project_collection.update_one( - {'_id': ObjectId(project_id)}, - { - "$set": {f"stages.{generate_id()}": stage_create.model_dump()} - } - ) - return await get_project_by_id(project_id) - - -async def get_stages_by_project_id(project_id: str) -> list[StageResponse] | list[None] | None: - project_collection = db.get_collection('project') - project = await project_collection.find_one({"_id": ObjectId(project_id)}, {"stages": 1}) - if project and "stages" in project: - stages = [StageResponse(id=stage_id, **stage_data) for stage_id, stage_data in - project["stages"].items()] - return stages - elif project is None: - return None - return [] - - -async def get_stage_by_id(project_id: str, stage_id: str) -> StageResponse | None: - project_collection = db.get_collection('project') - project = await project_collection.find_one( - { - "_id": ObjectId(project_id), - f"stages.{stage_id}": {"$exists": True} - }, - { - f"stages.{stage_id}": 1 - } - ) - - if project: - stage = project["stages"][stage_id] - return StageResponse(id=stage_id, **stage) - return None - - -async def add_risk_to_project(project_id: str, risk_create: Risk): - project_collection = db.get_collection('project') - result = await project_collection.update_one( - {"_id": ObjectId(project_id)}, - { - "$set": {f"risks.{generate_id()}": risk_create.model_dump()} - } - ) - return await get_project_by_id(project_id) - - -async def get_risks_by_project_id(project_id: str) -> list[RiskResponse] | list[None] | None: - project_collection = db.get_collection('project') - project = await project_collection.find_one({"_id": ObjectId(project_id)}, {"risks": 1}) - if project and "risks" in project: - risks = [RiskResponse(id=risk_id, **risk_data) for risk_id, risk_data in - project["risks"].items()] - return risks - elif project is None: - return None - return [] - - - -async def get_risk_by_id(project_id: str, risk_id: str) -> RiskResponse | None: - project_collection = db.get_collection('project') - project = await project_collection.find_one( - { - "_id": ObjectId(project_id), - f"risks.{risk_id}": {"$exists": True} - }, - { - f"risks.{risk_id}": 1 - } - ) - - if project: - stage = project["risks"][risk_id] - return RiskResponse(id=risk_id, **stage) - return None + return await cls.get_risk_by_id(project_id, risk_id) diff --git a/backend/dao/stat.py b/backend/dao/stat.py new file mode 100644 index 0000000..6563d7d --- /dev/null +++ b/backend/dao/stat.py @@ -0,0 +1,51 @@ +from datetime import datetime +from typing import Dict, Any + +from bson import ObjectId +from bson.errors import InvalidId + +from dao.project import ProjectDao + + +class StatDao(ProjectDao): + + @classmethod + async def get_stats(cls, project_ids: list[str], stat_type: str, start_date: datetime = None, + end_date: datetime = None) -> dict[str, Any]: + + project_ids = [ObjectId(pid) for pid in project_ids] + + base_filter = {"_id": {"$in": project_ids}} # Фильтр по ID проекта + + pipeline = [ + {"$match": base_filter}, # Фильтр по ID + { + "$project": { + "_id": 1, + "filtered_items": { + "$filter": { + "input": {"$objectToArray": f"${stat_type}"}, + "as": "item", + "cond": { + "$and": [ + {"$gte": ["$$item.v.created_at", start_date]} if start_date else {}, + {"$lte": ["$$item.v.created_at", end_date]} if end_date else {} + ] + } + } + } + } + }, + { + "$project": { + "_id": 1, + "count": {"$size": "$filtered_items"} + } + } + ] + + + cursor = cls.collection.aggregate(pipeline) + results = await cursor.to_list(length=None) + + return {str(result["_id"]): result["count"] for result in results} diff --git a/backend/dao/task.py b/backend/dao/task.py index 3f4efa6..ea4354e 100644 --- a/backend/dao/task.py +++ b/backend/dao/task.py @@ -1,105 +1,192 @@ from bson import ObjectId +from dao.base import BaseDao from database import db -from schemas.task import Task, TaskResponse +from schemas.task import Task, TaskResponse, TaskUpdate, TaskStatusUpdate, ProjectTaskResponse from schemas.user import Worker -from schemas.utils import generate_id - - -async def add_task(project_id: str, stage_id, task_create: Task) -> TaskResponse | None: - project_collection = db.get_collection('project') - task_id = generate_id() - result = await project_collection.update_one( - {"_id": ObjectId(project_id), f"stages.{stage_id}": {"$exists": True}}, - { - "$set": {f"stages.{stage_id}.tasks.{task_id}": task_create.model_dump()} - } - ) - if result.modified_count > 0: - return TaskResponse(id=task_id, **task_create.model_dump()) - return None - - -async def get_task_by_id(project_id: str, stage_id: str, task_id: str) -> TaskResponse | None: - project_collection = db.get_collection('project') - project = await project_collection.find_one( - { - "_id": ObjectId(project_id), - f"stages.{stage_id}.tasks.{task_id}": {"$exists": True} - }, - { - f"stages.{stage_id}.tasks.{task_id}": 1 - } - ) - - if project: - task = project["stages"][stage_id]["tasks"][task_id] - return TaskResponse(id=task_id, **task) - return None - -async def get_tasks_by_stage_id(project_id: str, stage_id: str) -> list[TaskResponse] | list[None]: - project_collection = db.get_collection('project') - project = await project_collection.find_one( - { - "_id": ObjectId(project_id), - f"stages.{stage_id}.tasks": {"$exists": True} - } - ) - - stage_tasks = [] - if not project or "stages" not in project or stage_id not in project["stages"]: - return [] - - tasks = project["stages"][stage_id].get("tasks", {}) - - tasks_list = [ - TaskResponse(id=task_id, **task_data) for task_id, task_data in tasks.items() - ] - return tasks_list - - -async def get_all_tasks_by_user(user_id: str) -> list[Task] | list[None]: - project_collection = db.get_collection('project') - cursor = project_collection.find( - { - "stages": { - "$exists": True +from schemas.utils import generate_id, get_date_now + + +class TaskDAO(BaseDao): + collection = db.get_collection('project') + + @classmethod + async def add_task(cls, project_id: str, stage_id, task_create: Task) -> TaskResponse | None: + task_id = generate_id() + result = await cls.collection.update_one( + {"_id": ObjectId(project_id), f"stages.{stage_id}": {"$exists": True}}, + { + "$set": {f"stages.{stage_id}.tasks.{task_id}": task_create.model_dump()} } - } - ) - - tasks = [] - async for project in cursor: - for stage_id, stage in project.get("stages", {}).items(): - for task_id, task in stage.get("tasks", {}).items(): - for worker_id, worker in task.get("workers", {}).items(): - if worker.get("user_id") == ObjectId(user_id): - tasks.append(TaskResponse(id=task_id, **task)) - - return tasks - - -async def add_worker_to_task(project_id: str, stage_id: str, task_id: str, worker_id: str, - worker: Worker) -> TaskResponse | dict[str: str]: - project_collection = db.get_collection('project') - - update_result = await project_collection.update_one( - { - "_id": ObjectId(project_id), - f"stages.{stage_id}.tasks.{task_id}": {"$exists": True} - }, - { - "$set": { - f"stages.{stage_id}.tasks.{task_id}.workers.{worker_id}": { - "user_id": ObjectId(worker_id), - "name": worker.name, - "role": worker.role, - } + ) + if result.modified_count > 0: + return TaskResponse(id=task_id, **task_create.model_dump()) + return None + + @classmethod + async def remove_task(cls, project_id: str, stage_id: str, task_id: str) -> str | None: + deleted_id = await cls._update_with_query( + { + "_id": ObjectId(project_id), + f"stages.{stage_id}.tasks.{task_id}": {"$exists": True} + }, + { + "$unset": {f"stages.{stage_id}.tasks.{task_id}": ""} } - } - ) + ) + return deleted_id + + @classmethod + async def get_task_by_id(cls, project_id: str, stage_id: str, task_id: str) -> TaskResponse | None: + project = await cls.collection.find_one( + { + "_id": ObjectId(project_id), + f"stages.{stage_id}.tasks.{task_id}": {"$exists": True} + }, + { + f"stages.{stage_id}.tasks.{task_id}": 1 + } + ) - if update_result.modified_count == 0: + if project: + task = project["stages"][stage_id]["tasks"][task_id] + return TaskResponse(id=task_id, **task) return None - return await get_task_by_id(project_id, stage_id, task_id) + @classmethod + async def update_task_by_id(cls, project_id: str, stage_id: str, task_id: str, + task_update: TaskUpdate) -> TaskResponse | None: + + update_data = task_update.model_dump() + + updated_id = await cls._update_with_query( + { + "_id": ObjectId(project_id), + f"stages.{stage_id}.tasks.{task_id}": {"$exists": True} + }, + { + "$set": { + f"stages.{stage_id}.tasks.{task_id}.name": update_data["name"], + f"stages.{stage_id}.tasks.{task_id}.description": update_data["description"], + f"stages.{stage_id}.tasks.{task_id}.status": update_data["status"], + f"stages.{stage_id}.tasks.{task_id}.start_date": update_data["start_date"], + f"stages.{stage_id}.tasks.{task_id}.end_date": update_data["end_date"], + f"stages.{stage_id}.tasks.{task_id}.updated_at": get_date_now(), + } + } + ) + + if updated_id is None: + return None + + return await cls.get_task_by_id(project_id, stage_id, task_id) + + @classmethod + async def update_task_status(cls, project_id: str, stage_id: str, task_id: str, + task_update: TaskStatusUpdate) -> TaskResponse | None: + update_data = task_update.model_dump() + + updated_id = await cls._update_with_query( + { + "_id": ObjectId(project_id), + f"stages.{stage_id}.tasks.{task_id}": {"$exists": True} + }, + { + "$set": { + f"stages.{stage_id}.tasks.{task_id}.status": update_data["status"], + f"stages.{stage_id}.tasks.{task_id}.updated_at": get_date_now(), + } + } + ) + + if updated_id is None: + return None + + return await cls.get_task_by_id(project_id, stage_id, task_id) + + @classmethod + async def get_tasks_by_stage_id(cls, project_id: str, stage_id: str) -> list[TaskResponse] | list[None]: + project = await cls.collection.find_one( + { + "_id": ObjectId(project_id), + f"stages.{stage_id}.tasks": {"$exists": True} + } + ) + + if not project or "stages" not in project or stage_id not in project["stages"]: + return [] + + tasks = project["stages"][stage_id].get("tasks", {}) + + tasks_list = [ + TaskResponse(id=task_id, **task_data) for task_id, task_data in tasks.items() + ] + return tasks_list + + @classmethod + async def get_all_tasks_by_user(cls, user_id: str) -> list[ProjectTaskResponse] | list[None]: + cursor = cls.collection.find( + { + "stages": { + "$exists": True + } + } + ) + + tasks = [] + async for project in cursor: + for stage_id, stage in project.get("stages", {}).items(): + for task_id, task in stage.get("tasks", {}).items(): + for worker_id, worker in task.get("workers", {}).items(): + if worker_id == user_id: + tasks.append(ProjectTaskResponse( + id=task_id, + project_id=str(project["_id"]), + project_name=project["name"], + stage_id=str(stage_id), + **task + )) + + return tasks + + @classmethod + async def add_worker_to_task(cls, project_id: str, stage_id: str, task_id: str, worker_id: str, + worker: Worker) -> TaskResponse | dict[str: str]: + update_result = await cls.collection.update_one( + { + "_id": ObjectId(project_id), + f"stages.{stage_id}.tasks.{task_id}": {"$exists": True} + }, + { + "$set": { + f"stages.{stage_id}.tasks.{task_id}.workers.{worker_id}": { + "name": worker.name, + "role": worker.role, + } + } + } + ) + + if update_result.modified_count == 0: + return None + + return await cls.get_task_by_id(project_id, stage_id, task_id) + + @classmethod + async def delete_worker_from_task(cls, project_id: str, stage_id: str, task_id: str, worker_id: str) -> str | None: + delete_result = await cls.collection.update_one( + { + "_id": ObjectId(project_id), + f"stages.{stage_id}.tasks.{task_id}": {"$exists": True} + }, + { + "$unset": { + f"stages.{stage_id}.tasks.{task_id}.workers.{worker_id}": "" + } + } + ) + + if delete_result.modified_count == 0: + return None + + return task_id diff --git a/backend/dao/user.py b/backend/dao/user.py index b40aa49..cb6d1c7 100644 --- a/backend/dao/user.py +++ b/backend/dao/user.py @@ -1,47 +1,139 @@ +import re from bson import ObjectId -from datetime import datetime, timezone + +from dao.base import BaseDao from database import db -from schemas.user import UserCreateSchema, UserDao, UserBaseSchema -from schemas.utils import object_id_to_str +from schemas.user import UserCreateSchema, User, UserResponse, Contact, ContactResponse, Role +from schemas.utils import object_id_to_str, get_date_now +from utils.password import get_password_hash -async def find_user_by_email(email) -> UserDao | None: - users = db.get_collection('user') - user = await users.find_one({'email': email}) - if not user: - return None - user = object_id_to_str(user) - return UserDao(**user) +class UserDao(BaseDao): + collection = db.get_collection('user') + @classmethod + async def find_user_by_email(cls, email) -> User | None: + user = await cls._get_one_by_field('email', email) + if not user: + return None + return User(**user) -async def find_user_by_id(user_id: str) -> UserDao | None: - users = db.get_collection('user') - user = await users.find_one({'_id': ObjectId(user_id)}) - if not user: - return None - user = object_id_to_str(user) - return UserDao(**user) + @classmethod + async def find_user_by_id(cls, user_id: str) -> User | None: + user = await cls._get_one_by_id(user_id) + if not user: + return None + return User(**user) + @classmethod + async def find_users(cls, name: str = "", lastname: str = "", middlename: str = "", role: Role = None) -> list[ + UserResponse | None]: -async def find_all_users() -> list[UserBaseSchema]: - users_collection = db.get_collection('user') - users = users_collection.find() - users_list = [] - for user in await users.to_list(): - user = object_id_to_str(user) - users_list.append(UserBaseSchema(**user)) - return users_list + query = {} + if name: + query["name"] = {"$regex": f"^{name}", "$options": "i"} + if lastname: + query["lastname"] = {"$regex": f"^{lastname}", "$options": "i"} + if middlename: + query["middlename"] = {"$regex": f"^{middlename}", "$options": "i"} + if role: + query["role"] = str(role.value) + cursor = cls.collection.find(query) + user_list = [] -async def find_users_by_project(project_id: str) -> list[UserBaseSchema]: - users_collection = db.get_collection('user') - + for user in await cursor.to_list(): + user = object_id_to_str(user) + user_list.append(UserResponse(**user)) + return user_list + + @classmethod + async def find_users_from_project(cls, project_id: str, name: str = "", lastname: str = "", middlename: str = "", + role: Role = None) -> list[ + ContactResponse | None] | None: + project_collection = db.get_collection('project') + + search_params = {} + + if lastname: + search_params["lastname"] = lastname + if name: + search_params["name"] = name + if middlename: + search_params["middlename"] = middlename + + project = await project_collection.find_one({"_id": ObjectId(project_id)}) + + if project is None: + return None + + contacts = project.get("contacts", {}) + matched_contacts = [] + + # случай, когда нет фильтров ФИО + if not search_params: + # но есть фильтр роли + if role: + for contact_id, contact in contacts.items(): + contact_role = contact.get("role", None) + if contact_role == role.value: + matched_contacts.append(ContactResponse(id=contact_id, **contact)) + return matched_contacts + + # без фильтров - все контакты проекта + return [ContactResponse(id=contact_id, **contact) for contact_id, contact in contacts.items()] + # случай, когда есть фильтры ФИО + search_query = ".*".join(search_params) + + for contact_id, contact in contacts.items(): + username = contact.get("username", "") + fio = username.split() + contact_role = contact.get("role", None) + + if "lastname" in search_params and len(fio) > 0: + lastname_match = re.search(search_params["lastname"], fio[0], re.IGNORECASE) + else: + lastname_match = True # Если lastname не указан, то считаем его найденным + + # Проверяем наличие имени + if "name" in search_params and len(fio) > 1: + name_match = re.search(search_params["name"], fio[1], re.IGNORECASE) + else: + name_match = True # Если name не указан, то считаем его найденным + + # Проверяем наличие отчества + if "middlename" in search_params and len(fio) > 2: + middlename_match = re.search(search_params["middlename"], fio[2], re.IGNORECASE) + else: + middlename_match = True # Если middlename не указан, то считаем его найденным + + # Условие совпадения всех параметров + if lastname_match and name_match and middlename_match: + if role and contact_role != role.value: + continue + matched_contacts.append(ContactResponse(id=contact_id, **contact)) + + return matched_contacts + + @classmethod + async def find_all_users(cls) -> list[UserResponse | None]: + users = cls.collection.find() + users_list = [] + for user in await users.to_list(): + user = object_id_to_str(user) + users_list.append(UserResponse(**user)) + return users_list + + @classmethod + async def create_user(cls, user: UserCreateSchema): + user_dict = user.model_dump() + return await cls._create(user_dict) + + @classmethod + async def update_user(cls, user_id: str, user_update: UserCreateSchema): + update_data = user_update.model_dump() + update_data["password"] = get_password_hash(update_data["password"]) + updated_id = await cls._update(user_id, update_data) + return await cls.find_user_by_id(updated_id) -async def create_user(user: UserCreateSchema): - users_collection = db.get_collection('user') - user_dict = user.model_dump() - user_dict['created_at'] = datetime.now(timezone.utc) - user_dict['updated_at'] = datetime.now(timezone.utc) - result = await users_collection.insert_one(user_dict) - return str(result.inserted_id) \ No newline at end of file diff --git a/backend/database.py b/backend/database.py index 8ba6ece..8cffb77 100644 --- a/backend/database.py +++ b/backend/database.py @@ -1,3 +1,6 @@ +import json + +from bson import ObjectId from motor.motor_asyncio import AsyncIOMotorClient from config import settings @@ -7,6 +10,9 @@ async def create_indexation(): + """ + Создает индексы для коллекций + """ users = db.get_collection('user') await users.create_index('email') @@ -17,4 +23,34 @@ async def create_indexation(): ("id", 1), ("timestamp", -1), ] - ) \ No newline at end of file + ) + +async def is_database_empty() -> bool: + """ + Проверяет, пуста ли база данных. + """ + collections = await db.list_collection_names() + for collection_name in collections: + if await db[collection_name].count_documents({}) > 0: + return False + return True + + +def load_default_data(): + """ + Загружает данные из JSON-файла в базу данных. + """ + try: + with open(settings.DEFAULT_DATA_FILE, "r") as file: + data = json.load(file) + + for collection_name, documents in data.items(): + for doc in documents: + if "_id" in doc: + doc["_id"] = ObjectId(doc["_id"]) + + collection = db[collection_name] + collection.insert_many(documents) + print("Default data has been loaded successfully.") + except Exception as e: + print(f"Error loading default data: {e}") \ No newline at end of file diff --git a/backend/dump/dump.json b/backend/dump/dump.json new file mode 100755 index 0000000..9a26548 --- /dev/null +++ b/backend/dump/dump.json @@ -0,0 +1,1061 @@ +{ + "chat": [ + { + "_id": "6756c5c7bdb8727a5b52c6bf", + "participants": { + "67372742c192aa85549e9682": { + "name": "Сидоров Виктор Александрович", + "lastSeen": "2024-12-09T09:37:14.201000" + }, + "67372636c192aa85549e9681": { + "name": "Смирнова Елена Петровна", + "lastSeen": "2024-12-09T09:37:14.201000" + } + }, + "lastMessage": { + "content": "Это вполне возможно. Мы можем начать с покраски одного из коридоров и, если вам понравится, продолжим по всему зданию. Учитываем все пожелания по срокам и материалам.", + "sender": "67372742c192aa85549e9682", + "status": "Не прочитано", + "timestamp": "2024-12-09T09:37:14.202000" + }, + "created_at": "2024-12-09T09:37:14.203000", + "updated_at": "2024-12-09T10:31:18.369000" + }, + { + "_id": "6756c5e5bdb8727a5b52c6c1", + "participants": { + "67372742c192aa85549e9682": { + "name": "Сидоров Виктор Александрович", + "lastSeen": "2024-12-09T09:37:14.201000" + }, + "673727fdc192aa85549e9683": { + "name": "Кузнецов Алексей Сергеевич", + "lastSeen": "2024-12-09T09:37:14.201000" + } + }, + "lastMessage": { + "content": "Хорошо, я сейчас привезу дополнительный крепеж. Пожалуйста, продолжайте работы.", + "sender": "67372742c192aa85549e9682", + "status": "Не прочитано", + "timestamp": "2024-12-09T09:37:14.202000" + }, + "created_at": "2024-12-09T09:37:14.203000", + "updated_at": "2024-12-09T10:31:36.210000" + }, + { + "_id": "6756c5fabdb8727a5b52c6c3", + "participants": { + "67372742c192aa85549e9682": { + "name": "Сидоров Виктор Александрович", + "lastSeen": "2024-12-09T09:37:14.201000" + }, + "67372863c192aa85549e9684": { + "name": "Морозов Сергей Иванович", + "lastSeen": "2024-12-09T09:37:14.201000" + } + }, + "lastMessage": { + "content": "Отлично, давайте ускорим процесс, чтобы не задерживать покраску. Проверю материал для покраски, если все в порядке — начинаем сразу.", + "sender": "67372742c192aa85549e9682", + "status": "Не прочитано", + "timestamp": "2024-12-09T09:37:14.202000" + }, + "created_at": "2024-12-09T09:37:14.203000", + "updated_at": "2024-12-09T10:31:48.671000" + }, + { + "_id": "6756c638bdb8727a5b52c6c5", + "participants": { + "67372742c192aa85549e9682": { + "name": "Сидоров Виктор Александрович", + "lastSeen": "2024-12-09T09:37:14.201000" + }, + "67372917c192aa85549e9685": { + "name": "Петров Николай Андреевич", + "lastSeen": "2024-12-09T09:37:14.201000" + } + }, + "lastMessage": { + "content": "Понял, проверим. В принципе, монтаж можно закончить в течение двух дней, и после этого сделаем финальную проверку.", + "sender": "67372917c192aa85549e9685", + "status": "Не прочитано", + "timestamp": "2024-12-09T09:37:14.202000" + }, + "created_at": "2024-12-09T09:37:14.203000", + "updated_at": "2024-12-09T10:34:27.285000" + }, + { + "_id": "6756c64dbdb8727a5b52c6c7", + "participants": { + "67372742c192aa85549e9682": { + "name": "Сидоров Виктор Александрович", + "lastSeen": "2024-12-09T09:37:14.201000" + }, + "6756bc2ebdb8727a5b52c6bd": { + "name": "Страков Александр Викторович", + "lastSeen": "2024-12-09T09:37:14.201000" + } + }, + "lastMessage": { + "content": "Спасибо, все будет сделано в срок.", + "sender": "6756bc2ebdb8727a5b52c6bd", + "status": "Не прочитано", + "timestamp": "2024-12-09T09:37:14.202000" + }, + "created_at": "2024-12-09T09:37:14.203000", + "updated_at": "2024-12-09T10:35:27.685000" + } + ], + "project": [ + { + "_id": "6756bd40bdb8727a5b52c6be", + "name": "Ремонт и модернизация учебных корпусов университета", + "description": "Проект направлен на обновление инфраструктуры учебных корпусов университета. Включает ремонт аудиторий, модернизацию инженерных сетей, улучшение доступной среды для маломобильных групп населения и внедрение современных образовательных технологий.", + "start_date": "2024-12-09T00:00:00", + "end_date": "2025-07-30T00:00:00", + "status": "Новый", + "created_at": "2024-12-09T09:49:52.493000", + "updated_at": "2024-12-09T09:50:07.444000", + "contacts": { + "67372487c192aa85549e9680": { + "username": "Иванов Иван Иванович", + "role": "Администратор", + "created_at": "2024-12-09T09:37:14.076000", + "updated_at": "2024-12-09T09:37:14.076000" + }, + "67372636c192aa85549e9681": { + "username": "Смирнова Елена Петровна", + "role": "Заказчик", + "updated_at": "2024-12-09T09:50:15.964000", + "created_at": "2024-11-15T10:45:10.050000" + }, + "67372742c192aa85549e9682": { + "username": "Сидоров Виктор Александрович", + "role": "Прораб", + "updated_at": "2024-12-09T09:50:26.292000", + "created_at": "2024-11-15T10:49:38.947000" + }, + "673727fdc192aa85549e9683": { + "username": "Кузнецов Алексей Сергеевич", + "role": "Рабочий", + "updated_at": "2024-12-09T09:50:33.046000", + "created_at": "2024-11-15T10:52:45.608000" + }, + "67372863c192aa85549e9684": { + "username": "Морозов Сергей Иванович", + "role": "Рабочий", + "updated_at": "2024-12-09T09:50:40.275000", + "created_at": "2024-11-15T10:54:27.989000" + }, + "67372917c192aa85549e9685": { + "username": "Петров Николай Андреевич", + "role": "Рабочий", + "updated_at": "2024-12-09T09:50:48.242000", + "created_at": "2024-11-15T10:57:27.551000" + }, + "6756bc2ebdb8727a5b52c6bd": { + "username": "Страков Александр Викторович", + "role": "Рабочий", + "updated_at": "2024-12-09T09:50:59.214000", + "created_at": "2024-12-09T09:45:18.794000" + } + }, + "stages": { + "c58e3d4c-4f0f-44a0-ad1a-c8d882e31a23": { + "name": "Подготовительные работы", + "start_date": "2024-12-15T00:00:00", + "end_date": "2025-01-15T00:00:00", + "tasks": { + "a3f88b6a-919d-43f6-959c-ccf763a5cd6f": { + "name": "Составление плана и технического задания", + "description": "Разработка общего плана ремонта, определение ключевых задач и сроков реализации.", + "status": "Нет статуса", + "start_date": "2024-12-15T00:00:00", + "end_date": "2024-12-20T00:00:00", + "workers": { + "67372742c192aa85549e9682": { + "name": "Сидоров Виктор Александрович", + "role": "Прораб" + } + }, + "created_at": "2024-12-09T09:37:14.141000", + "updated_at": "2024-12-09T09:37:14.141000" + }, + "872a9313-f6f8-4c3d-8d63-eb22afb62f99": { + "name": "Проведение обследования зданий", + "description": "Техническое обследование состояния зданий и составление заключения о необходимости ремонтных работ.", + "status": "Нет статуса", + "start_date": "2024-12-21T00:00:00", + "end_date": "2025-01-05T00:00:00", + "workers": { + "67372742c192aa85549e9682": { + "name": "Сидоров Виктор Александрович", + "role": "Прораб" + } + }, + "created_at": "2024-12-09T09:37:14.141000", + "updated_at": "2024-12-09T09:37:14.141000" + }, + "0b2fa450-d055-4784-88a5-3a99025c99ae": { + "name": "Согласование проектной документации", + "description": "Обсуждение и утверждение проектной документации с руководством университета.", + "status": "Нет статуса", + "start_date": "2025-01-06T00:00:00", + "end_date": "2025-01-15T00:00:00", + "workers": { + "67372742c192aa85549e9682": { + "name": "Сидоров Виктор Александрович", + "role": "Прораб" + } + }, + "created_at": "2024-12-09T09:37:14.141000", + "updated_at": "2024-12-09T09:37:14.141000" + } + }, + "created_at": "2024-12-09T09:37:14.149000", + "updated_at": "2024-12-09T09:37:14.149000" + }, + "b674e7e7-e775-498a-83bf-a4b0032cba31": { + "name": "Ремонт и модернизация инженерных сетей", + "start_date": "2025-01-16T00:00:00", + "end_date": "2025-03-15T00:00:00", + "tasks": { + "e9dca3e0-61b4-41da-b2a1-aa01b5c2ece8": { + "name": "Демонтаж устаревших инженерных систем", + "description": "Удаление старых труб, проводки и других элементов коммуникаций.", + "status": "Нет статуса", + "start_date": "2025-01-16T00:00:00", + "end_date": "2025-01-31T00:00:00", + "workers": { + "673727fdc192aa85549e9683": { + "name": "Кузнецов Алексей Сергеевич", + "role": "Рабочий" + }, + "67372863c192aa85549e9684": { + "name": "Морозов Сергей Иванович", + "role": "Рабочий" + } + }, + "created_at": "2024-12-09T09:37:14.141000", + "updated_at": "2024-12-09T09:37:14.141000" + }, + "fd81c08d-cdd8-4622-845b-1abdfe16c920": { + "name": "Установка новых инженерных систем", + "description": "Монтаж современных систем отопления, электроснабжения и вентиляции.", + "status": "Нет статуса", + "start_date": "2025-02-01T00:00:00", + "end_date": "2025-02-28T00:00:00", + "workers": { + "67372917c192aa85549e9685": { + "name": "Петров Николай Андреевич", + "role": "Рабочий" + }, + "6756bc2ebdb8727a5b52c6bd": { + "name": "Страков Александр Викторович", + "role": "Рабочий" + } + }, + "created_at": "2024-12-09T09:37:14.141000", + "updated_at": "2024-12-09T09:37:14.141000" + }, + "85802005-e7b3-4633-8ff2-5b15f8739dc5": { + "name": "Проверка и тестирование систем", + "description": "Проведение испытаний и настройка инженерных систем перед вводом в эксплуатацию.", + "status": "Нет статуса", + "start_date": "2025-03-01T00:00:00", + "end_date": "2025-03-15T00:00:00", + "workers": { + "673727fdc192aa85549e9683": { + "name": "Кузнецов Алексей Сергеевич", + "role": "Рабочий" + }, + "67372917c192aa85549e9685": { + "name": "Петров Николай Андреевич", + "role": "Рабочий" + }, + "6756bc2ebdb8727a5b52c6bd": { + "name": "Страков Александр Викторович", + "role": "Рабочий" + } + }, + "created_at": "2024-12-09T09:37:14.141000", + "updated_at": "2024-12-09T09:37:14.141000" + } + }, + "created_at": "2024-12-09T09:37:14.149000", + "updated_at": "2024-12-09T09:37:14.149000" + }, + "2c802c7e-33d1-4e60-9d69-62bfbf83eab3": { + "name": "Ремонт учебных помещений", + "start_date": "2025-03-16T00:00:00", + "end_date": "2025-05-30T00:00:00", + "tasks": { + "3f2467b1-2d42-4742-8613-d9022cdfc2f7": { + "name": "Демонтаж старой отделки и мебели", + "description": " Удаление обветшавших покрытий стен, полов и старой мебели.", + "status": "Нет статуса", + "start_date": "2025-03-16T00:00:00", + "end_date": "2025-04-01T00:00:00", + "workers": { + "67372863c192aa85549e9684": { + "name": "Морозов Сергей Иванович", + "role": "Рабочий" + } + }, + "created_at": "2024-12-09T09:37:14.141000", + "updated_at": "2024-12-09T09:37:14.141000" + }, + "82dc4835-8199-4294-b19c-6685c9d5e200": { + "name": "Проведение отделочных работ", + "description": "Покраска стен, укладка новых полов, установка подвесных потолков.", + "status": "Нет статуса", + "start_date": "2025-04-02T00:00:00", + "end_date": "2025-05-05T00:00:00", + "workers": { + "67372917c192aa85549e9685": { + "name": "Петров Николай Андреевич", + "role": "Рабочий" + }, + "6756bc2ebdb8727a5b52c6bd": { + "name": "Страков Александр Викторович", + "role": "Рабочий" + } + }, + "created_at": "2024-12-09T09:37:14.141000", + "updated_at": "2024-12-09T09:37:14.141000" + }, + "0b6fbd83-55f1-49fe-abe9-a8c9d64294fa": { + "name": "Установка новой мебели и оборудования", + "description": "Расстановка мебели и установка современных образовательных технологий, таких как интерактивные панели.", + "status": "Нет статуса", + "start_date": "2025-05-02T00:00:00", + "end_date": "2025-05-30T00:00:00", + "workers": { + "673727fdc192aa85549e9683": { + "name": "Кузнецов Алексей Сергеевич", + "role": "Рабочий" + }, + "67372863c192aa85549e9684": { + "name": "Морозов Сергей Иванович", + "role": "Рабочий" + }, + "67372917c192aa85549e9685": { + "name": "Петров Николай Андреевич", + "role": "Рабочий" + }, + "6756bc2ebdb8727a5b52c6bd": { + "name": "Страков Александр Викторович", + "role": "Рабочий" + } + }, + "created_at": "2024-12-09T09:37:14.141000", + "updated_at": "2024-12-09T09:37:14.141000" + } + }, + "created_at": "2024-12-09T09:37:14.149000", + "updated_at": "2024-12-09T09:37:14.149000" + }, + "08c93779-31e1-4da8-84af-3076953d07d3": { + "name": "Благоустройство прилегающей территории", + "start_date": "2025-06-01T00:00:00", + "end_date": "2025-07-30T00:00:00", + "tasks": { + "e090320c-9cf6-478d-8831-ecb9120042cf": { + "name": "Очистка территории и вывоз мусора", + "description": "Уборка территории после строительных работ, вывоз мусора и подготовка к благоустройству.", + "status": "Нет статуса", + "start_date": "2025-06-01T00:00:00", + "end_date": "2025-06-15T00:00:00", + "workers": { + "673727fdc192aa85549e9683": { + "name": "Кузнецов Алексей Сергеевич", + "role": "Рабочий" + } + }, + "created_at": "2024-12-09T09:37:14.141000", + "updated_at": "2024-12-09T09:37:14.141000" + }, + "5e15b785-5f1c-4477-ab5e-b4643f2cb21e": { + "name": "Укладка плитки и установка освещения", + "description": "Обустройство пешеходных дорожек и установка уличных фонарей.", + "status": "Нет статуса", + "start_date": "2025-06-16T00:00:00", + "end_date": "2025-07-01T00:00:00", + "workers": { + "67372863c192aa85549e9684": { + "name": "Морозов Сергей Иванович", + "role": "Рабочий" + }, + "67372917c192aa85549e9685": { + "name": "Петров Николай Андреевич", + "role": "Рабочий" + } + }, + "created_at": "2024-12-09T09:37:14.141000", + "updated_at": "2024-12-09T09:37:14.141000" + }, + "1225b2bb-d0bf-40e9-981e-4f90d66b416d": { + "name": "Озеленение территории", + "description": "Посадка деревьев, кустарников и оформление газонов для создания комфортной атмосферы.", + "status": "Нет статуса", + "start_date": "2025-07-02T00:00:00", + "end_date": "2025-07-30T00:00:00", + "workers": { + "6756bc2ebdb8727a5b52c6bd": { + "name": "Страков Александр Викторович", + "role": "Рабочий" + } + }, + "created_at": "2024-12-09T09:37:14.141000", + "updated_at": "2024-12-09T09:37:14.141000" + } + }, + "created_at": "2024-12-09T09:37:14.149000", + "updated_at": "2024-12-09T09:37:14.149000" + } + }, + "procurements": { + "a6a1dc83-ad03-4456-8a37-e8b07545267e": { + "item_name": "Цемент", + "quantity": "200", + "price": "50000.0", + "inStock": false, + "units": "тонна", + "delivery_date": null, + "created_by": { + "username": "Иванов Иван Иванович", + "role": "Администратор", + "created_at": "2024-12-09T09:37:14.076000", + "updated_at": "2024-12-09T09:37:14.076000" + }, + "created_at": "2024-12-09T09:37:14.156000", + "updated_at": "2024-12-09T09:37:14.156000" + }, + "57413bee-1178-4766-9f1b-7cb29293c5de": { + "item_name": "Песок", + "quantity": "150", + "price": "3500.0", + "inStock": false, + "units": "тонна", + "delivery_date": null, + "created_by": { + "username": "Иванов Иван Иванович", + "role": "Администратор", + "created_at": "2024-12-09T09:37:14.076000", + "updated_at": "2024-12-09T09:37:14.076000" + }, + "created_at": "2024-12-09T09:37:14.156000", + "updated_at": "2024-12-09T09:37:14.156000" + }, + "1354033e-7fb0-4ddc-b25b-7bd0fa6ba539": { + "item_name": "Штукатурка", + "quantity": "500", + "price": "350.0", + "inStock": true, + "units": "м²", + "delivery_date": null, + "created_by": { + "username": "Иванов Иван Иванович", + "role": "Администратор", + "created_at": "2024-12-09T09:37:14.076000", + "updated_at": "2024-12-09T09:37:14.076000" + }, + "created_at": "2024-12-09T09:37:14.156000", + "updated_at": "2024-12-09T09:37:14.156000" + }, + "fd56d28c-9076-418b-90a8-31c7f16b95b0": { + "item_name": "Окна", + "quantity": "100", + "price": "50000.0", + "inStock": true, + "units": "шт", + "delivery_date": null, + "created_by": { + "username": "Иванов Иван Иванович", + "role": "Администратор", + "created_at": "2024-12-09T09:37:14.076000", + "updated_at": "2024-12-09T09:37:14.076000" + }, + "created_at": "2024-12-09T09:37:14.156000", + "updated_at": "2024-12-09T09:37:14.156000" + }, + "ed330a35-8b10-494d-b978-a133bbacfbc2": { + "item_name": "Двери", + "quantity": "15", + "price": "8000.0", + "inStock": false, + "units": "шт", + "delivery_date": null, + "created_by": { + "username": "Иванов Иван Иванович", + "role": "Администратор", + "created_at": "2024-12-09T09:37:14.076000", + "updated_at": "2024-12-09T09:37:14.076000" + }, + "created_at": "2024-12-09T09:37:14.156000", + "updated_at": "2024-12-09T09:37:14.156000" + }, + "e5506d75-8f1f-4d7a-a0cc-0c7335722b4f": { + "item_name": "Подмости строительные", + "quantity": "50", + "price": "1000.0", + "inStock": false, + "units": "м", + "delivery_date": null, + "created_by": { + "username": "Иванов Иван Иванович", + "role": "Администратор", + "created_at": "2024-12-09T09:37:14.076000", + "updated_at": "2024-12-09T09:37:14.076000" + }, + "created_at": "2024-12-09T09:37:14.156000", + "updated_at": "2024-12-09T09:37:14.156000" + }, + "b7c84174-5bbb-4d76-a54e-2fd784679e9d": { + "item_name": "Электрический кабель", + "quantity": "1000", + "price": "50.0", + "inStock": false, + "units": "м", + "delivery_date": null, + "created_by": { + "username": "Иванов Иван Иванович", + "role": "Администратор", + "created_at": "2024-12-09T09:37:14.076000", + "updated_at": "2024-12-09T09:37:14.076000" + }, + "created_at": "2024-12-09T09:37:14.156000", + "updated_at": "2024-12-09T09:37:14.156000" + }, + "bcb2261a-1d36-475c-8e47-252f0f6827e8": { + "item_name": " Трубопровод для водоснабжения", + "quantity": "500", + "price": "150.0", + "inStock": true, + "units": "м", + "delivery_date": null, + "created_by": { + "username": "Иванов Иван Иванович", + "role": "Администратор", + "created_at": "2024-12-09T09:37:14.076000", + "updated_at": "2024-12-09T09:37:14.076000" + }, + "created_at": "2024-12-09T09:37:14.156000", + "updated_at": "2024-12-09T09:37:14.156000" + }, + "5110aa18-4ebe-47d1-8b00-76e56a3acab5": { + "item_name": "Сантехнические смесители", + "quantity": "80", + "price": "2500.0", + "inStock": true, + "units": "шт", + "delivery_date": null, + "created_by": { + "username": "Иванов Иван Иванович", + "role": "Администратор", + "created_at": "2024-12-09T09:37:14.076000", + "updated_at": "2024-12-09T09:37:14.076000" + }, + "created_at": "2024-12-09T09:37:14.156000", + "updated_at": "2024-12-09T09:37:14.156000" + }, + "29ca81be-ed65-4763-b81c-a4cce63e3963": { + "item_name": "Краска для стен", + "quantity": "300", + "price": "800.0", + "inStock": false, + "units": "л", + "delivery_date": null, + "created_by": { + "username": "Иванов Иван Иванович", + "role": "Администратор", + "created_at": "2024-12-09T09:37:14.076000", + "updated_at": "2024-12-09T09:37:14.076000" + }, + "created_at": "2024-12-09T09:37:14.156000", + "updated_at": "2024-12-09T09:37:14.156000" + }, + "b48c23b7-92b2-4c2c-bbb1-5a147775f13c": { + "item_name": "Ламинат", + "quantity": "500", + "price": "1200.0", + "inStock": false, + "units": "м²", + "delivery_date": null, + "created_by": { + "username": "Иванов Иван Иванович", + "role": "Администратор", + "created_at": "2024-12-09T09:37:14.076000", + "updated_at": "2024-12-09T09:37:14.076000" + }, + "created_at": "2024-12-09T09:37:14.156000", + "updated_at": "2024-12-09T09:37:14.156000" + }, + "56ad52f1-3afb-4246-a18f-5c38685d9f51": { + "item_name": "Обои", + "quantity": "200", + "price": "1500.0", + "inStock": false, + "units": "рулоны", + "delivery_date": null, + "created_by": { + "username": "Иванов Иван Иванович", + "role": "Администратор", + "created_at": "2024-12-09T09:37:14.076000", + "updated_at": "2024-12-09T09:37:14.076000" + }, + "created_at": "2024-12-09T09:37:14.156000", + "updated_at": "2024-12-09T09:37:14.156000" + }, + "303d6523-e9f6-4377-975a-e587fdff7cdc": { + "item_name": "Полы из линолеума", + "quantity": "250", + "price": "900.0", + "inStock": true, + "units": "м²", + "delivery_date": null, + "created_by": { + "username": "Иванов Иван Иванович", + "role": "Администратор", + "created_at": "2024-12-09T09:37:14.076000", + "updated_at": "2024-12-09T09:37:14.076000" + }, + "created_at": "2024-12-09T09:37:14.156000", + "updated_at": "2024-12-09T09:37:14.156000" + }, + "d00037af-4757-4185-9625-bf792fdb3b44": { + "item_name": "Гипсокартон", + "quantity": "150", + "price": "350.0", + "inStock": false, + "units": "лист", + "delivery_date": null, + "created_by": { + "username": "Иванов Иван Иванович", + "role": "Администратор", + "created_at": "2024-12-09T09:37:14.076000", + "updated_at": "2024-12-09T09:37:14.076000" + }, + "created_at": "2024-12-09T09:37:14.156000", + "updated_at": "2024-12-09T09:37:14.156000" + }, + "c1ff43ed-56b3-42b0-805f-67a659865eff": { + "item_name": "Сухая строительная смесь", + "quantity": "200", + "price": "700.0", + "inStock": false, + "units": "мешки", + "delivery_date": null, + "created_by": { + "username": "Иванов Иван Иванович", + "role": "Администратор", + "created_at": "2024-12-09T09:37:14.076000", + "updated_at": "2024-12-09T09:37:14.076000" + }, + "created_at": "2024-12-09T09:37:14.156000", + "updated_at": "2024-12-09T09:37:14.156000" + }, + "8f65d657-2a19-4f92-8917-8a7bd81d81d1": { + "item_name": "Строительные ножницы", + "quantity": "20", + "price": "600.0", + "inStock": true, + "units": "м²", + "delivery_date": null, + "created_by": { + "username": "Иванов Иван Иванович", + "role": "Администратор", + "created_at": "2024-12-09T09:37:14.076000", + "updated_at": "2024-12-09T09:37:14.076000" + }, + "created_at": "2024-12-09T09:37:14.156000", + "updated_at": "2024-12-09T09:37:14.156000" + }, + "c77e1ca4-fa48-4df7-bd3d-1aede1bf5dda": { + "item_name": "Плитка керамическая", + "quantity": "300", + "price": "1500.0", + "inStock": true, + "units": "м²", + "delivery_date": null, + "created_by": { + "username": "Иванов Иван Иванович", + "role": "Администратор", + "created_at": "2024-12-09T09:37:14.076000", + "updated_at": "2024-12-09T09:37:14.076000" + }, + "created_at": "2024-12-09T09:37:14.156000", + "updated_at": "2024-12-09T09:37:14.156000" + }, + "40944af6-8cac-40d7-b2b4-3b9b0a09e12d": { + "item_name": "Строительный клей", + "quantity": "500", + "price": "250.0", + "inStock": true, + "units": "кг", + "delivery_date": null, + "created_by": { + "username": "Иванов Иван Иванович", + "role": "Администратор", + "created_at": "2024-12-09T09:37:14.076000", + "updated_at": "2024-12-09T09:37:14.076000" + }, + "created_at": "2024-12-09T09:37:14.156000", + "updated_at": "2024-12-09T09:37:14.156000" + }, + "fe711323-4aa4-48d2-a38e-0c012a9bce36": { + "item_name": "Освежители воздуха", + "quantity": "50", + "price": "10.0", + "inStock": true, + "units": "шт", + "delivery_date": null, + "created_by": { + "username": "Иванов Иван Иванович", + "role": "Администратор", + "created_at": "2024-12-09T09:37:14.076000", + "updated_at": "2024-12-09T09:37:14.076000" + }, + "created_at": "2024-12-09T09:37:14.156000", + "updated_at": "2024-12-09T09:37:14.156000" + }, + "4a627919-40b7-4e66-af9a-d624268aef6b": { + "item_name": "Ручной инструмент (молоты, отвертки и т.п.)", + "quantity": "100", + "price": "500.0", + "inStock": false, + "units": "шт", + "delivery_date": null, + "created_by": { + "username": "Иванов Иван Иванович", + "role": "Администратор", + "created_at": "2024-12-09T09:37:14.076000", + "updated_at": "2024-12-09T09:37:14.076000" + }, + "created_at": "2024-12-09T09:37:14.156000", + "updated_at": "2024-12-09T09:37:14.156000" + } + }, + "risks": { + "8c0f9b22-b122-41ba-ac61-991bfc016cfe": { + "name": "Задержка поставок материалов", + "description": "Нарушение сроков доставки может привести к задержкам в выполнении работ.", + "created_at": "2024-12-09T09:37:14.152000", + "updated_at": "2024-12-09T09:37:14.152000" + }, + "6f5d85d5-1731-4ca4-ae74-10e2bc1f7783": { + "name": "Выявление скрытых дефектов", + "description": "В процессе ремонта могут быть обнаружены проблемы, не учтенные в первоначальном проекте.", + "created_at": "2024-12-09T09:37:14.152000", + "updated_at": "2024-12-09T09:37:14.152000" + }, + "8566a879-498c-46a0-bbc3-f53fdc52bc0d": { + "name": "Конфликты с подрядчиками", + "description": "Невыполнение подрядчиками своих обязанностей или необходимость замены подрядчиков.", + "created_at": "2024-12-09T09:37:14.152000", + "updated_at": "2024-12-09T09:37:14.152000" + }, + "978363d2-53c1-41bb-b23e-f0e9062f886a": { + "name": "Перекрытие доступа к учебным аудиториям", + "description": "Проведение ремонта в период учебного года может усложнить организацию занятий.", + "created_at": "2024-12-09T09:37:14.152000", + "updated_at": "2024-12-09T09:37:14.152000" + }, + "a1cabe42-434e-4da7-b1d9-1d2c97f42b50": { + "name": "Превышение бюджета", + "description": "Непредвиденные расходы на дополнительные работы или материалы могут превысить запланированный бюджет.", + "created_at": "2024-12-09T09:37:14.152000", + "updated_at": "2024-12-09T09:37:14.152000" + } + } + } + ], + "message": [ + { + "_id": "6756c5c8bdb8727a5b52c6c0", + "content": "Здравствуйте! Начали работы по ремонту, сейчас завершаем демонтаж старых окон и дверей. Планируем завершить на этой неделе. Могу ли я уточнить, есть ли еще пожелания по дизайну помещений?", + "sender": "67372742c192aa85549e9682", + "status": "Не прочитано", + "timestamp": "2024-12-09T09:37:14.202000", + "chatId": "6756c5c7bdb8727a5b52c6bf", + "receiver": "67372636c192aa85549e9681" + }, + { + "_id": "6756c5e5bdb8727a5b52c6c2", + "content": "Алексей, как идут работы по установке окон? Нужно ли вам помочь с инструментами или материалами?", + "sender": "67372742c192aa85549e9682", + "status": "Не прочитано", + "timestamp": "2024-12-09T09:37:14.202000", + "chatId": "6756c5e5bdb8727a5b52c6c1", + "receiver": "673727fdc192aa85549e9683" + }, + { + "_id": "6756c5fabdb8727a5b52c6c4", + "content": "Сергей, как продвигается штукатурка стен в аудиториях? Нужно ли провести дополнительные проверки?", + "sender": "67372742c192aa85549e9682", + "status": "Не прочитано", + "timestamp": "2024-12-09T09:37:14.202000", + "chatId": "6756c5fabdb8727a5b52c6c3", + "receiver": "67372863c192aa85549e9684" + }, + { + "_id": "6756c638bdb8727a5b52c6c6", + "content": "Николай, как идет монтаж сантехники в санузлах? Есть ли какие-то проблемы с трубами или оборудованием?", + "sender": "67372742c192aa85549e9682", + "status": "Не прочитано", + "timestamp": "2024-12-09T09:37:14.202000", + "chatId": "6756c638bdb8727a5b52c6c5", + "receiver": "67372917c192aa85549e9685" + }, + { + "_id": "6756c64dbdb8727a5b52c6c8", + "content": "Александр, как идет прокладка электропроводки? Вижу, что уже начали монтировать розетки и выключатели. Как там?", + "sender": "67372742c192aa85549e9682", + "status": "Не прочитано", + "timestamp": "2024-12-09T09:37:14.202000", + "chatId": "6756c64dbdb8727a5b52c6c7", + "receiver": "6756bc2ebdb8727a5b52c6bd" + }, + { + "_id": "6756c66bbdb8727a5b52c6c9", + "content": "Здравствуйте! Спасибо за обновления. По поводу дизайна — хотелось бы изменить цвет стен на более светлый, например, на пастельный оттенок. Как думаете, это возможно?", + "sender": "67372636c192aa85549e9681", + "status": "Не прочитано", + "timestamp": "2024-12-09T10:28:59.579000", + "chatId": "6756c5c7bdb8727a5b52c6bf", + "receiver": "67372742c192aa85549e9682" + }, + { + "_id": "6756c692bdb8727a5b52c6ca", + "content": "Все идет по плану, окна устанавливаем без задержек. Единственное — немного не хватает крепежа, но это решаемо.", + "sender": "673727fdc192aa85549e9683", + "status": "Не прочитано", + "timestamp": "2024-12-09T10:29:38.701000", + "chatId": "6756c5e5bdb8727a5b52c6c1", + "receiver": "67372742c192aa85549e9682" + }, + { + "_id": "6756c6afbdb8727a5b52c6cb", + "content": "Стены почти готовы, еще один слой штукатурки, и можно будет приступать к покраске. Пока не вижу проблем, работаем как надо.", + "sender": "67372863c192aa85549e9684", + "status": "Не прочитано", + "timestamp": "2024-12-09T10:30:07.463000", + "chatId": "6756c5fabdb8727a5b52c6c3", + "receiver": "67372742c192aa85549e9682" + }, + { + "_id": "6756c6c7bdb8727a5b52c6cc", + "content": "В целом все идет по плану. Мы установили трубы для горячей и холодной воды, подключили раковины и унитазы. Немного сложности возникли с установкой душевых кабин — не все крепежи подошли.", + "sender": "67372917c192aa85549e9685", + "status": "Не прочитано", + "timestamp": "2024-12-09T10:30:31.982000", + "chatId": "6756c638bdb8727a5b52c6c5", + "receiver": "67372742c192aa85549e9682" + }, + { + "_id": "6756c6d8bdb8727a5b52c6cd", + "content": "Электропроводка готова в двух аудиториях и коридоре. Прокладываем дальше в другие помещения. Но возникла небольшая проблема с проводами для освещения в одном из кабинетов, надо будет заменить их на более мощные.", + "sender": "6756bc2ebdb8727a5b52c6bd", + "status": "Не прочитано", + "timestamp": "2024-12-09T10:30:48.973000", + "chatId": "6756c64dbdb8727a5b52c6c7", + "receiver": "67372742c192aa85549e9682" + }, + { + "_id": "6756c6f6bdb8727a5b52c6ce", + "content": "Это вполне возможно. Мы можем начать с покраски одного из коридоров и, если вам понравится, продолжим по всему зданию. Учитываем все пожелания по срокам и материалам.", + "sender": "67372742c192aa85549e9682", + "status": "Не прочитано", + "timestamp": "2024-12-09T10:31:18.370000", + "chatId": "6756c5c7bdb8727a5b52c6bf", + "receiver": "67372636c192aa85549e9681" + }, + { + "_id": "6756c708bdb8727a5b52c6cf", + "content": "Хорошо, я сейчас привезу дополнительный крепеж. Пожалуйста, продолжайте работы.", + "sender": "67372742c192aa85549e9682", + "status": "Не прочитано", + "timestamp": "2024-12-09T10:31:36.212000", + "chatId": "6756c5e5bdb8727a5b52c6c1", + "receiver": "673727fdc192aa85549e9683" + }, + { + "_id": "6756c714bdb8727a5b52c6d0", + "content": "Отлично, давайте ускорим процесс, чтобы не задерживать покраску. Проверю материал для покраски, если все в порядке — начинаем сразу.", + "sender": "67372742c192aa85549e9682", + "status": "Не прочитано", + "timestamp": "2024-12-09T10:31:48.673000", + "chatId": "6756c5fabdb8727a5b52c6c3", + "receiver": "67372863c192aa85549e9684" + }, + { + "_id": "6756c728bdb8727a5b52c6d1", + "content": "Понял. Если понадобятся дополнительные крепежи, скажите, привезу. Как насчет проверки всех соединений на герметичность? Лучше это сделать до завершения.", + "sender": "67372742c192aa85549e9682", + "status": "Не прочитано", + "timestamp": "2024-12-09T10:32:08.331000", + "chatId": "6756c638bdb8727a5b52c6c5", + "receiver": "67372917c192aa85549e9685" + }, + { + "_id": "6756c738bdb8727a5b52c6d2", + "content": "Понял, замена проводов — это важный момент. Постарайтесь решить это как можно быстрее. Важно, чтобы освещение было стабильным и безопасным.", + "sender": "67372742c192aa85549e9682", + "status": "Не прочитано", + "timestamp": "2024-12-09T10:32:24.202000", + "chatId": "6756c64dbdb8727a5b52c6c7", + "receiver": "6756bc2ebdb8727a5b52c6bd" + }, + { + "_id": "6756c753bdb8727a5b52c6d3", + "content": "Я проверил все соединения, нет утечек, но на всякий случай стоит провести вторичную проверку после установки душевых кабин.", + "sender": "67372917c192aa85549e9685", + "status": "Не прочитано", + "timestamp": "2024-12-09T10:32:51.844000", + "chatId": "6756c638bdb8727a5b52c6c5", + "receiver": "67372742c192aa85549e9682" + }, + { + "_id": "6756c768bdb8727a5b52c6d4", + "content": "Да, не переживайте, провода уже заказали. Завтра привезут. Параллельно продолжаем работу в других помещениях. Появятся ли еще работы по электрике в следующих этапах?", + "sender": "6756bc2ebdb8727a5b52c6bd", + "status": "Не прочитано", + "timestamp": "2024-12-09T10:33:12.698000", + "chatId": "6756c64dbdb8727a5b52c6c7", + "receiver": "67372742c192aa85549e9682" + }, + { + "_id": "6756c786bdb8727a5b52c6d5", + "content": "Хорошо, давайте сделаем проверку на следующей неделе, как только все установим. Также проверьте, чтобы сливные системы были правильно подключены, а то могут быть проблемы с течью в будущем.", + "sender": "67372742c192aa85549e9682", + "status": "Не прочитано", + "timestamp": "2024-12-09T10:33:42.361000", + "chatId": "6756c638bdb8727a5b52c6c5", + "receiver": "67372917c192aa85549e9685" + }, + { + "_id": "6756c79dbdb8727a5b52c6d6", + "content": "Да, после того как все основные работы с электрикой завершатся, нужно будет установить наружное освещение и подключить систему безопасности. Мы начнем с этого после того, как сделаем все тесты.", + "sender": "67372742c192aa85549e9682", + "status": "Не прочитано", + "timestamp": "2024-12-09T10:34:05.223000", + "chatId": "6756c64dbdb8727a5b52c6c7", + "receiver": "6756bc2ebdb8727a5b52c6bd" + }, + { + "_id": "6756c7b3bdb8727a5b52c6d7", + "content": "Понял, проверим. В принципе, монтаж можно закончить в течение двух дней, и после этого сделаем финальную проверку.", + "sender": "67372917c192aa85549e9685", + "status": "Не прочитано", + "timestamp": "2024-12-09T10:34:27.286000", + "chatId": "6756c638bdb8727a5b52c6c5", + "receiver": "67372742c192aa85549e9682" + }, + { + "_id": "6756c7ccbdb8727a5b52c6d8", + "content": "Хорошо, тогда все будет готово к следующей неделе. Я напишу, как только придут новые провода и мы сможем продолжить работы.", + "sender": "6756bc2ebdb8727a5b52c6bd", + "status": "Не прочитано", + "timestamp": "2024-12-09T10:34:52.666000", + "chatId": "6756c64dbdb8727a5b52c6c7", + "receiver": "67372742c192aa85549e9682" + }, + { + "_id": "6756c7e0bdb8727a5b52c6d9", + "content": "Отлично, жду от вас отчета. Если понадобится помощь в организации дополнительных материалов, сообщите.", + "sender": "67372742c192aa85549e9682", + "status": "Не прочитано", + "timestamp": "2024-12-09T10:35:12.024000", + "chatId": "6756c64dbdb8727a5b52c6c7", + "receiver": "6756bc2ebdb8727a5b52c6bd" + }, + { + "_id": "6756c7efbdb8727a5b52c6da", + "content": "Спасибо, все будет сделано в срок.", + "sender": "6756bc2ebdb8727a5b52c6bd", + "status": "Не прочитано", + "timestamp": "2024-12-09T10:35:27.687000", + "chatId": "6756c64dbdb8727a5b52c6c7", + "receiver": "67372742c192aa85549e9682" + } + ], + "user": [ + { + "_id": "67372487c192aa85549e9680", + "email": "admin1@example.com", + "name": "Иван", + "lastname": "Иванов", + "middlename": "Иванович", + "password": "$2b$12$2BkJdyuM0uQwYlMM.x7DXOUMP.k9S9CVQNiSdzuZPUwa4TNLoZWbS", + "role": "Администратор", + "created_at": "2024-11-15T10:37:59.101000", + "updated_at": "2024-11-15T10:37:59.101000" + }, + { + "_id": "67372636c192aa85549e9681", + "email": "customer1@example.com", + "name": "Елена", + "lastname": "Смирнова", + "middlename": "Петровна", + "password": "$2b$12$vjPWtVwwAFh.SCjDXeopXuJBAKfPhksDbybWwgvtCT9kek0e.xnEq", + "role": "Заказчик", + "created_at": "2024-11-15T10:45:10.050000", + "updated_at": "2024-11-15T10:45:10.050000" + }, + { + "_id": "67372742c192aa85549e9682", + "email": "foreman1@example.com", + "name": "Виктор", + "lastname": "Сидоров", + "middlename": "Александрович", + "password": "$2b$12$IfZrTTAy.G8C4wOt3U.DheiDmIAvYs5Wn0YViE4Yy5wVlvF2TdgMa", + "role": "Прораб", + "created_at": "2024-11-15T10:49:38.947000", + "updated_at": "2024-11-15T10:49:38.947000" + }, + { + "_id": "673727fdc192aa85549e9683", + "email": "worker1@example.com", + "name": "Алексей", + "lastname": "Кузнецов", + "middlename": "Сергеевич", + "password": "$2b$12$DKcsmtlsWSGHqeBOuaLO0OZEzPheM1Ew.rG3xoftKNhyrA89dLacm", + "role": "Рабочий", + "created_at": "2024-11-15T10:52:45.608000", + "updated_at": "2024-11-15T10:52:45.608000" + }, + { + "_id": "67372863c192aa85549e9684", + "email": "worker2@example.com", + "name": "Сергей", + "lastname": "Морозов", + "middlename": " Иванович", + "password": "$2b$12$u0Ncvro827IXItz1vczNW.MiMFGWfd3vvBycD3sONrerFS4.Us2bu", + "role": "Рабочий", + "created_at": "2024-11-15T10:54:27.989000", + "updated_at": "2024-11-15T10:54:27.989000" + }, + { + "_id": "67372917c192aa85549e9685", + "email": "worker3@example.com", + "name": "Николай", + "lastname": "Петров", + "middlename": "Андреевич", + "password": "$2b$12$WjclQu47rcwq8klB0edBzeHh5MjsbgP5e.W2csflFARv12QKjC9OW", + "role": "Рабочий", + "created_at": "2024-11-15T10:57:27.551000", + "updated_at": "2024-11-15T10:57:27.551000" + }, + { + "_id": "6756bc2ebdb8727a5b52c6bd", + "email": "worker4@example.com", + "name": " Александр", + "lastname": "Страков", + "middlename": "Викторович", + "password": "$2b$12$SbY50Fat.OJ40hOz.9Gequ3rW34.1IDEGeQtahUhljUCiMKEmjtQa", + "role": "Рабочий", + "updated_at": "2024-12-09T09:45:18.794000", + "created_at": "2024-12-09T09:45:18.794000" + } + ] +} \ No newline at end of file diff --git a/backend/init_users.py b/backend/init_users.py deleted file mode 100644 index 2fc3692..0000000 --- a/backend/init_users.py +++ /dev/null @@ -1,57 +0,0 @@ -from dao.user import create_user, find_user_by_email -from schemas.user import UserCreateSchema, Role -from utils.password import get_password_hash - - -async def create_users(): - admin_init = UserCreateSchema( - name='Иван', - lastname='Петров', - middlename='Александрович', - email='ivan.petrov@example.com', - password=get_password_hash('admin123'), - role=Role.admin - ) - - foreman_init = UserCreateSchema( - name='Мария', - lastname='Смирнова', - middlename='Ивановна', - email='maria.smirnova@example.com', - password=get_password_hash('securepassword1'), - role=Role.foreman - ) - - worker_init = UserCreateSchema( - name='Дмитрий', - lastname='Кузнецов', - middlename='Петрович', - email='dmitry.kuznetsov@example.com', - password=get_password_hash('securepassword2'), - role=Role.worker - ) - - customer_init = UserCreateSchema( - name='Алексей', - lastname='Сидоров', - middlename='Леонидович', - email='alex.sidorov@example.com', - password=get_password_hash('securepassword3'), - role=Role.customer - ) - - exist_admin = await find_user_by_email(admin_init.email) - if exist_admin is None: - await create_user(admin_init) - - exist_foreman = await find_user_by_email(foreman_init.email) - if exist_foreman is None: - await create_user(foreman_init) - - exist_worker = await find_user_by_email(worker_init.email) - if exist_worker is None: - await create_user(worker_init) - - exist_customer = await find_user_by_email(customer_init.email) - if exist_customer is None: - await create_user(customer_init) diff --git a/backend/main.py b/backend/main.py index 7befc24..12ac824 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1,26 +1,25 @@ from contextlib import asynccontextmanager import uvicorn -from fastapi import FastAPI, Request +from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware -from database import db, create_indexation -from init_users import create_users -from routers import auth, user, project, task, message +from database import db, create_indexation, is_database_empty, load_default_data +from routers import (auth, user, project, task, message, risk, + stage, contact, procurement, stat, data) @asynccontextmanager async def lifespan(app: FastAPI): app.database = db + if await is_database_empty(): + load_default_data() await create_indexation() - await create_users() - yield + yield app = FastAPI(lifespan=lifespan) - - allowed_origins = [ "http://localhost:8080", "http://localhost:8000", @@ -37,9 +36,15 @@ async def lifespan(app: FastAPI): app.include_router(auth.router, tags=['Auth'], prefix="/api/auth") app.include_router(user.router, tags=['User'], prefix="/api/user") app.include_router(project.router, tags=['Project'], prefix="/api/projects") +app.include_router(risk.router, tags=['Risk'], prefix="/api/projects") +app.include_router(stage.router, tags=['Stage'], prefix="/api/projects") +app.include_router(contact.router, tags=['Contact'], prefix="/api/projects") +app.include_router(procurement.router, tags=['Procurement'], prefix="/api/projects") app.include_router(task.router, tags=['Task'], prefix="/api/tasks") app.include_router(message.router, tags=['Message'], prefix="/api/message") +app.include_router(stat.router, tags=['Stats'], prefix="/api/statistic") +app.include_router(data.router, tags=['Data'], prefix="/api/data") if __name__ == "__main__": uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True) diff --git a/backend/requirements.txt b/backend/requirements.txt index 03335bb..04f06b6 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -9,4 +9,5 @@ bcrypt==4.0.1 motor passlib[bcrypt] pydantic_settings -python-jose \ No newline at end of file +python-jose +python-multipart \ No newline at end of file diff --git a/backend/routers/auth.py b/backend/routers/auth.py index 3aacdac..a254b12 100644 --- a/backend/routers/auth.py +++ b/backend/routers/auth.py @@ -1,41 +1,18 @@ -from typing import List from fastapi import APIRouter, HTTPException, status, Response, Depends from config import settings -from dao.user import find_all_users, find_user_by_email, create_user, find_user_by_id -from schemas.user import UserCreateSchema, UserLoginSchema, UserDao, UserBaseSchema +from dao.user import UserDao +from schemas.user import UserCreateSchema, UserLoginSchema, User, UserResponse from utils.password import verify_password, create_access_token, get_password_hash +from utils.role import get_admin_role from utils.token import get_current_user router = APIRouter() -ACCESS_TOKEN_EXPIRES_IN = settings.ACCESS_TOKEN_EXPIRES_IN -ALGORITHM = settings.JWT_ALGORITHM -SECRET_KEY = settings.JWT_SECRET_KEY - - -@router.post("/register") -async def register_user(user_data: UserCreateSchema) -> dict: - user = await find_user_by_email(user_data.email.lower()) - if user: - raise HTTPException( - status_code=status.HTTP_409_CONFLICT, - detail='Пользователь уже существует' - ) - - user_data.password = get_password_hash(user_data.password) - user_id = await create_user(user_data) - return {"status": "success", "user_id": user_id} - - -@router.get("/get") -async def get_users() -> dict[str, List[UserBaseSchema]]: - users = await find_all_users() - return {"users": users} @router.post("/login") async def login_user(response: Response, user_data: UserLoginSchema) -> dict: - user = await find_user_by_email(user_data.email) + user = await UserDao.find_user_by_email(user_data.email) if not user: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, @@ -52,12 +29,14 @@ async def login_user(response: Response, user_data: UserLoginSchema) -> dict: value=access_token, secure=False, httponly=True, - samesite='lax') + samesite='lax', + expires=settings.ACCESS_TOKEN_EXPIRES_IN * 60 + ) return {'access_token': access_token, 'refresh_token': None} @router.get("/me/") -async def get_me(user_data: UserDao = Depends(get_current_user)): +async def get_me(user_data: User = Depends(get_current_user)): return user_data @@ -65,13 +44,3 @@ async def get_me(user_data: UserDao = Depends(get_current_user)): async def logout_user(response: Response): response.delete_cookie(key="users_access_token") return {'message': 'Пользователь успешно вышел из системы'} - - -@router.get("/get_user/{user_id}", response_model=UserDao) -async def get_user(user_id: str, ser: UserDao = Depends(get_current_user)): - finded_user = await find_user_by_id(user_id) - if finded_user is None: - raise HTTPException(status_code=404, detail="User not found") - - del finded_user.password - return finded_user diff --git a/backend/routers/contact.py b/backend/routers/contact.py new file mode 100644 index 0000000..d852bc9 --- /dev/null +++ b/backend/routers/contact.py @@ -0,0 +1,44 @@ +from fastapi import APIRouter, Depends, HTTPException, status + +from dao.project import ProjectDao +from dao.user import UserDao +from schemas.project import ProjectResponse +from schemas.user import User, ContactCreate, ContactResponse +from utils.role import get_foreman_role +from utils.token import get_current_user + +router = APIRouter() + + +@router.post("/{project_id}/add_contact", response_model=dict[str, ProjectResponse | None]) +async def add_contact(project_id: str, contact_data: ContactCreate, foreman: User = Depends(get_foreman_role)): + user_to_add = await UserDao.find_user_by_id(contact_data.user_id) + if not user_to_add: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail='Пользователя не существует' + ) + project = await ProjectDao.add_contact_to_project(project_id, user_to_add) + return {"updated_project": project} + + +@router.get("/{project_id}/get_contacts", response_model=dict[str, list[ContactResponse] | list[None]]) +async def get_contacts(project_id: str, user: User = Depends(get_current_user)): + contacts = await ProjectDao.get_contacts_by_project_id(project_id) + if contacts is None: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail='Проекта не существует' + ) + return {"contacts": contacts} + + +@router.delete("/{project_id}/delete_contact/{contact_id}", response_model=dict[str, ProjectResponse | None]) +async def delete_contact(project_id:str, contact_id: str, foreman: User = Depends(get_foreman_role)): + updated_project = await ProjectDao.delete_contact(project_id, contact_id) + if updated_project is None: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail='Проекта или контакта не существует' + ) + return {"updated_project": updated_project} diff --git a/backend/routers/data.py b/backend/routers/data.py new file mode 100644 index 0000000..16bf70a --- /dev/null +++ b/backend/routers/data.py @@ -0,0 +1,96 @@ +import json +from datetime import datetime + +from bson import ObjectId +from fastapi import APIRouter, HTTPException, UploadFile +from fastapi.params import Depends +from fastapi.responses import JSONResponse +from pymongo.errors import BulkWriteError + +from database import db +from schemas.user import User +from utils.role import get_admin_role + +router = APIRouter() + +def convert_to_serializable(data): + if isinstance(data, list): + return [convert_to_serializable(item) for item in data] + elif isinstance(data, dict): + return {key: convert_to_serializable(value) for key, value in data.items()} + elif isinstance(data, ObjectId): + return str(data) + elif isinstance(data, datetime): + return data.isoformat() + return data + +@router.get("/export/json") +async def export_json(admin: User = Depends(get_admin_role)): + """Экспорт всей базы данных в формате JSON.""" + collections = await db.list_collection_names() + if not collections: + raise HTTPException( + status_code=404, + detail="Коллекции не найдены, база данных пуста!" + ) + + # Сбор данных из всех коллекций + data = {} + for collection_name in collections: + collection = db[collection_name] + documents = await collection.find().to_list(length=None) + data[collection_name] = convert_to_serializable(documents) + + # Ответ в формате JSON + return JSONResponse(content=data) + + +@router.post("/import") +async def import_database(file: UploadFile): + """ + Импортирует коллекции из JSON-файла в MongoDB с проверкой существования коллекций. + + :param file: JSON-файл с данными + """ + if not file.filename.endswith(".json"): + raise HTTPException(status_code=400, detail="Only JSON files are allowed") + + try: + # Считываем содержимое файла + file_content = await file.read() + data = json.loads(file_content) + + if not isinstance(data, dict): + raise HTTPException(status_code=400, detail="JSON file must contain a dictionary of collections") + + # Получаем список существующих коллекций + existing_collections = await db.list_collection_names() + + # Проверка каждой коллекции в JSON + for collection_name in data.keys(): + if collection_name not in existing_collections: + raise HTTPException( + status_code=400, + detail=f"Collection '{collection_name}' does not exist in the database." + ) + + # Импорт данных + for collection_name, documents in data.items(): + if not isinstance(documents, list): + raise HTTPException(status_code=400, detail=f"Collection '{collection_name}' must contain a list of documents") + + # Преобразование _id в ObjectId + for doc in documents: + if "_id" in doc: + doc["_id"] = ObjectId(doc["_id"]) + + # Вставка данных в коллекцию + collection = db[collection_name] + await collection.insert_many(documents) + + except json.JSONDecodeError: + raise HTTPException(status_code=400, detail="Invalid JSON file") + except BulkWriteError as e: + raise HTTPException(status_code=400, detail="Идентичные данные уже существуют") + + return {"message": "Database imported successfully"} \ No newline at end of file diff --git a/backend/routers/message.py b/backend/routers/message.py index adf099e..d17ba20 100644 --- a/backend/routers/message.py +++ b/backend/routers/message.py @@ -1,21 +1,25 @@ -from typing import List +import asyncio +from typing import List, Dict from fastapi import APIRouter, HTTPException, status from fastapi.params import Depends +from starlette.websockets import WebSocket, WebSocketDisconnect from dao.messager import create_chat, get_chats_by_user_id, get_chat_by_id, get_chat_by_double_id, add_message_to_chat, \ create_message, get_chat_messages -from dao.user import find_user_by_id -from schemas.messager import CreateChatResponse, FirstMessage, ChatResponse, CreateMessage, Message, MessageResponse -from schemas.user import UserDao +from dao.user import UserDao +from schemas.messager import FirstMessage, ChatResponse, CreateMessage, Message, MessageResponse +from schemas.user import User from utils.token import get_current_user router = APIRouter() +active_connections: Dict[str, WebSocket] = {} + @router.post("/create_chat", response_model=ChatResponse) -async def new_chat(first_message: FirstMessage, user: UserDao = Depends(get_current_user)): - user_receiver = await find_user_by_id(first_message.id_receiver) +async def new_chat(first_message: FirstMessage, user: User = Depends(get_current_user)): + user_receiver = await UserDao.find_user_by_id(first_message.id_receiver) if user_receiver is None: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, @@ -36,13 +40,13 @@ async def new_chat(first_message: FirstMessage, user: UserDao = Depends(get_curr @router.get("/get_chats", response_model=List[ChatResponse]) -async def get_chats(user: UserDao = Depends(get_current_user)): +async def get_chats(user: User = Depends(get_current_user)): chats = await get_chats_by_user_id(user.id) return chats @router.get("/get_chat/{chat_id}", response_model=ChatResponse) -async def get_chat(chat_id: str, user: UserDao = Depends(get_current_user)): +async def get_chat(chat_id: str, user: User = Depends(get_current_user)): chat = await get_chat_by_id(chat_id, user.id) if chat is None: raise HTTPException( @@ -53,7 +57,7 @@ async def get_chat(chat_id: str, user: UserDao = Depends(get_current_user)): @router.get("/check_chat/{user_id}", response_model=ChatResponse | None) -async def get_chat(user_id: str, user: UserDao = Depends(get_current_user)): +async def get_chat(user_id: str, user: User = Depends(get_current_user)): if user_id == user.id: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, @@ -64,7 +68,7 @@ async def get_chat(user_id: str, user: UserDao = Depends(get_current_user)): @router.get("/get_messages/{chat_id}", response_model=list[Message]) -async def get_messages(chat_id: str, limit: int = 10, user: UserDao = Depends(get_current_user)): +async def get_messages(chat_id: str, limit: int = 10, user: User = Depends(get_current_user)): messages = await get_chat_messages(chat_id, user.id, limit) if messages is None: raise HTTPException( @@ -75,7 +79,7 @@ async def get_messages(chat_id: str, limit: int = 10, user: UserDao = Depends(ge @router.post("/create_message", response_model=MessageResponse) -async def new_message(message_data: CreateMessage, user: UserDao = Depends(get_current_user)): +async def new_message(message_data: CreateMessage, user: User = Depends(get_current_user)): chat_id = await add_message_to_chat(user.id, message_data) if chat_id is None: raise HTTPException( @@ -89,5 +93,26 @@ async def new_message(message_data: CreateMessage, user: UserDao = Depends(get_c status_code=status.HTTP_400_BAD_REQUEST, detail="Ошибка при добавлении сообщения" ) + + await notify_user(user.id, message_data.model_dump()) + await notify_user(message_data.receiver, message_data.model_dump()) return message + + +async def notify_user(user_id: str, message: dict): + if user_id in active_connections: + websocket = active_connections[user_id] + await websocket.send_json(message) + + +@router.websocket("/chat/{user_id}") +async def websocket_endpoint(websocket: WebSocket, user_id: str): + await websocket.accept() + active_connections[user_id] = websocket + + try: + while True: + await asyncio.sleep(1) + except WebSocketDisconnect: + active_connections.pop(user_id) diff --git a/backend/routers/procurement.py b/backend/routers/procurement.py new file mode 100644 index 0000000..acd02f9 --- /dev/null +++ b/backend/routers/procurement.py @@ -0,0 +1,76 @@ +from fastapi import APIRouter, Depends, HTTPException, status + +from dao.project import ProjectDao +from schemas.project import ProjectResponse, Procurement, ProcurementResponse, ProcurementUpdate +from schemas.user import Contact, User +from utils.role import get_foreman_role +from utils.token import get_current_user + +router = APIRouter() + + +@router.post("/{project_id}/add_procurement", response_model=dict[str, ProjectResponse | None]) +async def add_procurement(project_id: str, procurement_data: Procurement, foreman: User = Depends(get_foreman_role)): + procurement_data.created_by = Contact( + username=f'{foreman.lastname} {foreman.name} {foreman.middlename}', + role=foreman.role + ) + project = await ProjectDao.add_procurement_to_project(project_id, procurement_data) + if project is None: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail='Проекта не существует' + ) + return {"updated_project": project} + + +@router.get("/{project_id}/get_procurements", response_model=dict[str, list[ProcurementResponse] | list[None]]) +async def get_procurements(project_id: str, user: User = Depends(get_current_user)): + procurements = await ProjectDao.get_procurements_by_project_id(project_id) + if procurements is None: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail='Проекта не существует' + ) + return {"procurements": procurements} + + +@router.get("/{project_id}/get_procurement/{procurement_id}", response_model=dict[str, ProcurementResponse]) +async def get_procurement(project_id: str, procurement_id: str, user: User = Depends(get_current_user)): + procurement = await ProjectDao.get_procurement_by_id(project_id, procurement_id) + if procurement is None: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail='Закупка не найдена' + ) + return {"procurement": procurement} + + +@router.put("/{project_id}/update_procurement/{procurement_id}", response_model=dict[str, ProcurementResponse | None]) +async def update_procurement(project_id: str, procurement_id: str, procurement_data: ProcurementUpdate, + user: User = Depends(get_foreman_role)): + procurement_data.created_by = Contact( + username=f'{user.lastname} {user.name} {user.middlename}', + role=user.role + ) + + procurement = await ProjectDao.update_procurement_by_id(project_id, procurement_id, procurement_data) + if procurement is None: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail='Закупка не найдена' + ) + + return {"updated_procurement": procurement} + + +@router.delete("/{project_id}/delete_procurement/{procurement_id}", response_model=dict[str, str]) +async def remove_procurement(project_id: str, procurement_id: str, user: User = Depends(get_foreman_role)): + deleted_id = await ProjectDao.delete_procurement(project_id, procurement_id) + if deleted_id is None: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail='Закупка не найдена' + ) + + return {"deleted_id": deleted_id} diff --git a/backend/routers/project.py b/backend/routers/project.py index e09db34..64cf9c2 100644 --- a/backend/routers/project.py +++ b/backend/routers/project.py @@ -1,162 +1,76 @@ from fastapi import APIRouter, Depends, HTTPException, status -from dao.user import find_user_by_id -from schemas.project import Project, ProjectCreate, Procurement, Stage, Risk, ProcurementResponse, RiskResponse, \ - StageResponse -from schemas.user import UserDao, ContactCreate, Contact, ContactResponse +from dao.user import UserDao +from schemas.project import ProjectResponse, ProjectCreate, ProjectUpdate +from schemas.user import User, Role, UserResponse, ContactResponse, UserCreateSchema +from utils.role import get_customer_role, get_foreman_role from utils.token import get_current_user -from dao.project import get_project_by_id, create_project, get_project_by_name, get_projects_by_user, \ - add_contact_to_project, add_procurement_to_project, add_stage_to_project, add_risk_to_project, \ - get_contacts_by_project_id, get_procurements_by_project_id, get_risks_by_project_id, get_stages_by_project_id, \ - get_procurement_by_id, get_stage_by_id, get_risk_by_id +from dao.project import ProjectDao router = APIRouter() -@router.get("/one/{project_id}", response_model=dict[str, Project | None]) -async def get_project(project_id: str, user: UserDao = Depends(get_current_user)): - project = await get_project_by_id(project_id) +@router.get("/one/{project_id}", response_model=dict[str, ProjectResponse | None]) +async def get_project(project_id: str, user: User = Depends(get_current_user)): + project = await ProjectDao.get_project_by_id(project_id) return {"project": project} -@router.post("/create", response_model=dict[str, Project | None]) -async def create(project_data: ProjectCreate, user: UserDao = Depends(get_current_user)): - project = await get_project_by_name(project_data.name) - if project: - raise HTTPException( - status_code=status.HTTP_409_CONFLICT, - detail='Проект уже существует' - ) - new_project = await create_project(project_data, user) - return {"new_project": new_project} - - -@router.get("/all", response_model=list[Project]) -async def get_all(user: UserDao = Depends(get_current_user)): - return await get_projects_by_user(user.id) - - -@router.post("/{project_id}/add_contact", response_model=dict[str, Project | None]) -async def add_contact(project_id: str, contact_data: ContactCreate, user: UserDao = Depends(get_current_user)): - user_to_add = await find_user_by_id(contact_data.user_id) - if not user_to_add: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail='Пользователя не существует' - ) - project = await add_contact_to_project(project_id, user_to_add) - return {"updated_project": project} - - -@router.get("/{project_id}/get_contacts", response_model=dict[str, list[ContactResponse] | list[None]]) -async def get_contacts(project_id: str, user: UserDao = Depends(get_current_user)): - contacts = await get_contacts_by_project_id(project_id) - if contacts is None: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail='Проекта не существует' - ) - return {"contacts": contacts} - - -@router.post("/{project_id}/add_procurement", response_model=dict[str, Project | None]) -async def add_procurement(project_id: str, procurement_data: Procurement, user: UserDao = Depends(get_current_user)): - procurement_data.created_by = Contact( - username=f'{user.lastname} {user.name} {user.middlename}', - role=user.role - ) - project = await add_procurement_to_project(project_id, procurement_data) +@router.put("/update/{project_id}", response_model=dict[str, ProjectResponse | None]) +async def update_project(project_id: str, project_data: ProjectUpdate, user: User = Depends(get_current_user)): + project = await ProjectDao.update_project_by_id(project_id, project_data) if project is None: raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail='Проекта не существует' + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Проекта с id {project_id} не существует", ) - return {"updated_project": project} - - -@router.get("/{project_id}/get_procurements", response_model=dict[str, list[ProcurementResponse] | list[None]]) -async def get_procurements(project_id: str, user: UserDao = Depends(get_current_user)): - procurements = await get_procurements_by_project_id(project_id) - if procurements is None: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail='Проекта не существует' - ) - return {"procurements": procurements} + return {"project": project} -@router.get("/{project_id}/get_procurement/{procurement_id}", response_model=dict[str, ProcurementResponse]) -async def get_procurement(project_id: str, procurement_id: str, user: UserDao = Depends(get_current_user)): - procurement = await get_procurement_by_id(project_id, procurement_id) - if procurement is None: +@router.post("/create", response_model=dict[str, ProjectResponse | None]) +async def create(project_data: ProjectCreate, customer: User = Depends(get_customer_role)): + project = await ProjectDao.get_project_by_name(project_data.name) + if project: raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail='Закупка не найдена' + status_code=status.HTTP_409_CONFLICT, + detail='Проект уже существует' ) - return {"procurement": procurement} - - -@router.post("/{project_id}/add_stage", response_model=dict[str, Project | None]) -async def add_stage(project_id: str, stage_data: Stage, user: UserDao = Depends(get_current_user)): - project = await add_stage_to_project(project_id, stage_data) - if project is None: + new_project = await ProjectDao.create_project(project_data, customer) + if new_project is None: raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail='Проекта не существует' + status_code=status.HTTP_404_NOT_FOUND, + detail='Ошибка создания проекта' ) - return {"updated_project": project} + return {"new_project": new_project} -@router.get("/{project_id}/get_stages", response_model=dict[str, list[StageResponse] | list[None]]) -async def get_stages(project_id: str, user: UserDao = Depends(get_current_user)): - stages = await get_stages_by_project_id(project_id) - if stages is None: +@router.delete("/delete/{project_id}", response_model=dict[str, str]) +async def remove_project(project_id: str, user: User = Depends(get_foreman_role)): + deleted_id = await ProjectDao.delete_project(project_id) + if deleted_id is None: raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail='Проекта не существует' + status_code=status.HTTP_404_NOT_FOUND, + detail='Проект не найден' ) - return {"stages": stages} + return {"deleted_id": deleted_id} -@router.get("/{project_id}/get_stage/{stage_id}", response_model=dict[str, StageResponse]) -async def get_stage(project_id: str, stage_id: str, user: UserDao = Depends(get_current_user)): - stage = await get_stage_by_id(project_id, stage_id) - if stage is None: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail='Этап не найден' - ) - return {"stage": stage} +@router.get("/all", response_model=list[ProjectResponse]) +async def get_all(user: User = Depends(get_current_user)): + if user.role == Role.admin: + return await ProjectDao.get_all_projects() + return await ProjectDao.get_projects_by_user(user.id) -@router.post("/{project_id}/add_risk", response_model=dict[str, Project | None]) -async def add_stage(project_id: str, risk_data: Risk, user: UserDao = Depends(get_current_user)): - project = await add_risk_to_project(project_id, risk_data) - if project is None: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail='Проекта не существует' - ) - return {"updated_project": project} - +@router.get("/get_users/{project_id}", response_model=list[ContactResponse | None]) +async def get_users(project_id: str, name: str = "", lastname: str = "", middlename: str = "", + role: Role = None, user: User = Depends(get_foreman_role)): + users = await UserDao.find_users_from_project(project_id, name, lastname, middlename, role) -@router.get("/{project_id}/get_risks", response_model=dict[str, list[RiskResponse] | list[None]]) -async def get_risks(project_id: str, user: UserDao = Depends(get_current_user)): - risks = await get_risks_by_project_id(project_id) - if risks is None: + if users is None: raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, + status_code=status.HTTP_404_NOT_FOUND, detail='Проекта не существует' ) - return {"risks": risks} - -@router.get("/{project_id}/get_risk/{risk_id}", response_model=dict[str, RiskResponse]) -async def get_stage(project_id: str, risk_id: str, user: UserDao = Depends(get_current_user)): - risk = await get_risk_by_id(project_id, risk_id) - if risk is None: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail='Риск не найден' - ) - return {"risk": risk} \ No newline at end of file + return users diff --git a/backend/routers/risk.py b/backend/routers/risk.py new file mode 100644 index 0000000..db2b393 --- /dev/null +++ b/backend/routers/risk.py @@ -0,0 +1,66 @@ +from fastapi import APIRouter, HTTPException, status, Depends + +from dao.project import ProjectDao +from schemas.project import ProjectResponse, Risk, RiskResponse, RiskUpdate +from schemas.user import User +from utils.role import get_foreman_role +from utils.token import get_current_user + +router = APIRouter() + + +@router.post("/{project_id}/add_risk", response_model=dict[str, ProjectResponse | None]) +async def add_stage(project_id: str, risk_data: Risk, foreman: User = Depends(get_foreman_role)): + project = await ProjectDao.add_risk_to_project(project_id, risk_data) + if project is None: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail='Проекта не существует' + ) + return {"updated_project": project} + + +@router.get("/{project_id}/get_risks", response_model=dict[str, list[RiskResponse] | list[None]]) +async def get_risks(project_id: str, user: User = Depends(get_current_user)): + risks = await ProjectDao.get_risks_by_project_id(project_id) + if risks is None: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail='Проекта не существует' + ) + return {"risks": risks} + + +@router.get("/{project_id}/get_risk/{risk_id}", response_model=dict[str, RiskResponse]) +async def get_risk(project_id: str, risk_id: str, user: User = Depends(get_current_user)): + risk = await ProjectDao.get_risk_by_id(project_id, risk_id) + if risk is None: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail='Риск не найден' + ) + return {"risk": risk} + + +@router.put("/{project_id}/update_risk/{risk_id}", response_model=dict[str, RiskResponse | None]) +async def update_risk(project_id: str, risk_id: str, risk_data: RiskUpdate, user: User = Depends(get_foreman_role)): + risk = await ProjectDao.update_risk_by_id(project_id, risk_id, risk_data) + if risk is None: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail='Риск не найден' + ) + + return {"updated_risk": risk} + + +@router.delete("/{project_id}/delete_risk/{risk_id}", response_model=dict[str, str]) +async def remove_risk(project_id: str, risk_id: str, user: User = Depends(get_foreman_role)): + deleted_id = await ProjectDao.delete_risk(project_id, risk_id) + if deleted_id is None: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail='Риск не найден' + ) + + return {"deleted_risk": deleted_id} diff --git a/backend/routers/stage.py b/backend/routers/stage.py new file mode 100644 index 0000000..97062e5 --- /dev/null +++ b/backend/routers/stage.py @@ -0,0 +1,67 @@ +from fastapi import APIRouter, Depends, HTTPException, status + +from dao.project import ProjectDao +from schemas.project import ProjectResponse, Stage, StageResponse, StageUpdate +from schemas.user import User +from utils.role import get_foreman_role +from utils.token import get_current_user + +router = APIRouter() + + +@router.post("/{project_id}/add_stage", response_model=dict[str, ProjectResponse | None]) +async def add_stage(project_id: str, stage_data: Stage, foreman: User = Depends(get_foreman_role)): + project = await ProjectDao.add_stage_to_project(project_id, stage_data) + if project is None: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail='Проекта не существует' + ) + return {"updated_project": project} + + +@router.get("/{project_id}/get_stages", response_model=dict[str, list[StageResponse] | list[None]]) +async def get_stages(project_id: str, user: User = Depends(get_current_user)): + stages = await ProjectDao.get_stages_by_project_id(project_id) + if stages is None: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail='Проекта не существует' + ) + return {"stages": stages} + + +@router.get("/{project_id}/get_stage/{stage_id}", response_model=dict[str, StageResponse]) +async def get_stage(project_id: str, stage_id: str, user: User = Depends(get_current_user)): + stage = await ProjectDao.get_stage_by_id(project_id, stage_id) + if stage is None: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail='Этап не найден' + ) + return {"stage": stage} + + +@router.put("/{project_id}/update_stage/{stage_id}", response_model=dict[str, StageResponse | None]) +async def update_stage(project_id: str, stage_id: str, stage_data: StageUpdate, + user: User = Depends(get_foreman_role)): + stage = await ProjectDao.update_stage_by_id(project_id, stage_id, stage_data) + if stage is None: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail='Этап не найден' + ) + + return {"updated_stage": stage} + + +@router.delete("/{project_id}/delete_stage/{stage_id}", response_model=dict[str, str]) +async def remove_stage(project_id: str, stage_id: str, user: User = Depends(get_foreman_role)): + deleted_id = await ProjectDao.delete_stage(project_id, stage_id) + if deleted_id is None: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail='Этап не найден' + ) + + return {"deleted_id": deleted_id} diff --git a/backend/routers/stat.py b/backend/routers/stat.py new file mode 100644 index 0000000..139d696 --- /dev/null +++ b/backend/routers/stat.py @@ -0,0 +1,24 @@ +from bson.errors import InvalidId +from fastapi import APIRouter, Depends, HTTPException, status + +from dao.stat import StatDao +from schemas.stat import StatSchema +from utils.role import get_admin_role, get_customer_role +from utils.token import get_current_user + +router = APIRouter() + + +@router.post("/get_stat") +async def get_stat(stat_data: StatSchema, user = Depends(get_customer_role)): + result = 0 + try: + result = await StatDao.get_stats(stat_data.project_ids, stat_data.stat_type.value, + stat_data.start_date, stat_data.end_date) + except InvalidId as e: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail='Некорректные id проектов' + ) + + return result \ No newline at end of file diff --git a/backend/routers/task.py b/backend/routers/task.py index 57978c9..7eb8b38 100644 --- a/backend/routers/task.py +++ b/backend/routers/task.py @@ -1,36 +1,29 @@ -import asyncio - from fastapi import APIRouter, Depends, HTTPException, status -from dao.project import get_contacts_by_project_id -from dao.task import add_task, get_all_tasks_by_user, get_task_by_id, add_worker_to_task, get_tasks_by_stage_id -from dao.user import find_user_by_id -from schemas.task import Task, TaskResponse -from schemas.user import UserDao, Role, Worker +from dao.project import ProjectDao +from dao.task import TaskDAO +from schemas.task import Task, TaskResponse, TaskUpdate, ProjectTaskResponse, TaskStatusUpdate +from schemas.user import User, Worker +from utils.role import get_foreman_role from utils.token import get_current_user router = APIRouter() @router.post('/create/{project_id}/{stage_id}', response_model=TaskResponse) -async def create_task(project_id: str, stage_id: str, task_data: Task, user: UserDao = Depends(get_current_user)): - if user.role != Role.admin: - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, - detail='Недостаточно прав' - ) - new_task = await add_task(project_id, stage_id, task_data) +async def create_task(project_id: str, stage_id: str, task_data: Task, foreman: User = Depends(get_foreman_role)): + new_task = await TaskDAO.add_task(project_id, stage_id, task_data) if new_task is None: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail='Ошибка добавления в проект' + detail='Ошибка добавления задачи в проект' ) return new_task @router.get('/get_task/{project_id}/{stage_id}/{task_id}', response_model=TaskResponse) -async def get_task(project_id: str, stage_id: str, task_id: str, user: UserDao = Depends(get_current_user)): - task = await get_task_by_id(project_id, stage_id, task_id) +async def get_task(project_id: str, stage_id: str, task_id: str, user: User = Depends(get_current_user)): + task = await TaskDAO.get_task_by_id(project_id, stage_id, task_id) if task is None: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, @@ -39,29 +32,63 @@ async def get_task(project_id: str, stage_id: str, task_id: str, user: UserDao = return task +@router.delete('/delete/{project_id}/{stage_id}/{task_id}') +async def remove_task(project_id: str, stage_id: str, task_id: str, user: User = Depends(get_foreman_role)): + deleted_id = await TaskDAO.remove_task(project_id, stage_id, task_id) + if deleted_id is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail='Задача не найдена' + ) + return deleted_id + + +@router.put('/update_task/{project_id}/{stage_id}/{task_id}', response_model=TaskResponse) +async def update_task(project_id: str, stage_id: str, task_id: str, task_data: TaskUpdate, + user: User = Depends(get_foreman_role)): + task = await TaskDAO.update_task_by_id(project_id, stage_id, task_id, task_data) + if task is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail='Задача не найдена' + ) + return task + +@router.put('/update_status_task/{project_id}/{stage_id}/{task_id}', response_model=TaskResponse) +async def update_status_task(project_id: str, stage_id: str, task_id: str, task_data: TaskStatusUpdate, + user: User = Depends(get_current_user)): + updated_task = await TaskDAO.update_task_status(project_id, stage_id, task_id, task_data) + if updated_task is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail='Задача не найдена' + ) + return updated_task + + @router.get('/get_stage_tasks/{project_id}/{stage_id}', response_model=list[TaskResponse] | list[None]) async def get_stage_tasks(project_id: str, stage_id: str): - tasks = await get_tasks_by_stage_id(project_id, stage_id) + tasks = await TaskDAO.get_tasks_by_stage_id(project_id, stage_id) return tasks -@router.get('/get_all', response_model=list[TaskResponse] | list[None]) -async def get_all_tasks(user: UserDao = Depends(get_current_user)): - tasks = await get_all_tasks_by_user(user.id) +@router.get('/get_all', response_model=list[ProjectTaskResponse] | list[None]) +async def get_all_tasks(user: User = Depends(get_current_user)): + tasks = await TaskDAO.get_all_tasks_by_user(user.id) return tasks @router.put('/{project_id}/{stage_id}/{task_id}/{worker_id}', response_model=TaskResponse) async def add_worker(project_id: str, stage_id: str, task_id: str, worker_id: str, - user: UserDao = Depends(get_current_user)): - contacts = await get_contacts_by_project_id(project_id) + foreman: User = Depends(get_foreman_role)): + contacts = await ProjectDao.get_contacts_by_project_id(project_id) for contact in contacts: if contact.id == worker_id: worker = Worker( name=contact.username, role=contact.role ) - result = await add_worker_to_task(project_id, stage_id, task_id, worker_id, worker) + result = await TaskDAO.add_worker_to_task(project_id, stage_id, task_id, worker_id, worker) if result: return result raise HTTPException( @@ -73,3 +100,17 @@ async def add_worker(project_id: str, stage_id: str, task_id: str, worker_id: st status_code=status.HTTP_404_NOT_FOUND, detail='Работник отсутствует в контактах проекта' ) + + +@router.delete('/{project_id}/{stage_id}/{task_id}/{worker_id}', response_model=dict[str, str]) +async def delete_worker(project_id: str, stage_id: str, task_id: str, worker_id: str, + foreman: User = Depends(get_foreman_role)): + delete_id = await TaskDAO.delete_worker_from_task(project_id, stage_id, task_id, worker_id) + + if delete_id is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail='Работник не привязан к задаче' + ) + + return {"delete_id": delete_id} diff --git a/backend/routers/user.py b/backend/routers/user.py index a52cf48..72488c1 100644 --- a/backend/routers/user.py +++ b/backend/routers/user.py @@ -1,6 +1,55 @@ -from fastapi import APIRouter, Depends, HTTPException +from fastapi import APIRouter, Depends, HTTPException, status + +from dao.user import UserDao +from schemas.user import User, UserCreateSchema, UserResponse, Role +from utils.password import get_password_hash +from utils.role import get_admin_role, get_foreman_role +from utils.token import get_current_user router = APIRouter() +@router.post("/register") +async def register_user(user_data: UserCreateSchema, admin: User = Depends(get_admin_role)) -> dict: + user = await UserDao.find_user_by_email(user_data.email.lower()) + if user: + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail='Пользователь уже существует' + ) + + user_data.password = get_password_hash(user_data.password) + user_id = await UserDao.create_user(user_data) + return {"status": "success", "user_id": user_id} + + +@router.get("/get") +async def get_users() -> dict[str, list[UserResponse]]: + users = await UserDao.find_all_users() + return {"users": users} + + +@router.get("/find/", response_model=list[UserResponse | None]) +async def find_users_by_filters(name: str = "", lastname: str = "", middlename: str = "", role: Role = None, + user: User = Depends(get_current_user)) -> list[UserResponse | None]: + users = await UserDao.find_users(name, lastname, middlename, role) + return users + + +@router.get("/get_user/{user_id}", response_model=User) +async def get_user(user_id: str, ser: User = Depends(get_current_user)): + finded_user = await UserDao.find_user_by_id(user_id) + if finded_user is None: + raise HTTPException(status_code=404, detail="User not found") + + del finded_user.password + return finded_user + +@router.put("/update/{user_id}", response_model=User) +async def update_user(user_id: str, updated_data: UserCreateSchema, admin: User = Depends(get_admin_role)): + updated_user = await UserDao.update_user(user_id, updated_data) + if updated_user is None: + raise HTTPException(status_code=404, detail="User not found") + del updated_user.password + return updated_user diff --git a/backend/schemas/messager.py b/backend/schemas/messager.py index 8296522..a59bd70 100644 --- a/backend/schemas/messager.py +++ b/backend/schemas/messager.py @@ -4,6 +4,8 @@ from pydantic import BaseModel, Field +from schemas.utils import get_date_now + class FirstMessage(BaseModel): id_receiver: str = Field(...) @@ -12,7 +14,7 @@ class FirstMessage(BaseModel): class Participant(BaseModel): name: str - lastSeen: datetime = Field(datetime.now(timezone.utc)) + lastSeen: datetime = Field(get_date_now()) class StatusMsg(str, Enum): @@ -24,14 +26,14 @@ class LastMessage(BaseModel): content: str sender: str status: StatusMsg = StatusMsg.unread - timestamp: Optional[datetime] = Field(datetime.now(timezone.utc)) + timestamp: Optional[datetime] = Field(get_date_now()) class Chat(BaseModel): participants: Dict[str, Participant] = {} lastMessage: LastMessage - created_at: Optional[datetime] = Field(datetime.now(timezone.utc)) - updated_at: Optional[datetime] = Field(datetime.now(timezone.utc)) + created_at: Optional[datetime] = Field(get_date_now()) + updated_at: Optional[datetime] = Field(get_date_now()) class Config: from_attributes = True diff --git a/backend/schemas/project.py b/backend/schemas/project.py index 21bf1ab..d30c4df 100644 --- a/backend/schemas/project.py +++ b/backend/schemas/project.py @@ -1,5 +1,4 @@ -import uuid -from datetime import datetime, timezone +from datetime import datetime from enum import Enum from typing import Dict, Optional @@ -7,7 +6,7 @@ from schemas.task import Task from schemas.user import Contact -from schemas.utils import generate_id +from schemas.utils import generate_id, get_date_now class Stage(BaseModel): @@ -15,8 +14,8 @@ class Stage(BaseModel): start_date: datetime end_date: datetime tasks: Dict[str, Task] = {} - created_at: Optional[datetime] = Field(datetime.now(timezone.utc)) - updated_at: Optional[datetime] = Field(datetime.now(timezone.utc)) + created_at: Optional[datetime] = Field(get_date_now()) + updated_at: Optional[datetime] = Field(get_date_now()) def add_task(self, task: Task): self.tasks[generate_id()] = task @@ -29,11 +28,17 @@ class StageResponse(Stage): id: str +class StageUpdate(BaseModel): + name: str + start_date: datetime + end_date: datetime + + class Risk(BaseModel): name: str description: str - created_at: Optional[datetime] = Field(datetime.now(timezone.utc)) - updated_at: Optional[datetime] = Field(datetime.now(timezone.utc)) + created_at: Optional[datetime] = Field(get_date_now()) + updated_at: Optional[datetime] = Field(get_date_now()) class Config: from_attributes = True @@ -43,6 +48,11 @@ class RiskResponse(Risk): id: str +class RiskUpdate(BaseModel): + name: str + description: str + + class Procurement(BaseModel): item_name: str quantity: int @@ -51,8 +61,8 @@ class Procurement(BaseModel): units: Optional[str] = Field(None) delivery_date: Optional[datetime] = Field(None) created_by: Optional[Contact] = Field(None) - created_at: Optional[datetime] = Field(datetime.now(timezone.utc)) - updated_at: Optional[datetime] = Field(datetime.now(timezone.utc)) + created_at: Optional[datetime] = Field(get_date_now()) + updated_at: Optional[datetime] = Field(get_date_now()) class ProcurementResponse(Procurement): @@ -65,10 +75,21 @@ def calculate_cost(self): return self +class ProcurementUpdate(BaseModel): + item_name: str + quantity: int + price: float + inStock: Optional[bool] = Field(False) + units: Optional[str] = Field(None) + delivery_date: Optional[datetime] = Field(None) + created_by: Optional[Contact] = Field(None) + + class ProjectStatus(str, Enum): in_progress = "В процессе" done = "Готово" lateness = "Опоздание" + new_status = "Новый" none_status = "Нет статуса" @@ -77,9 +98,9 @@ class ProjectCreate(BaseModel): description: Optional[str] = Field(None) start_date: Optional[datetime] = Field(None) end_date: Optional[datetime] = Field(None) - status: ProjectStatus = Field(ProjectStatus.none_status) - created_at: Optional[datetime] = Field(datetime.now(timezone.utc)) - updated_at: Optional[datetime] = Field(datetime.now(timezone.utc)) + status: ProjectStatus = Field(ProjectStatus.new_status) + created_at: Optional[datetime] = Field(get_date_now()) + updated_at: Optional[datetime] = Field(get_date_now()) contacts: Dict[str, Contact] = {} stages: Dict[str, Stage] = {} procurements: Dict[str, Procurement] = {} @@ -101,5 +122,12 @@ class Config: from_attributes = True -class Project(ProjectCreate): +class ProjectResponse(ProjectCreate): id: str + + +class ProjectUpdate(BaseModel): + description: Optional[str] = Field(None) + status: ProjectStatus + start_date: Optional[datetime] = Field(None) + end_date: Optional[datetime] = Field(None) diff --git a/backend/schemas/stat.py b/backend/schemas/stat.py new file mode 100644 index 0000000..897040c --- /dev/null +++ b/backend/schemas/stat.py @@ -0,0 +1,17 @@ +from datetime import datetime +from enum import Enum +from typing import Optional, List + +from pydantic import BaseModel, Field + + +class StatType(str, Enum): + risks = "risks" + procurements = "procurements" + + +class StatSchema(BaseModel): + project_ids: List[str] = Field(..., min_length=1) + stat_type: StatType + start_date: Optional[datetime] = None + end_date: Optional[datetime] = None diff --git a/backend/schemas/task.py b/backend/schemas/task.py index 59857a2..6b2d234 100644 --- a/backend/schemas/task.py +++ b/backend/schemas/task.py @@ -1,11 +1,11 @@ -from datetime import datetime, timezone +from datetime import datetime from enum import Enum from typing import Dict, Optional from pydantic import BaseModel, Field -from schemas.user import Worker -from schemas.utils import generate_id +from schemas.user import Worker, WorkerResponse, WorkerCreate +from schemas.utils import generate_id, get_date_now class TaskStatus(str, Enum): @@ -22,15 +22,27 @@ class Task(BaseModel): start_date: Optional[datetime] = Field(None) end_date: Optional[datetime] = Field(None) workers: Dict[str, Worker] = {} - created_at: Optional[datetime] = Field(datetime.now(timezone.utc)) - updated_at: Optional[datetime] = Field(datetime.now(timezone.utc)) - - def add_worker(self, worker: Worker): - self.workers[generate_id()] = worker - class Config: - from_attributes = True + created_at: Optional[datetime] = Field(get_date_now()) + updated_at: Optional[datetime] = Field(get_date_now()) class TaskResponse(Task): id: str + +class ProjectTaskResponse(TaskResponse): + id: str + project_id: str + project_name: str + stage_id: str + + +class TaskUpdate(BaseModel): + name: str + description: str + status: TaskStatus = TaskStatus.none_status + start_date: Optional[datetime] = Field(None) + end_date: Optional[datetime] = Field(None) + +class TaskStatusUpdate(BaseModel): + status: TaskStatus diff --git a/backend/schemas/user.py b/backend/schemas/user.py index 2dc944d..84b0db7 100644 --- a/backend/schemas/user.py +++ b/backend/schemas/user.py @@ -1,8 +1,10 @@ -from datetime import datetime, timezone +from datetime import datetime from enum import Enum from typing import Optional from pydantic import BaseModel, EmailStr, Field +from schemas.utils import get_date_now + class Role(str, Enum): admin = "Администратор" @@ -23,10 +25,10 @@ class Config: from_attributes = True -class UserBaseSchema(UserCreateSchema): +class UserResponse(UserCreateSchema): id: str - created_at: datetime | None = datetime.now(timezone.utc) - updated_at: datetime | None = datetime.now(timezone.utc) + created_at: Optional[datetime] = Field(get_date_now()) + updated_at: Optional[datetime] = Field(get_date_now()) class UserLoginSchema(BaseModel): @@ -34,15 +36,15 @@ class UserLoginSchema(BaseModel): password: str = Field(min_length=8) -class UserDao(UserBaseSchema): +class User(UserResponse): password: str = Field(min_length=8) class Contact(BaseModel): username: str role: Role = Role.worker - created_at: Optional[datetime] = Field(datetime.now(timezone.utc)) - updated_at: Optional[datetime] = Field(datetime.now(timezone.utc)) + created_at: Optional[datetime] = Field(get_date_now()) + updated_at: Optional[datetime] = Field(get_date_now()) class ContactResponse(Contact): @@ -60,3 +62,9 @@ class Worker(BaseModel): class WorkerResponse(Worker): id: str + + +class WorkerCreate(BaseModel): + user_id: str + role: Role = Role.worker + username: str diff --git a/backend/schemas/utils.py b/backend/schemas/utils.py index 1dff8af..2379b31 100644 --- a/backend/schemas/utils.py +++ b/backend/schemas/utils.py @@ -1,11 +1,18 @@ import uuid +from datetime import datetime, timezone def generate_id(): return str(uuid.uuid4()) -def object_id_to_str(data) -> dict[str, str]: +def object_id_to_str(data) -> dict[str, str] | None: + if data is None: + return None data['id'] = str(data['_id']) del data['_id'] - return data \ No newline at end of file + return data + + +def get_date_now() -> datetime: + return datetime.now(timezone.utc) diff --git a/backend/utils/password.py b/backend/utils/password.py index f35eea8..bdc967a 100644 --- a/backend/utils/password.py +++ b/backend/utils/password.py @@ -1,8 +1,9 @@ -from datetime import datetime, timezone, timedelta +from datetime import timedelta from jose import jwt from passlib.context import CryptContext from config import settings +from schemas.utils import get_date_now pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") @@ -15,7 +16,7 @@ def verify_password(plain_password: str, hashed_password: str) -> bool: def create_access_token(data: dict) -> str: to_encode = data.copy() - expire = datetime.now(timezone.utc) + timedelta(days=30) + expire = get_date_now() + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRES_IN) to_encode.update({"exp": expire}) encode_jwt = jwt.encode(to_encode, settings.JWT_SECRET_KEY, algorithm=settings.JWT_ALGORITHM) return encode_jwt \ No newline at end of file diff --git a/backend/utils/role.py b/backend/utils/role.py new file mode 100644 index 0000000..d9fd37d --- /dev/null +++ b/backend/utils/role.py @@ -0,0 +1,31 @@ +from fastapi import Depends, HTTPException, status + +from schemas.user import User, Role +from utils.token import get_current_user + + +def get_admin_role(user: User = Depends(get_current_user)): + if user.role != Role.admin: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail='Нет прав администратора' + ) + return user + + +def get_foreman_role(user: User = Depends(get_current_user)): + if user.role != Role.foreman and user.role != Role.admin: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail='Нет прав прораба' + ) + return user + + +def get_customer_role(user: User = Depends(get_current_user)): + if user.role != Role.customer and user.role != Role.admin: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail='Нет прав заказчика' + ) + return user diff --git a/backend/utils/token.py b/backend/utils/token.py index ca9eb16..a79cf77 100644 --- a/backend/utils/token.py +++ b/backend/utils/token.py @@ -1,10 +1,10 @@ -from bson import ObjectId from fastapi import Request, HTTPException, status, Depends from jose import jwt, JWTError from datetime import datetime, timezone from config import settings -from dao.user import find_user_by_id +from dao.user import UserDao +from schemas.utils import get_date_now def get_token(request: Request): @@ -22,16 +22,16 @@ async def get_current_user(token: str = Depends(get_token)): expire: str = payload.get('exp') expire_time = datetime.fromtimestamp(int(expire), tz=timezone.utc) - if (not expire) or (expire_time < datetime.now(timezone.utc)): + if (not expire) or (expire_time < get_date_now()): raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail='Токен истек') user_id = payload.get('sub') if not user_id: raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail='Не найден ID пользователя') - user = await find_user_by_id(str(user_id)) + user = await UserDao.find_user_by_id(str(user_id)) if not user: raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail='User not found') del user.password - return user \ No newline at end of file + return user diff --git a/docker-compose.yaml b/docker-compose.yaml index f7e981f..fa3e73c 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -6,8 +6,6 @@ services: - MONGO_INITDB_ROOT_USERNAME=admin - MONGO_INITDB_ROOT_PASSWORD=password123 - MONGO_INITDB_DATABASE=fastapi - ports: - - "27017:27017" volumes: - mongo:/data/db networks: @@ -48,4 +46,4 @@ networks: app-network: driver: bridge - \ No newline at end of file + diff --git a/frontend/package-lock.json b/frontend/package-lock.json index db047c7..522fb9d 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -9,6 +9,7 @@ "version": "0.1.0", "dependencies": { "axios": "^1.7.7", + "chart.js": "^4.4.7", "core-js": "^3.8.3", "js-cookie": "^3.0.5", "mitt": "^3.0.1", @@ -1836,6 +1837,12 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", + "license": "MIT" + }, "node_modules/@leichtgewicht/ip-codec": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", @@ -3744,6 +3751,18 @@ "node": ">=8" } }, + "node_modules/chart.js": { + "version": "4.4.7", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.7.tgz", + "integrity": "sha512-pwkcKfdzTMAU/+jNosKhNL2bHtJc/sSmYgVbuGTEDhzkrhmyihmP7vUc/5ZK9WopidMDHNe3Wm7jOd/WhuHWuw==", + "license": "MIT", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index b36c138..287f7b0 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,6 +9,7 @@ }, "dependencies": { "axios": "^1.7.7", + "chart.js": "^4.4.7", "core-js": "^3.8.3", "js-cookie": "^3.0.5", "mitt": "^3.0.1", diff --git a/frontend/src/assets/icons/delete_person.png b/frontend/src/assets/icons/delete_person.png new file mode 100644 index 0000000..2923c7f Binary files /dev/null and b/frontend/src/assets/icons/delete_person.png differ diff --git a/frontend/src/assets/icons/home.png b/frontend/src/assets/icons/home.png new file mode 100644 index 0000000..8f9bac5 Binary files /dev/null and b/frontend/src/assets/icons/home.png differ diff --git a/frontend/src/assets/icons/user.png b/frontend/src/assets/icons/user.png new file mode 100644 index 0000000..97c898c Binary files /dev/null and b/frontend/src/assets/icons/user.png differ diff --git a/frontend/src/main.js b/frontend/src/main.js index 3248020..44f593a 100644 --- a/frontend/src/main.js +++ b/frontend/src/main.js @@ -2,6 +2,7 @@ import { createApp } from 'vue'; import App from './App.vue'; import router from './src/router'; import axios from 'axios'; +import store from './src/store'; axios.defaults.withCredentials = true; axios.defaults.baseURL = 'http://localhost:8000/'; // the FastAPI backend @@ -14,4 +15,5 @@ axios.defaults.headers = { const app = createApp(App); // Создание экземпляра приложения app.use(router); // Использование маршрутизатора +app.use(store); app.mount('#app'); // Монтирование приложения в DOM \ No newline at end of file diff --git a/frontend/src/src/components/ContentComponent.vue b/frontend/src/src/components/ContentComponent.vue new file mode 100644 index 0000000..f8de6b9 --- /dev/null +++ b/frontend/src/src/components/ContentComponent.vue @@ -0,0 +1,131 @@ + + + + + diff --git a/frontend/src/src/components/MainPage.vue b/frontend/src/src/components/MainPage.vue index ede8716..05cd39c 100644 --- a/frontend/src/src/components/MainPage.vue +++ b/frontend/src/src/components/MainPage.vue @@ -1,131 +1,41 @@ + \ No newline at end of file diff --git a/frontend/src/src/components/auth_reg/AuthForm.vue b/frontend/src/src/components/auth_reg/AuthForm.vue index 7adc755..8558b54 100644 --- a/frontend/src/src/components/auth_reg/AuthForm.vue +++ b/frontend/src/src/components/auth_reg/AuthForm.vue @@ -177,7 +177,17 @@ export default { console.log(res); setUserName(res.data.lastname + ' ' + res.data.name + ' ' + res.data.middlename); setUserId(res.data.id) + const newUserData = { name: res.data.lastname + ' ' + res.data.name + ' ' + res.data.middlename, role: res.data.role }; + this.$store.commit('addSingleUser', newUserData); // Изменяем состояние + console.log(newUserData); this.$router.push("/main"); + /*if(newUserData.role === "Рабочий") { + this.$router.push('/worker'); + } + else{ + this.$router.push("/main"); + }*/ + }) .catch(err => { console.log(err); @@ -208,7 +218,7 @@ export default { }; try { - const res = await axios.post('/api/auth/register', dataToSend, { + const res = await axios.post('/api/user/register', dataToSend, { headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', diff --git a/frontend/src/src/components/bars/ProjectSidebarComponent.vue b/frontend/src/src/components/bars/ProjectSidebarComponent.vue deleted file mode 100644 index f005a82..0000000 --- a/frontend/src/src/components/bars/ProjectSidebarComponent.vue +++ /dev/null @@ -1,128 +0,0 @@ - - - - - diff --git a/frontend/src/src/components/bars/SidebarComponent.vue b/frontend/src/src/components/bars/SidebarComponent.vue index 4340f24..a74c5f6 100644 --- a/frontend/src/src/components/bars/SidebarComponent.vue +++ b/frontend/src/src/components/bars/SidebarComponent.vue @@ -1,76 +1,187 @@ - - - - + diff --git a/frontend/src/src/components/bars/StaticSidebarComponent.vue b/frontend/src/src/components/bars/StaticSidebarComponent.vue deleted file mode 100644 index 2049776..0000000 --- a/frontend/src/src/components/bars/StaticSidebarComponent.vue +++ /dev/null @@ -1,101 +0,0 @@ - - - - - diff --git a/frontend/src/src/components/main_content/TaskCard.vue b/frontend/src/src/components/main_content/TaskCard.vue new file mode 100644 index 0000000..5e66506 --- /dev/null +++ b/frontend/src/src/components/main_content/TaskCard.vue @@ -0,0 +1,219 @@ + + + + \ No newline at end of file diff --git a/frontend/src/src/components/main_content/WorkerContentComponent.vue b/frontend/src/src/components/main_content/WorkerContentComponent.vue new file mode 100644 index 0000000..fa584f0 --- /dev/null +++ b/frontend/src/src/components/main_content/WorkerContentComponent.vue @@ -0,0 +1,208 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/src/components/material/MaterialDetail.vue b/frontend/src/src/components/material/MaterialDetail.vue deleted file mode 100644 index 3c42e53..0000000 --- a/frontend/src/src/components/material/MaterialDetail.vue +++ /dev/null @@ -1,98 +0,0 @@ - - - - - \ No newline at end of file diff --git a/frontend/src/src/components/material/MaterialEditAdd.vue b/frontend/src/src/components/material/MaterialEditAdd.vue index e022576..3ef7ee7 100644 --- a/frontend/src/src/components/material/MaterialEditAdd.vue +++ b/frontend/src/src/components/material/MaterialEditAdd.vue @@ -1,144 +1,193 @@ - - - - \ No newline at end of file + goToProcurements() { + this.$router.push(`/procurements`); + } + }, + beforeMount() { + this.checkPath(); + }, +}; + + + diff --git a/frontend/src/src/components/material/ProcurementsItemComponent.vue b/frontend/src/src/components/material/ProcurementsItemComponent.vue index 841215e..a0592a0 100644 --- a/frontend/src/src/components/material/ProcurementsItemComponent.vue +++ b/frontend/src/src/components/material/ProcurementsItemComponent.vue @@ -45,23 +45,50 @@
- - - + + +
@@ -127,15 +154,16 @@ export default { padding: 5px 10px; border: none; cursor: pointer; + border-radius: 10px; } .details-button { - background-color: #007bff; + background-color: #625b71; color: white; } .delete-button { - background-color: #dc3545; + background-color: #625b71; color: white; } diff --git a/frontend/src/src/components/material/ProcurementsPage.vue b/frontend/src/src/components/material/ProcurementsPage.vue index 7eca4eb..2d51e48 100644 --- a/frontend/src/src/components/material/ProcurementsPage.vue +++ b/frontend/src/src/components/material/ProcurementsPage.vue @@ -1,7 +1,7 @@