Compare commits

...

No commits in common. "master" and "release" have entirely different histories.

111 changed files with 9440 additions and 163 deletions

162
.gitignore vendored
View File

@ -1,162 +0,0 @@
# ---> Python
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
.pdm.toml
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/

3
.idea/.gitignore generated vendored Normal file
View File

@ -0,0 +1,3 @@
# Default ignored files
/shelf/
/workspace.xml

14
.idea/Internet_auto_parts_store.iml generated Normal file
View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/venv" />
</content>
<orderEntry type="jdk" jdkName="Python 3.9 (Internet_auto_parts_store)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="PyDocumentationSettings">
<option name="format" value="PLAIN" />
<option name="myDocStringFormat" value="Plain" />
</component>
</module>

View File

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

10
.idea/misc.xml generated Normal file
View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Black">
<option name="sdkName" value="Python 3.9 (Internet_auto_parts_store)" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.9 (Internet_auto_parts_store)" project-jdk-type="Python SDK" />
<component name="PyCharmProfessionalAdvertiser">
<option name="shown" value="true" />
</component>
</project>

8
.idea/modules.xml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/Internet_auto_parts_store.iml" filepath="$PROJECT_DIR$/.idea/Internet_auto_parts_store.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

View File

@ -1,2 +1,28 @@
# Internet_auto_parts_store
После скачивания проекта из сайта git.is.ulstu.ru
Нужно установить пакеты из файла requirements.txt с помощью команды pip
Нужно еще скачать контейнеры elasticsearch и kibana с помощью команды docker-compose up
После этого нужно установить пакеты node js, c помощью команды npm install
Для запуска проекта:
1. Запустить виртуальную оболочку venv, затем нужно запустить файл main.py;
2. Перейти в каталог front_admin и запустить команду npm run start;
3. Перейти в каталог front и запустить предыдущую команду.
Описание
Вес проект состоит из трех частей:
1. Работа с хранилищем: для взаимодействия с сервером используется
пакет Flask, для работы с базой данных SQL Alchemy.
2. Бизнес-логика: реализована во сервисе, и взаимодействует с контроллерами.
3. Уровень пользовательского интерфейса: выполнен с помощью vue js.
В диаграмме классов показаны подробно соответствующие классы и компоненты.
В пакеты сервис, репозиторий и контроллеры, реализованы классы, которые используют функции add, update, delete, get и get_all.
Функция add используется для добавления нового объекта, функция update используется для изменения данных определенного объекта по id, функция delete удаляет объект из базы по id.
Функция get предоставляет данные определенного объекта по id.
Функция get_all предоставляет информацию в качестве списка, определенной сущности из базы данных.
В классе User из пакета сервис дополнительно еще реализованы функции get_hash и create_token.
Функция get_hash используется для того чтобы зашифровать пароль пользователя, а также используется при аутентификация пользователя.
Функция create_token создает token, которая нужна для того чтобы безопасно передавать данные из базы данных пользователю.
Еще в этой программе дополнительно реализовано функции для вывода данных такие как get_all_parts, get_all_category_id, get_part_manufacturer и get_parts.
Функции add_order_items и search это еще дополнительные функции, которые были созданы в этой программе.
Функция add_order_items нужно для добавления данных, т.е. целиком добавление всего списка запчастей из корзины в базу.
Функция search это основная функция, которая нужно для семантического поиска.
Семантический поиск реализован с помощью функций, которые в свою очередь используются библиотеку elasticsearch.

26
application/config.py Normal file
View File

@ -0,0 +1,26 @@
from pydantic_settings import BaseSettings, SettingsConfigDict
from elasticsearch import Elasticsearch
class Settings(BaseSettings):
MODE: str
DB_HOST: str
DB_PORT: int
DB_USER: str
DB_PASS: str
DB_NAME: str
@property
def DATABASE_URL(self):
return f"postgresql+psycopg://{self.DB_USER}:{self.DB_PASS}@{self.DB_HOST}:{self.DB_PORT}/{self.DB_NAME}"
model_config = SettingsConfigDict(env_file=".env")
class Config:
SECRET_KEY = 'Ford Mustang ShowRoom'
settings = Settings()
es = Elasticsearch(['http://localhost:9200'])

View File

@ -0,0 +1,83 @@
from flask import request
from flask_restx import Resource, fields, abort, marshal_with
from flask_jwt_extended import jwt_required, get_jwt_identity
from json import loads, JSONDecodeError
from sqlalchemy.exc import SQLAlchemyError
from application.controllers.ReviewController import api, check_data
from application.services.CartService import CartService
ns = api.namespace('cart', description='Cart operations')
part_fields_without_feature = ns.model("PartWithoutFeature", {
'id': fields.Integer(readonly=True, description='The part unique identifier'),
'name': fields.String(required=True, description='Name of the part'),
'description': fields.String(required=True, description='Description of the part'),
'price': fields.Float(required=True, description='Price of the part'),
'category_id': fields.Integer(required=True, description='Category ID associated with this part'),
'manufacturer_id': fields.Integer(required=True, description='Manufacturer ID associated with this part')
})
cart_fields = ns.model("Cart", {
'id': fields.Integer(readonly=True, description='The cart unique identifier'),
'count': fields.Integer(required=True, description='Quantity of the item in the cart'),
'part': fields.Nested(part_fields_without_feature, allow_null=True)
})
@ns.route('/')
class CartList(Resource):
@marshal_with(cart_fields)
@jwt_required()
def get(self):
try:
cart = CartService.get_all(int(get_jwt_identity()))
return cart, 200
except Exception as e:
abort(500, message=f"Internal Server Error: {str(e)}")
@ns.doc(params={
'id': {'description': 'The cart unique identifier', 'required': True, 'type': 'integer'},
'count': {'description': 'Quantity of the item', 'required': True, 'type': 'integer'},
'feature': {'description': 'Cart parts in JSON format', 'required': True, 'type': 'string'},
})
@marshal_with(cart_fields)
@jwt_required()
def post(self):
id = request.args.get('id', default=0, type=int)
count = request.args.get('count', default=0, type=int)
part_str = request.args.get('feature', default='', type=str)
if not check_data([id, count]):
abort(400, message="Incorrect input data.")
part = None
if part_str:
try:
part = loads(part_str)
except JSONDecodeError:
abort(400, message="Invalid JSON format for part.")
try:
cart = CartService.add(int(get_jwt_identity()), {"id": id, "count": count,
"part": part["part"]})
return cart, 201
except SQLAlchemyError as e:
abort(500, message=f"Database error: {str(e)}")
@ns.route('/<int:id>')
@ns.response(404, 'Cart not found')
@ns.param('id', 'The part identifier')
class Cart(Resource):
@ns.response(200, "Part deleted")
@marshal_with(cart_fields)
@jwt_required()
def delete(self, id):
try:
cart = CartService.get(id, int(get_jwt_identity()))
if cart is None:
abort(404, message=f"Part with ID {id} does not exist.")
cart = CartService.delete(id, int(get_jwt_identity()))
return cart, 200
except SQLAlchemyError as e:
abort(500, message=f"Database error: {str(e)}")

View File

@ -0,0 +1,127 @@
import json
from flask import request
from flask_restx import Resource, fields, abort, marshal_with
from flask_jwt_extended import jwt_required
from sqlalchemy.exc import SQLAlchemyError
from application.controllers.UserController import api, check_data
from application.dto.CategoryDTO import CategoryDTO
from application.services.CategoryService import CategoryService
ns = api.namespace('categories', description='Category operations')
category_fields = ns.model("Category", {
'id': fields.Integer(readonly=True, description='The category unique identifier'),
'name': fields.String(required=True, description='Category name'),
'description': fields.String(required=True, description='Category description'),
'feature': fields.Raw(description='Category features in JSON format'),
'parent_category_id': fields.Integer(description='Parent category ID')
})
@ns.route('/')
class CategoryList(Resource):
@marshal_with(category_fields)
def get(self):
try:
categories = CategoryService.get_all()
return categories, 200
except Exception as e:
abort(500, message=f"Internal Server Error: {str(e)}")
@ns.doc(params={
'name': {'description': 'Category name', 'required': True, 'type': 'string'},
'description': {'description': 'Category description', 'required': True, 'type': 'string'},
'feature': {'description': 'Category features in JSON format', 'required': False, 'type': 'string'},
'parent_category_id': {'description': 'Parent category ID', 'required': False, 'type': 'integer'}
})
@marshal_with(category_fields)
@jwt_required()
def post(self):
name = request.args.get('name', default='', type=str)
description = request.args.get('description', default='', type=str)
feature_str = request.args.get('feature', default='', type=str)
parent_category_id = request.args.get('parent_category_id', default=0, type=int)
if not check_data([name, description]):
abort(400, message="Incorrect input data.")
feature = None
if feature_str:
try:
feature = json.loads(feature_str)
except json.JSONDecodeError:
abort(400, message="Invalid JSON format for feature.")
try:
if parent_category_id == 0:
parent_category_id = None
category_dto = CategoryDTO(name=name,
description=description,
feature=feature,
parent_category_id=parent_category_id)
new_category = CategoryService.add(category_dto)
return new_category, 201
except SQLAlchemyError as e:
abort(500, message=f"Database error: {str(e)}")
@ns.route('/<int:id>')
@ns.response(404, 'Category not found')
@ns.param('id', 'The category identifier')
class Category(Resource):
@marshal_with(category_fields)
def get(self, id):
try:
category = CategoryService.get(id)
if category is None:
abort(404, message=f"Category with ID {id} does not exist.")
return category, 200
except Exception as e:
abort(500, message=f"Internal Server Error: {str(e)}")
@ns.expect(category_fields)
@marshal_with(category_fields)
@jwt_required()
def put(self, id):
name = ns.payload['name']
description = ns.payload['description']
parent_category_id = ns.payload['parent_category_id']
feature_str = ns.payload.get('feature', '')
feature = None
if feature_str:
try:
feature = json.loads(feature_str) if isinstance(feature_str, str) else feature_str
except json.JSONDecodeError:
abort(400, message="Invalid JSON format for feature.")
if not check_data([name, description, feature]):
abort(400, message="Incorrect input data.")
try:
category = CategoryService.get(id)
if category is None:
abort(404, message=f"Category with ID {id} does not exist.")
if parent_category_id == 0:
parent_category_id = None
category_dto = CategoryDTO(name=name, description=description, feature=feature,
parent_category_id=parent_category_id)
updated_category = CategoryService.update(id, category_dto)
return updated_category, 200
except SQLAlchemyError as e:
abort(500, message=f"Database error: {str(e)}")
@ns.response(200, "Category deleted")
@marshal_with(category_fields)
@jwt_required()
def delete(self, id):
try:
category = CategoryService.get(id)
if category is None:
abort(404, message=f"Category with ID {id} does not exist.")
CategoryService.delete(id)
return category, 200
except SQLAlchemyError as e:
abort(500, message=f"Database error: {str(e)}")

View File

@ -0,0 +1,95 @@
from flask import request
from flask_restx import Resource, fields, abort, marshal_with
from flask_jwt_extended import jwt_required
from sqlalchemy.exc import SQLAlchemyError
from application.controllers.CategoryController import api, check_data
from application.dto.ManufacturerDTO import ManufacturerDTO
from application.services.ManufacturerService import ManufacturerService
ns = api.namespace('manufacturers', description='Manufacturer operations')
manufacturer_fields = ns.model("Manufacturer", {
'id': fields.Integer(readonly=True, description='The manufacturer unique identifier'),
'name': fields.String(required=True, description='Name of the manufacturer'),
'country': fields.String(required=True, description='Country of the manufacturer')
})
@ns.route('/')
class ManufacturerList(Resource):
@marshal_with(manufacturer_fields)
def get(self):
try:
manufacturers = ManufacturerService.get_all()
return manufacturers, 200
except Exception as e:
abort(500, message=f"Internal Server Error: {str(e)}")
@ns.doc(params={
'name': {'description': 'Name of the manufacturer', 'required': True, 'type': 'string'},
'country': {'description': 'Country of the manufacturer', 'required': True, 'type': 'string'}
})
@marshal_with(manufacturer_fields)
@jwt_required()
def post(self):
name = request.args.get('name', default='', type=str)
country = request.args.get('country', default='', type=str)
if not check_data([name, country]):
abort(400, message="Incorrect input data.")
try:
manufacturer_dto = ManufacturerDTO(name=name, country=country)
new_manufacturer = ManufacturerService.add(manufacturer_dto)
return new_manufacturer, 201
except SQLAlchemyError as e:
abort(500, message=f"Database error: {str(e)}")
@ns.route('/<int:id>')
@ns.response(404, 'Manufacturer not found')
@ns.param('id', 'The manufacturer identifier')
class Manufacturer(Resource):
@marshal_with(manufacturer_fields)
def get(self, id):
try:
manufacturer = ManufacturerService.get(id)
if manufacturer is None:
abort(404, message=f"Manufacturer with ID {id} does not exist.")
return manufacturer, 200
except Exception as e:
abort(500, message=f"Internal Server Error: {str(e)}")
@ns.expect(manufacturer_fields)
@marshal_with(manufacturer_fields)
@jwt_required()
def put(self, id):
name = ns.payload['name']
country = ns.payload['country']
if not check_data([name, country]):
abort(400, message="Incorrect input data.")
try:
manufacturer = ManufacturerService.get(id)
if manufacturer is None:
abort(404, message=f"Manufacturer with ID {id} does not exist.")
manufacturer_dto = ManufacturerDTO(name=name, country=country)
updated_manufacturer = ManufacturerService.update(id, manufacturer_dto)
return updated_manufacturer, 200
except SQLAlchemyError as e:
abort(500, message=f"Database error: {str(e)}")
@ns.response(200, "Manufacturer deleted")
@marshal_with(manufacturer_fields)
@jwt_required()
def delete(self, id):
try:
manufacturer = ManufacturerService.get(id)
if manufacturer is None:
abort(404, message=f"Manufacturer with ID {id} does not exist.")
ManufacturerService.delete(id)
return manufacturer, 200
except SQLAlchemyError as e:
abort(500, message=f"Database error: {str(e)}")

View File

@ -0,0 +1,149 @@
import json
from datetime import date
from flask import request
from flask_restx import Resource, fields, abort, marshal_with
from flask_jwt_extended import jwt_required, get_jwt_identity
from sqlalchemy.exc import SQLAlchemyError
from application.controllers.ManufacturerController import api, check_data
from application.dto.OrderDTO import OrderDTO
from application.services.OrderService import OrderService
ns = api.namespace('orders', description='Order operations')
order_fields = ns.model("Order", {
'id': fields.Integer(readonly=True, description='The order unique identifier'),
'order_date': fields.Date(required=True, description='Date of the order'),
'total_amount': fields.Float(required=True, description='Total amount of the order'),
'status': fields.String(required=True, description='Status of the order'),
'user_id': fields.Integer(required=True, description='User ID associated with this order')
})
@ns.route('/')
class OrderList(Resource):
@marshal_with(order_fields)
@jwt_required()
def get(self):
try:
orders = OrderService.get_all(int(get_jwt_identity()))
return orders, 200
except Exception as e:
abort(500, message=f"Internal Server Error: {str(e)}")
@ns.doc(params={
'order_date': {'description': 'Date of the order', 'required': True, 'type': 'string', 'format': 'date'},
'total_amount': {'description': 'Total amount of the order', 'required': True, 'type': 'number'},
'status': {'description': 'Status of the order', 'required': True, 'type': 'string'}
})
@marshal_with(order_fields)
@jwt_required()
def post(self):
order_date_str = request.args.get('order_date', default='', type=str)
total_amount = request.args.get('total_amount', default=0.0, type=float)
status = request.args.get('status', default='', type=str)
user_id = get_jwt_identity()
order_date = date.today()
try:
order_date = date.fromisoformat(order_date_str)
except ValueError:
abort(400, message="Invalid date format. Use YYYY-MM-DD.")
if not check_data([order_date_str, total_amount, status, user_id]):
abort(400, message="Incorrect input data.")
try:
order_dto = OrderDTO(order_date=order_date,
total_amount=total_amount,
status=int(status),
user_id=user_id)
new_order = OrderService.add(order_dto)
return new_order, 201
except SQLAlchemyError as e:
abort(500, message=f"Database error: {str(e)}")
@ns.route('/<int:id>')
@ns.response(404, 'Order not found')
@ns.param('id', 'The order identifier')
class Order(Resource):
@marshal_with(order_fields)
@jwt_required()
def get(self, id):
try:
order = OrderService.get(id)
if order is None:
abort(404, message=f"Order with ID {id} does not exist.")
return order, 200
except Exception as e:
abort(500, message=f"Internal Server Error: {str(e)}")
@ns.response(200, "Order deleted")
@marshal_with(order_fields)
@jwt_required()
def delete(self, id):
try:
order = OrderService.get(id)
if order is None:
abort(404, message=f"Order with ID {id} does not exist.")
OrderService.delete(id)
return order, 200
except SQLAlchemyError as e:
abort(500, message=f"Database error: {str(e)}")
@ns.route('/orderwithitems')
class OrderWithItems(Resource):
@ns.doc(params={
'order_date': {'description': 'Date of the order', 'required': True, 'type': 'string', 'format': 'date'},
'total_amount': {'description': 'Total amount of the order', 'required': True, 'type': 'number'},
'status': {'description': 'Status of the order', 'required': True, 'type': 'string'},
'items': {
'description': 'List of order items',
'required': True,
'type': 'array',
'items': {
'type': 'object',
'properties': {
'count': {'description': 'Quantity of the item', 'required': True, 'type': 'integer'},
'price': {'description': 'Price of the item', 'required': True, 'type': 'number'},
'part_id': {'description': 'ID of the associated part', 'required': True, 'type': 'integer'}
}
}
}
})
@marshal_with(order_fields)
@jwt_required()
def post(self):
order_date_str = request.args.get('order_date', default='', type=str)
total_amount = request.args.get('total_amount', default=0.0, type=float)
status = request.args.get('status', default='', type=str)
items_json = request.args.get('items', '[]')
user_id = get_jwt_identity()
order_date = date.today()
try:
order_date = date.fromisoformat(order_date_str)
except ValueError:
abort(400, message="Invalid date format. Use YYYY-MM-DD.")
items = None
if items_json:
try:
items = json.loads(items_json)
except json.JSONDecodeError:
abort(400, message="Invalid JSON format for items.")
if not check_data([order_date_str, total_amount, status, user_id]):
abort(400, message="Incorrect input data.")
try:
order_dto = OrderDTO(order_date=order_date,
total_amount=total_amount,
status=int(status),
user_id=user_id)
new_order = OrderService.add_order_items(order_dto, items)
print(new_order)
return new_order, 201
except SQLAlchemyError as e:
abort(500, message=f"Database error: {str(e)}")

View File

@ -0,0 +1,116 @@
from flask import request
from flask_restx import Resource, fields, abort, marshal_with
from flask_jwt_extended import jwt_required
from sqlalchemy.exc import SQLAlchemyError
from application.controllers.OrderController import api, check_data
from application.dto.OrderItemDTO import OrderItemDTO
from application.services.OrderItemService import OrderItemService
ns = api.namespace('order_items', description='Order Item operations')
order_item_fields = ns.model("OrderItem", {
'id': fields.Integer(readonly=True, description='The order item unique identifier'),
'count': fields.Integer(required=True, description='Quantity of the item'),
'price': fields.Float(required=True, description='Price of the item'),
'part_id': fields.Integer(required=True, description='ID of the associated part'),
'order_id': fields.Integer(required=True, description='ID of the associated order')
})
order_item_parts_fields = ns.model("OrderItemList", {
'id': fields.Integer(readonly=True, description='The order item unique identifier'),
'count': fields.Integer(required=True, description='Quantity of the item'),
'price': fields.Float(required=True, description='Price of the item'),
'order_id': fields.Integer(required=True, description='ID of the associated order'),
'name_part': fields.String(required=True, description='The name of part')
})
@ns.route('/')
class OrderItemCreate(Resource):
@ns.doc(params={
'count': {'description': 'Quantity of the item', 'required': True, 'type': 'integer'},
'price': {'description': 'Price of the item', 'required': True, 'type': 'number'},
'part_id': {'description': 'ID of the associated part', 'required': True, 'type': 'integer'},
'order_id': {'description': 'ID of the associated order', 'required': True, 'type': 'integer'}
})
@marshal_with(order_item_fields)
@jwt_required()
def post(self):
count = request.args.get('count', default=0, type=int)
price = request.args.get('price', default=0.0, type=float)
part_id = request.args.get('part_id', default=0, type=int)
order_id = request.args.get('order_id', default=0, type=int)
if not check_data([count, price, part_id, order_id]):
abort(400, message="Incorrect input data.")
try:
order_item_dto = OrderItemDTO(count=count,
price=price,
part_id=part_id,
order_id=order_id)
new_order_item = OrderItemService.add(order_item_dto)
return new_order_item, 201
except SQLAlchemyError as e:
abort(500, message=f"Database error: {str(e)}")
@ns.route('/<int:id>')
@ns.response(404, 'Order item not found')
@ns.param('id', 'The order item identifier')
class OrderItem(Resource):
@marshal_with(order_item_fields)
@jwt_required()
def get(self, id):
try:
order_item = OrderItemService.get(id)
if order_item is None:
abort(404, message=f"Order item with ID {id} does not exist.")
return order_item, 200
except Exception as e:
abort(500, message=f"Internal Server Error: {str(e)}")
@ns.response(200, "Order item deleted")
@marshal_with(order_item_fields)
@jwt_required()
def delete(self, id):
try:
order_item = OrderItemService.get(id)
if order_item is None:
abort(404, message=f"Order item with ID {id} does not exist.")
OrderItemService.delete(id)
return order_item, 200
except SQLAlchemyError as e:
abort(500, message=f"Database error: {str(e)}")
@ns.route('/get_all_order_items/<int:order_id>')
@ns.response(404, 'Order items not found')
@ns.param('order_id', 'The order item identifier')
class OrderItemList(Resource):
@marshal_with(order_item_fields)
@jwt_required()
def get(self, order_id):
try:
if order_id <= 0:
abort(404, "Invalid order id")
order_items = OrderItemService.get_all(order_id)
return order_items, 200
except Exception as e:
abort(500, message=f"Internal Server Error: {str(e)}")
@ns.route('/get_all_order_items_parts/<int:order_id>')
@ns.response(404, 'Order items not found')
@ns.param('order_id', 'The order item identifier')
class OrderItemListParts(Resource):
@marshal_with(order_item_parts_fields)
@jwt_required()
def get(self, order_id):
try:
if order_id <= 0:
abort(404, "Invalid order id")
order_items = OrderItemService.get_all_parts(order_id)
return order_items, 200
except Exception as e:
abort(500, message=f"Internal Server Error: {str(e)}")

View File

@ -0,0 +1,190 @@
import json
from flask import request
from flask_restx import Resource, fields, abort, marshal_with
from flask_jwt_extended import jwt_required
from sqlalchemy.exc import SQLAlchemyError
from application.controllers.OrderItemController import api, check_data
from application.controllers.ManufacturerController import manufacturer_fields
from application.dto.PartDTO import PartDTO
from application.services.PartService import PartService, PartIndexManager
ns = api.namespace('parts', description='Part operations')
part_fields = ns.model("Part", {
'id': fields.Integer(readonly=True, description='The part unique identifier'),
'name': fields.String(required=True, description='Name of the part'),
'description': fields.String(required=True, description='Description of the part'),
'feature': fields.Raw(description='Additional features of the part'),
'price': fields.Float(required=True, description='Price of the part'),
'category_id': fields.Integer(required=True, description='Category ID associated with this part'),
'manufacturer_id': fields.Integer(required=True, description='Manufacturer ID associated with this part')
})
part_manufacturer_fields = ns.model("PartManufacturer", {
'id': fields.Integer(readonly=True, description='The part unique identifier'),
'name': fields.String(required=True, description='Name of the part'),
'description': fields.String(required=True, description='Description of the part'),
'feature': fields.Raw(description='Additional features of the part'),
'price': fields.Float(required=True, description='Price of the part'),
'category_id': fields.Integer(required=True, description='Category ID associated with this part'),
'manufacturer': fields.Nested(manufacturer_fields)
})
@ns.route('/get_all_parts/<int:category_id>')
@ns.response(404, 'Parts not found')
@ns.param('category_id', 'The category identifier')
class PartCategory(Resource):
@marshal_with(part_fields)
def get(self, category_id):
try:
if category_id <= 0:
abort(404, "Invalid category id")
parts = PartService.get_all_category_id(category_id)
return parts, 200
except Exception as e:
abort(500, message=f"Internal Server Error: {str(e)}")
@ns.route('/')
class PartList(Resource):
@marshal_with(part_fields)
def get(self):
try:
parts = PartService.get_all()
return parts, 200
except Exception as e:
abort(500, message=f"Internal Server Error: {str(e)}")
@ns.doc(params={
'name': {'description': 'Name of the part', 'required': True, 'type': 'string'},
'description': {'description': 'Description of the part', 'required': True, 'type': 'string'},
'feature': {'description': 'Category features in JSON format', 'required': False, 'type': 'string'},
'price': {'description': 'Price of the part', 'required': True, 'type': 'number'},
'category_id': {'description': 'Category ID associated with this part', 'required': True, 'type': 'integer'},
'manufacturer_id': {'description': 'Manufacturer ID associated with this part', 'required': True, 'type': 'integer'}
})
@marshal_with(part_fields)
@jwt_required()
def post(self):
name = request.args.get('name', default='', type=str)
description = request.args.get('description', default='', type=str)
feature_str = request.args.get('feature', default='', type=str)
price = request.args.get('price', default=0.0, type=float)
category_id = request.args.get('category_id', default=0, type=int)
manufacturer_id = request.args.get('manufacturer_id', default=0, type=int)
if not check_data([name, description, price, category_id, manufacturer_id]):
abort(400, message="Incorrect input data.")
feature = None
if feature_str:
try:
feature = json.loads(feature_str)
except json.JSONDecodeError:
abort(400, message="Invalid JSON format for feature.")
try:
part_dto = PartDTO(name=name,
description=description,
feature=feature,
price=price,
category_id=category_id,
manufacturer_id=manufacturer_id)
new_part = PartService.add(part_dto)
return new_part, 201
except SQLAlchemyError as e:
abort(500, message=f"Database error: {str(e)}")
@ns.route('/<int:id>')
@ns.response(404, 'Part not found')
@ns.param('id', 'The part identifier')
class Part(Resource):
@marshal_with(part_fields)
def get(self, id):
try:
part = PartService.get(id)
if part is None:
abort(404, message=f"Part with ID {id} does not exist.")
return part, 200
except Exception as e:
abort(500, message=f"Internal Server Error: {str(e)}")
@ns.expect(part_fields)
@marshal_with(part_fields)
@jwt_required()
def put(self, id):
name = ns.payload['name']
description = ns.payload['description']
feature_str = ns.payload.get('feature', '')
price = ns.payload['price']
category_id = ns.payload['category_id']
manufacturer_id = ns.payload['manufacturer_id']
feature = None
if feature_str:
try:
feature = json.loads(feature_str) if isinstance(feature_str, str) else feature_str
except json.JSONDecodeError:
abort(400, message="Invalid JSON format for feature.")
if not check_data([name, description, feature, price, category_id, manufacturer_id]):
abort(400, message="Incorrect input data.")
try:
part = PartService.get(id)
if part is None:
abort(404, message=f"Part with ID {id} does not exist.")
part_dto = PartDTO(name=name, description=description, feature=feature,
price=price, category_id=category_id, manufacturer_id=manufacturer_id)
updated_part = PartService.update(id, part_dto)
return updated_part, 200
except SQLAlchemyError as e:
abort(500, message=f"Database error: {str(e)}")
@ns.response(200, "Part deleted")
@marshal_with(part_fields)
@jwt_required()
def delete(self, id):
try:
part = PartService.get(id)
if part is None:
abort(404, message=f"Part with ID {id} does not exist.")
PartService.delete(id)
return part, 200
except SQLAlchemyError as e:
abort(500, message=f"Database error: {str(e)}")
@ns.route('/get_part_manufacturer/<int:part_id>')
@ns.response(404, 'Part not found')
@ns.param('part_id', 'The part identifier')
class PartManufacturer(Resource):
@marshal_with(part_manufacturer_fields)
def get(self, part_id):
try:
part = PartService.get_part_manufacturer(part_id)
if part is None:
abort(404, message=f"Part with ID {id} does not exist.")
return part, 200
except Exception as e:
abort(500, message=f"Internal Server Error: {str(e)}")
@ns.route('/get_part_review')
class PartReview(Resource):
@ns.doc(params={
'search_query': {'description': 'Field for the searching information in the system', 'required': True,
'type': 'string'}
})
@marshal_with(part_fields)
def get(self):
try:
search_query = request.args.get('search_query', default='', type=str)
result_search = PartIndexManager.search(search_query)
parts = PartIndexManager.get_parts(result_search)
return parts, 200
except Exception as e:
abort(500, message=f"Internal Server Error: {str(e)}")

View File

@ -0,0 +1,104 @@
from datetime import date
from flask import request
from flask_restx import Resource, fields, abort, marshal_with
from flask_jwt_extended import jwt_required, get_jwt_identity
from sqlalchemy.exc import SQLAlchemyError
from application.controllers.PartController import api, check_data
from application.dto.ReviewDTO import ReviewDTO
from application.services.ReviewService import ReviewService
ns = api.namespace('reviews', description='Review operations')
review_fields = ns.model("Review", {
'id': fields.Integer(readonly=True, description='The review unique identifier'),
'rating': fields.Integer(required=True, description='Rating given in the review'),
'comment': fields.String(required=True, description='Comment in the review'),
'created_at': fields.Date(required=True, description='Date when the review was created'),
'updated_at': fields.Date(required=True, description='Date when the review was last updated'),
'user_id': fields.Integer(required=True, description='User ID associated with this review'),
'part_id': fields.Integer(required=True, description='Part ID associated with this review')
})
review_user_fields = ns.model("ReviewUser", {
'id': fields.Integer(readonly=True, description='The review unique identifier'),
'rating': fields.Integer(required=True, description='Rating given in the review'),
'comment': fields.String(required=True, description='Comment in the review'),
'email': fields.String(required=True, description='The email of the user'),
'user_name': fields.String(required=True, description='The first name last name patronymic of the user'),
'part_id': fields.Integer(required=True, description='Part ID associated with this review')
})
@ns.route('/get_all_reviews/<int:part_id>')
@ns.response(404, 'Reviews not found')
@ns.param('part_id', 'The part identifier')
class ReviewList(Resource):
@marshal_with(review_user_fields)
def get(self, part_id):
try:
if part_id <= 0:
abort(404, "Invalid part id")
engines = ReviewService.get_all(part_id)
return engines, 200
except Exception as e:
abort(500, message=f"Internal Server Error: {str(e)}")
@ns.route('/')
class ReviewCreate(Resource):
@ns.doc(params={
'rating': {'description': 'Rating given in the review', 'required': True, 'type': 'integer'},
'comment': {'description': 'Comment in the review', 'required': True, 'type': 'string'},
'part_id': {'description': 'Part ID associated with this review', 'required': True, 'type': 'integer'}
})
@marshal_with(review_fields)
@jwt_required()
def post(self):
rating = request.args.get('rating', default=0, type=int)
comment = request.args.get('comment', default='', type=str)
user_id = get_jwt_identity()
part_id = request.args.get('part_id', default=0, type=int)
if not check_data([rating, comment, user_id, part_id]):
abort(400, message="Incorrect input data.")
try:
current_date = date.today()
review_dto = ReviewDTO(rating=rating,
comment=comment,
created_at=current_date,
updated_at=current_date,
user_id=user_id,
part_id=part_id)
new_review = ReviewService.add(review_dto)
return new_review, 201
except SQLAlchemyError as e:
abort(500, message=f"Database error: {str(e)}")
@ns.route('/<int:id>')
@ns.response(404, 'Review not found')
@ns.param('id', 'The review identifier')
class Review(Resource):
@marshal_with(review_fields)
def get(self, id):
try:
review = ReviewService.get(id)
if review is None:
abort(404, message=f"Review with ID {id} does not exist.")
return review, 200
except Exception as e:
abort(500, message=f"Internal Server Error: {str(e)}")
@ns.response(200, "Review deleted")
@marshal_with(review_fields)
@jwt_required()
def delete(self, id):
try:
review = ReviewService.get(id)
if review is None:
abort(404, message=f"Review with ID {id} does not exist.")
ReviewService.delete(id)
return review, 200
except SQLAlchemyError as e:
abort(500, message=f"Database error: {str(e)}")

View File

@ -0,0 +1,178 @@
from flask import Flask, request, make_response
from flask_cors import CORS
from flask_restx import Api, Resource, fields, abort, marshal_with
from flask_jwt_extended import JWTManager, get_jwt_identity, jwt_required
from sqlalchemy.exc import SQLAlchemyError
from application.dto.UserDTO import UserDTO
from application.services.UserService import UserService
from application.config import Config
app = Flask(__name__)
app.config['JWT_SECRET_KEY'] = Config.SECRET_KEY
jwt = JWTManager(app)
CORS(app, resources={r"/*": {"origins": "*", "allow_headers": ["Content-Type", "Authorization"],
"expose_headers": ["Authorization"]}})
authorizations = {
'Bearer Auth': {
'type': 'apiKey',
'in': 'header',
'name': 'Authorization'
},
}
api = Api(app, version='1.0', title='Internet auto parts store API',
description='The Internet auto parts store API', doc='/swagger', specs_url='/api/swagger.json',
authorizations=authorizations, security='Bearer Auth')
ns = api.namespace('users', description='User operations')
user_fields = api.model("User", {
'id': fields.Integer(readonly=True, description='The user unique identifier'),
'email': fields.String(required=True, description='The email of the user'),
'name': fields.String(required=True, description='The first name last name patronymic of the user'),
'phone': fields.String(required=True, description='The phone of the user'),
'password_hash': fields.String(required=True, description='The password of the user'),
'address': fields.String(required=True, description='The address of the user'),
'role': fields.Integer(required=True, description='The role of the user')
})
user_token = api.model("UserToken", {
'id': fields.Integer(readonly=True, description='The user unique identifier'),
'email': fields.String(required=True, description='The email of the user'),
'name': fields.String(required=True, description='The first name last name patronymic of the user'),
'role': fields.String(required=True, description='The role of the user'),
'token': fields.String(readonly=True, description='The token of the user')
})
def check_data(values):
check = True
i = 0
while (i < len(values)) and ((type(values[i]) == str and values[i] != '') or
(type(values[i]) == int and values[i] != 0) or
(type(values[i]) == float and values[i] != 0.0)):
i += 1
if i > len(values):
check = False
return check
@app.before_request
def handle_preflight():
if request.method == "OPTIONS":
response = make_response()
response.headers.add("Access-Control-Allow-Origin", "*")
response.headers.add("Access-Control-Allow-Headers", "Content-Type,Authorization")
response.headers.add("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS")
return response
@ns.route('/')
class UserList(Resource):
@marshal_with(user_fields)
@jwt_required()
def get(self):
try:
user = UserService.get_user_id(int(get_jwt_identity()))
if user["role"].value == 1:
abort(403, message="This method is forbidding for this user.")
if user["id"] == 1:
return UserService.get_all_users()
return UserService.get_all_users_role()
except Exception as e:
abort(500, message=f"Internal Server Error: {str(e)}")
@ns.doc(params={
'email': {'description': 'The email of the user', 'required': True, 'type': 'string'},
'name': {'description': 'The first name last name patronymic of the user', 'required': True, 'type': 'string'},
'phone': {'description': 'The address of the user', 'required': True, 'type': 'string'},
'password_hash': {'description': 'The password of the user', 'required': True, 'type': 'string'},
'address': {'description': 'The address of the user', 'required': True, 'type': 'string'},
'role': {'description': 'The role of the user', 'required': True, 'type': 'integer'}
})
@marshal_with(user_token)
def post(self):
email = request.args.get('email', default='', type=str)
name = request.args.get('name', default='', type=str)
phone = request.args.get('phone', default='', type=str)
password_hash = request.args.get('password_hash', default='', type=str)
address = request.args.get('address', default='', type=str)
role = request.args.get('role', default='', type=int)
if not check_data([email, name, phone, password_hash, address, role]):
abort(400, message="Incorrect input data.")
try:
user = UserDTO(email=email, name=name, phone=phone, password_hash=password_hash,
address=address, role=role)
new_user = UserService.add_user(user)
return new_user, 201
except SQLAlchemyError as e:
abort(500, message=f"Database error: {str(e)}")
@ns.route('/<int:id>')
@ns.response(404, 'User not found')
@ns.param('id', 'The user identifier')
class User(Resource):
@marshal_with(user_fields)
@jwt_required()
def get(self, id):
user = UserService.get_user_id(int(get_jwt_identity()))
if user["role"].value == 1:
abort(403, message="This method is forbidding for this user.")
if user is None:
abort(404, message=f"Model with ID {id} does not exist.")
try:
user = UserService.get_user_id(id, True)
return user, 200
except Exception as e:
abort(500, message=f"Internal Server Error: {str(e)}")
@ns.expect(user_fields)
@marshal_with(user_fields)
@jwt_required()
def put(self, id):
email = api.payload['email']
name = api.payload['name']
phone = api.payload['phone']
password_hash = api.payload['password_hash']
address = api.payload['address']
role = api.payload['role']
if not check_data([email, name, password_hash, address, role]):
abort(400, message="Incorrect input data.")
try:
user = UserService.get_user_id(int(get_jwt_identity()))
if user["role"].value == "user":
abort(403, message="This method is forbidding for this user.")
user = UserService.get_user_id(id)
if user is None:
abort(404, message=f"Model with ID {id} does not exist.")
user = UserDTO(email=email, name=name, phone=phone, password_hash=password_hash,
address=address, role=role)
updated_user = UserService.update_user(id, user)
return updated_user, 201
except SQLAlchemyError as e:
abort(500, message=f"Database error: {str(e)}")
@ns.route('/getuser')
class UserAuthentication(Resource):
@ns.doc(params={
'email': {'description': 'User email', 'required': True, 'type': 'string'},
'password': {'description': 'User password', 'required': True, 'type': 'string'}
})
@marshal_with(user_token)
def get(self):
email = request.args.get('email', default='', type=str)
password = request.args.get('password', default='', type=str)
if not check_data([email, password]):
abort(400, message="Incorrect input data.")
try:
user = UserService.get_user(email, password)
if user is None:
abort(404, message="User not found!")
return user, 200
except Exception as e:
abort(500, message=f"Internal Server Error: {str(e)}")

15
application/database.py Normal file
View File

@ -0,0 +1,15 @@
from sqlalchemy import create_engine
from sqlalchemy.orm import DeclarativeBase, sessionmaker
from config import settings
engine = create_engine(
url=settings.DATABASE_URL,
echo=True
)
SessionFactory = sessionmaker(engine)
class Base(DeclarativeBase):
pass

View File

@ -0,0 +1,21 @@
from pydantic import BaseModel, Field
from typing import Optional
class CategoryDTO(BaseModel):
id: Optional[int] = Field(default=None)
name: str = Field(default="Default Category Name")
description: str = Field(default="Default Description")
feature: dict = Field(default={"part_1": "part"})
parent_category_id: Optional[int] = Field(default=None)
def __eq__(self, other):
if not isinstance(other, type(self)):
return False
for attr in ["name", "description", "feature", "parent_category_id"]:
if getattr(self, attr) != getattr(other, attr):
return False
return True
def to_dictionary(self) -> dict:
return self.model_dump(exclude={"id"})

View File

@ -0,0 +1,19 @@
from pydantic import BaseModel, Field
from typing import Optional
class ManufacturerDTO(BaseModel):
id: Optional[int] = Field(default=None)
name: str = Field(default="Default Manufacturer Name")
country: str = Field(default="USA")
def __eq__(self, other):
if not isinstance(other, type(self)):
return False
for attr in ["name", "country"]:
if getattr(self, attr) != getattr(other, attr):
return False
return True
def to_dictionary(self) -> dict:
return self.model_dump(exclude={"id"})

View File

@ -0,0 +1,27 @@
from pydantic import BaseModel, Field
from datetime import date
from typing import Optional
from application.models.Orders import OrderStatus
class OrderDTO(BaseModel):
id: Optional[int] = Field(default=None)
order_date: date = Field(default_factory=date.today)
total_amount: float = Field(default=0.0)
status: OrderStatus = Field(default=OrderStatus.in_processing)
user_id: int = Field(default=1)
def __eq__(self, other):
if not isinstance(other, type(self)):
return False
for attr in ["order_date", "total_amount", "status", "user_id"]:
if getattr(self, attr) != getattr(other, attr):
return False
return True
def to_dictionary(self) -> dict:
return self.model_dump(exclude={"id"})
class OrderDTOInt(OrderDTO):
status: int = Field(default=0)

View File

@ -0,0 +1,21 @@
from pydantic import BaseModel, Field
from typing import Optional
class OrderItemDTO(BaseModel):
id: Optional[int] = Field(default=None)
count: int = Field(default=1)
price: float = Field(default=0.0)
part_id: int = Field(default=1)
order_id: int = Field(default=1)
def __eq__(self, other):
if not isinstance(other, type(self)):
return False
for attr in ["count", "price", "part_id", "order_id"]:
if getattr(self, attr) != getattr(other, attr):
return False
return True
def to_dictionary(self) -> dict:
return self.model_dump(exclude={"id"})

View File

@ -0,0 +1,38 @@
from pydantic import BaseModel, Field
from typing import Optional
class PartDTO(BaseModel):
id: Optional[int] = Field(default=None)
name: str = Field(default="Default Part Name")
description: str = Field(default="Default Description")
feature: dict = Field(default={"part_1": "part"})
price: float = Field(default=0.0)
category_id: int = Field(default=1)
manufacturer_id: int = Field(default=1)
def __eq__(self, other):
if not isinstance(other, type(self)):
return False
for attr in ["name", "description", "feature", "price", "category_id", "manufacturer_id"]:
if getattr(self, attr) != getattr(other, attr):
return False
return True
def to_dictionary(self) -> dict:
return self.model_dump(exclude={"id"})
parts_mapping = {
"mappings": {
"properties": {
"id": {"type": "integer"},
"name": {"type": "keyword"},
"description": {"type": "text"},
"feature": {"type": "object"},
"price": {"type": "float"},
"category_id": {"type": "integer"},
"manufacturer_id": {"type": "integer"}
}
}
}

View File

@ -0,0 +1,39 @@
from pydantic import BaseModel, Field
from datetime import date
from typing import Optional
class ReviewDTO(BaseModel):
id: Optional[int] = Field(default=None)
rating: int = Field(default=5)
comment: str = Field(default="Default Comment")
created_at: date = Field(default_factory=date.today)
updated_at: date = Field(default_factory=date.today)
user_id: int = Field(default=1)
part_id: int = Field(default=1)
def __eq__(self, other):
if not isinstance(other, type(self)):
return False
for attr in ["rating", "comment", "created_at", "updated_at", "user_id", "part_id"]:
if getattr(self, attr) != getattr(other, attr):
return False
return True
def to_dictionary(self) -> dict:
return self.model_dump(exclude={"id"})
reviews_mapping = {
"mappings": {
"properties": {
"id": {"type": "integer"},
"rating": {"type": "integer"},
"comment": {"type": "text"},
"created_at": {"type": "date"},
"updated_at": {"type": "date"},
"user_id": {"type": "integer"},
"part_id": {"type": "integer"}
}
}
}

View File

@ -0,0 +1,37 @@
from pydantic import BaseModel, Field
from typing import Optional
from application.models.Users import RoleUser
class UserDTO(BaseModel):
id: Optional[int] = Field(default=None)
email: str = Field(default="default@email.com")
name: str = Field(default="Default User Name")
phone: str = Field(default="+1234567890")
password_hash: str = Field(default="123")
address: str = Field(default="Default Address")
role: RoleUser = Field(default=RoleUser.user)
cart: dict = Field(default={})
def __eq__(self, other):
if not isinstance(other, type(self)):
return False
for attr in ["email", "name", "phone", "password_hash", "address", "role", "cart"]:
if getattr(self, attr) != getattr(other, attr):
return False
return True
def to_dictionary(self) -> dict:
return self.model_dump(exclude={"id"})
class UserDTOInt(UserDTO):
role: int = Field(default=1)
class UserDTOToken(BaseModel):
id: Optional[int] = Field(default=1)
email: str = Field(default="user@mail.ru")
name: str = Field(default="User User")
role: int = Field(default=1)
token: str = Field(default="Token")

55
application/main.py Normal file
View File

@ -0,0 +1,55 @@
import sys
from pathlib import Path
from elasticsearch.exceptions import NotFoundError
sys.path.append(str(Path(__file__).parent.parent))
from application.config import es
from application.database import Base, engine
from application.dto.UserDTO import UserDTO
from application.services.UserService import UserService
from application.controllers.CartController import api
from application.dto.PartDTO import parts_mapping
from application.dto.ReviewDTO import reviews_mapping
def create_tables():
engine.echo = False
Base.metadata.drop_all(engine)
Base.metadata.create_all(engine)
user = UserDTO(email="admin@gmail.com", name="admin", password_hash="admin", phone="+992(501)226245",
address="everywhere", role=0, cart={})
add_user = UserService.add_default_user(user)
engine.echo = True
print(f"{add_user=}")
def create_indices():
if not es.indices.exists(index='parts'):
es.indices.create(index='parts', body=parts_mapping)
print("Индекс parts успешно создан")
if not es.indices.exists(index='reviews'):
es.indices.create(index='reviews', body=reviews_mapping)
print("Индекс reviews успешно создан")
def delete_indices():
indices = es.indices.get_alias().keys()
for index in indices:
try:
if (index == "parts") or (index == "reviews"):
es.indices.delete(index=index)
print(f"Индекс {index} успешно удален")
except NotFoundError:
print(f"Индекс {index} не найден")
except Exception as e:
print(f"Ошибка при удалении индекса {index}: {str(e)}")
if __name__ == '__main__':
delete_indices()
create_tables()
create_indices()
api.app.run(port=5001)

View File

@ -0,0 +1,18 @@
from sqlalchemy import String, Text, ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.dialects.postgresql import JSONB
from application.database import Base
class Categories(Base):
__tablename__: str = "categories"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(40), nullable=False)
description: Mapped[str] = mapped_column(Text, nullable=False)
feature: Mapped[dict] = mapped_column(JSONB, nullable=True)
parent_category_id: Mapped[int] = mapped_column(ForeignKey("categories.id", ondelete="CASCADE"), nullable=True)
parts: Mapped[list["Parts"]] = relationship(
back_populates="category"
)

View File

@ -0,0 +1,15 @@
from sqlalchemy import String
from sqlalchemy.orm import Mapped, mapped_column, relationship
from application.database import Base
class Manufacturers(Base):
__tablename__: str = "manufacturers"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(40), nullable=False)
country: Mapped[str] = mapped_column(String(40), nullable=False)
parts: Mapped[list["Parts"]] = relationship(
back_populates="manufacturer"
)

View File

@ -0,0 +1,28 @@
from sqlalchemy import ForeignKey, CheckConstraint, Index, Integer
from sqlalchemy.orm import Mapped, mapped_column, relationship
from application.database import Base
class OrderItems(Base):
__tablename__: str = "order_items"
id: Mapped[int] = mapped_column(primary_key=True)
count: Mapped[int] = mapped_column(Integer, nullable=False)
price: Mapped[float] = mapped_column(nullable=False)
part_id: Mapped[int] = mapped_column(ForeignKey("parts.id", ondelete="CASCADE"))
order_id: Mapped[int] = mapped_column(ForeignKey("orders.id", ondelete="CASCADE"))
part: Mapped["Parts"] = relationship(
back_populates="order_items"
)
order: Mapped["Orders"] = relationship(
back_populates="order_items"
)
__table_args__ = (
Index("order_items_price_index", "price"),
CheckConstraint("price > 0", name="check_price"),
Index("count_index", "count"),
CheckConstraint("count > 0", name="check_count")
)

View File

@ -0,0 +1,35 @@
from datetime import date
from enum import Enum
from sqlalchemy import ForeignKey, Date, CheckConstraint, Index
from sqlalchemy.orm import Mapped, mapped_column, relationship
from application.database import Base
class OrderStatus(Enum):
in_processing = 0
shipped = 1
delivered = 2
class Orders(Base):
__tablename__ = "orders"
id: Mapped[int] = mapped_column(primary_key=True)
order_date: Mapped[date] = mapped_column(Date, nullable=False)
total_amount: Mapped[float] = mapped_column(nullable=False)
status: Mapped[OrderStatus] = mapped_column(nullable=False)
user_id: Mapped[int] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"))
order_items: Mapped[list["OrderItems"]] = relationship(
back_populates="order"
)
user: Mapped["Users"] = relationship(
back_populates="orders"
)
__table_args__ = (
CheckConstraint("total_amount > 0", name="check_total_amount"),
Index("order_date_index", "order_date"),
Index("status_index", "status"),
)

View File

@ -0,0 +1,37 @@
from sqlalchemy import String, Text, ForeignKey, Index, CheckConstraint
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.dialects.postgresql import JSONB
from application.database import Base
class Parts(Base):
__tablename__: str = "parts"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(40), nullable=False)
description: Mapped[str] = mapped_column(Text, nullable=False)
feature: Mapped[dict] = mapped_column(JSONB, nullable=True)
price: Mapped[float] = mapped_column(nullable=False)
category_id: Mapped[int] = mapped_column(ForeignKey("categories.id", ondelete="CASCADE"))
manufacturer_id: Mapped[int] = mapped_column(ForeignKey("manufacturers.id", ondelete="CASCADE"))
order_items: Mapped[list["OrderItems"]] = relationship(
back_populates="part"
)
reviews: Mapped[list["Reviews"]] = relationship(
back_populates="part"
)
category: Mapped["Categories"] = relationship(
back_populates="parts"
)
manufacturer: Mapped["Manufacturers"] = relationship(
back_populates="parts"
)
__table_args__ = (
Index("parts_price_index", "price"),
CheckConstraint("price > 0", name="check_price")
)

View File

@ -0,0 +1,29 @@
from datetime import date
from sqlalchemy import ForeignKey, CheckConstraint, Index, Integer, Text, Date
from sqlalchemy.orm import Mapped, mapped_column, relationship
from application.database import Base
class Reviews(Base):
__tablename__: str = "reviews"
id: Mapped[int] = mapped_column(primary_key=True)
rating: Mapped[int] = mapped_column(Integer, nullable=False)
comment: Mapped[str] = mapped_column(Text, nullable=False)
created_at: Mapped[date] = mapped_column(Date, nullable=False)
updated_at: Mapped[date] = mapped_column(Date, nullable=False)
user_id: Mapped[int] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"))
part_id: Mapped[int] = mapped_column(ForeignKey("parts.id", ondelete="CASCADE"))
part: Mapped["Parts"] = relationship(
back_populates="reviews"
)
user: Mapped["Users"] = relationship(
back_populates="reviews"
)
__table_args__ = (
Index("rating_index", "rating"),
CheckConstraint("rating > 0", name="check_rating")
)

View File

@ -0,0 +1,31 @@
from enum import Enum
from sqlalchemy import String
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.dialects.postgresql import JSONB
from application.database import Base
class RoleUser(Enum):
admin = 0
user = 1
class Users(Base):
__tablename__: str = "users"
id: Mapped[int] = mapped_column(primary_key=True)
email: Mapped[str] = mapped_column(String(40), nullable=False)
name: Mapped[str] = mapped_column(String(40), nullable=False)
password_hash: Mapped[str] = mapped_column(String(70), nullable=False)
phone: Mapped[str] = mapped_column(String(40), nullable=False)
address: Mapped[str] = mapped_column(String(40), nullable=False)
role: Mapped[RoleUser] = mapped_column(nullable=False)
cart: Mapped[dict] = mapped_column(JSONB, nullable=True)
orders: Mapped[list["Orders"]] = relationship(
back_populates="user"
)
reviews: Mapped[list["Reviews"]] = relationship(
back_populates="user"
)

View File

@ -0,0 +1,17 @@
from abc import ABC, abstractmethod
class AbstractBasicRepository(ABC):
@classmethod
@abstractmethod
def add(cls, values: dict):
pass
@abstractmethod
def get_all(self):
pass
@classmethod
@abstractmethod
def delete(cls, id: int):
pass

View File

@ -0,0 +1,27 @@
from abc import ABC, abstractmethod
class AbstractRepository(ABC):
@classmethod
@abstractmethod
def add(cls, values: dict):
pass
@classmethod
@abstractmethod
def get(cls, id: int):
pass
@abstractmethod
def get_all(self):
pass
@classmethod
@abstractmethod
def update(cls, id: int, values: dict):
pass
@classmethod
@abstractmethod
def delete(cls, id: int):
pass

View File

@ -0,0 +1,21 @@
from sqlalchemy import select, update
from application.database import SessionFactory
from application.models.Users import Users
class CartRepository:
@classmethod
def add(cls, id: int, values: dict):
stmt = update(Users).where(Users.id == id).values(cart=values)
with SessionFactory() as session:
session.execute(stmt)
session.commit()
return cls.get(id)
@classmethod
def get(cls, id: int):
query = select(Users.cart).filter_by(id=id)
with SessionFactory() as session:
cart = session.execute(query)
session.commit()
return cart.mappings().one_or_none()

View File

@ -0,0 +1,48 @@
from abc import ABC
from sqlalchemy import delete, insert, select, update
from application.database import SessionFactory
from application.repositories.AbstractRepository import AbstractRepository
from application.models.Categories import Categories
class CategoryRepository(AbstractRepository, ABC):
@classmethod
def add(cls, values: dict):
with SessionFactory() as session:
stmt = insert(Categories).values(**values).returning(Categories)
new_category = session.execute(stmt)
session.commit()
return new_category.scalar_one()
@classmethod
def get(cls, id: int):
query = select(Categories.__table__.columns).filter_by(id=id)
with SessionFactory() as session:
category = session.execute(query)
session.commit()
return category.mappings().one_or_none()
@classmethod
def get_all(cls):
query = select(Categories.__table__.columns)
with SessionFactory() as session:
categories = session.execute(query)
session.commit()
return categories.mappings().all()
@classmethod
def update(cls, id: int, values: dict):
stmt = update(Categories).where(Categories.id == id).values(**values)
with SessionFactory() as session:
session.execute(stmt)
session.commit()
return cls.get(id)
@classmethod
def delete(cls, id: int):
category = cls.get(id)
stmt = delete(Categories).where(Categories.id == id)
with SessionFactory() as session:
session.execute(stmt)
session.commit()
return category

View File

@ -0,0 +1,48 @@
from abc import ABC
from sqlalchemy import delete, insert, select, update
from application.database import SessionFactory
from application.repositories.AbstractRepository import AbstractRepository
from application.models.Manufacturers import Manufacturers
class ManufacturerRepository(AbstractRepository, ABC):
@classmethod
def add(cls, values: dict):
with SessionFactory() as session:
stmt = insert(Manufacturers).values(**values).returning(Manufacturers)
new_manufacturer = session.execute(stmt)
session.commit()
return new_manufacturer.scalar_one()
@classmethod
def get(cls, id: int):
query = select(Manufacturers.__table__.columns).filter_by(id=id)
with SessionFactory() as session:
manufacturer = session.execute(query)
session.commit()
return manufacturer.mappings().one_or_none()
@classmethod
def get_all(cls):
query = select(Manufacturers.__table__.columns)
with SessionFactory() as session:
manufacturers = session.execute(query)
session.commit()
return manufacturers.mappings().all()
@classmethod
def update(cls, id: int, values: dict):
stmt = update(Manufacturers).where(Manufacturers.id == id).values(**values)
with SessionFactory() as session:
session.execute(stmt)
session.commit()
return cls.get(id)
@classmethod
def delete(cls, id: int):
manufacturer = cls.get(id)
stmt = delete(Manufacturers).where(Manufacturers.id == id)
with SessionFactory() as session:
session.execute(stmt)
session.commit()
return manufacturer

View File

@ -0,0 +1,40 @@
from abc import ABC
from sqlalchemy import delete, insert, select
from application.database import SessionFactory
from application.repositories.AbstractBasicRepository import AbstractBasicRepository
from application.models.OrderItems import OrderItems
class OrderItemRepository(AbstractBasicRepository, ABC):
@classmethod
def add(cls, values: dict):
with SessionFactory() as session:
stmt = insert(OrderItems).values(**values).returning(OrderItems)
new_order_item = session.execute(stmt)
session.commit()
return new_order_item.scalar_one()
@classmethod
def get(cls, id: int):
query = select(OrderItems.__table__.columns).filter_by(id=id)
with SessionFactory() as session:
order_item = session.execute(query)
session.commit()
return order_item.mappings().one_or_none()
@classmethod
def get_all(cls, order_id: int):
query = (select(OrderItems.__table__.columns).filter_by(order_id=order_id))
with SessionFactory() as session:
order_items = session.execute(query)
session.commit()
return order_items.mappings().all()
@classmethod
def delete(cls, id: int):
order_item = cls.get(id)
stmt = delete(OrderItems).where(OrderItems.id == id)
with SessionFactory() as session:
session.execute(stmt)
session.commit()
return order_item

View File

@ -0,0 +1,40 @@
from abc import ABC
from sqlalchemy import delete, insert, select
from application.database import SessionFactory
from application.repositories.AbstractBasicRepository import AbstractBasicRepository
from application.models.Orders import Orders
class OrderRepository(AbstractBasicRepository, ABC):
@classmethod
def add(cls, values: dict):
with SessionFactory() as session:
stmt = insert(Orders).values(**values).returning(Orders)
new_order = session.execute(stmt)
session.commit()
return new_order.scalar_one()
@classmethod
def get(cls, id: int):
query = select(Orders.__table__.columns).filter_by(id=id)
with SessionFactory() as session:
order = session.execute(query)
session.commit()
return order.mappings().one_or_none()
@classmethod
def get_all(cls, user_id: int):
query = (select(Orders.__table__.columns).filter_by(user_id=user_id))
with SessionFactory() as session:
orders = session.execute(query)
session.commit()
return orders.mappings().all()
@classmethod
def delete(cls, id: int):
order = cls.get(id)
stmt = delete(Orders).where(Orders.id == id)
with SessionFactory() as session:
session.execute(stmt)
session.commit()
return order

View File

@ -0,0 +1,56 @@
from abc import ABC
from sqlalchemy import delete, insert, select, update
from application.database import SessionFactory
from application.repositories.AbstractRepository import AbstractRepository
from application.models.Parts import Parts
class PartRepository(AbstractRepository, ABC):
@classmethod
def add(cls, values: dict):
with SessionFactory() as session:
stmt = insert(Parts).values(**values).returning(Parts)
new_part = session.execute(stmt)
session.commit()
return new_part.scalar_one()
@classmethod
def get(cls, id: int):
query = select(Parts.__table__.columns).filter_by(id=id)
with SessionFactory() as session:
part = session.execute(query)
session.commit()
return part.mappings().one_or_none()
@classmethod
def get_all(cls):
query = select(Parts.__table__.columns)
with SessionFactory() as session:
parts = session.execute(query)
session.commit()
return parts.mappings().all()
@classmethod
def update(cls, id: int, values: dict):
stmt = update(Parts).where(Parts.id == id).values(**values)
with SessionFactory() as session:
session.execute(stmt)
session.commit()
return cls.get(id)
@classmethod
def delete(cls, id: int):
part = cls.get(id)
stmt = delete(Parts).where(Parts.id == id)
with SessionFactory() as session:
session.execute(stmt)
session.commit()
return part
@classmethod
def get_all_category_id(cls, category_id: int):
query = (select(Parts.__table__.columns).filter_by(category_id=category_id))
with SessionFactory() as session:
parts = session.execute(query)
session.commit()
return parts.mappings().all()

View File

@ -0,0 +1,41 @@
from abc import ABC
from sqlalchemy import delete, insert, select
from application.database import SessionFactory
from application.repositories.AbstractBasicRepository import AbstractBasicRepository
from application.models.Reviews import Reviews
class ReviewRepository(AbstractBasicRepository, ABC):
@classmethod
def add(cls, values: dict):
with SessionFactory() as session:
stmt = insert(Reviews).values(**values).returning(Reviews)
new_review = session.execute(stmt)
session.commit()
return new_review.scalar_one()
@classmethod
def get(cls, id: int):
query = select(Reviews.__table__.columns).filter_by(id=id)
with SessionFactory() as session:
review = session.execute(query)
session.commit()
return review.mappings().one_or_none()
@classmethod
def get_all(cls, part_id: int):
query = (select(Reviews.__table__.columns).filter_by(part_id=part_id))
with SessionFactory() as session:
reviews = session.execute(query)
session.commit()
return reviews.mappings().all()
@classmethod
def delete(cls, id: int):
review = cls.get(id)
stmt = delete(Reviews).where(Reviews.id == id)
with SessionFactory() as session:
session.execute(stmt)
session.commit()
return review

View File

@ -0,0 +1,54 @@
from sqlalchemy import insert, select, update
from application.database import SessionFactory
from application.models.Users import Users, RoleUser
class UserRepository:
@classmethod
def add_user(cls, values: dict):
with SessionFactory() as session:
stmt = insert(Users).values(**values).returning(Users)
new_user = session.execute(stmt)
session.commit()
return new_user.scalar_one()
@classmethod
def get_user(cls, email: str, password: str):
query = (select(Users.__table__.columns)
.filter_by(email=email).filter_by(password_hash=password))
with SessionFactory() as session:
user = session.execute(query)
session.commit()
return user.mappings().one_or_none()
@classmethod
def get_user_id(cls, id: int):
query = select(Users.__table__.columns).filter_by(id=id)
with SessionFactory() as session:
user = session.execute(query)
session.commit()
return user.mappings().one_or_none()
@classmethod
def get_all_users_role(cls):
query = select(Users.__table__.columns).filter_by(role=RoleUser.user)
with SessionFactory() as session:
users = session.execute(query)
session.commit()
return users.mappings().all()
@classmethod
def get_all_users(cls):
query = select(Users)
with SessionFactory() as session:
users = session.execute(query)
session.commit()
return users.scalars().all()
@classmethod
def update_user(cls, id: int, values: dict):
stmt = update(Users).where(Users.id == id).values(**values)
with SessionFactory() as session:
session.execute(stmt)
session.commit()
return cls.get_user_id(id)

View File

@ -0,0 +1,6 @@
from abc import ABC
from application.repositories.AbstractBasicRepository import AbstractBasicRepository
class AbstractBasicService(AbstractBasicRepository, ABC):
pass

View File

@ -0,0 +1,6 @@
from abc import ABC
from application.repositories.AbstractRepository import AbstractRepository
class AbstractService(AbstractRepository, ABC):
pass

View File

@ -0,0 +1,51 @@
from application.repositories.CartRepository import CartRepository
from application.services.AbstractBasicService import AbstractBasicService
class CartService(AbstractBasicService):
@classmethod
def add(cls, user_id: int, values: dict):
user = CartRepository.get(user_id)
current_cart = user["cart"]
if len(current_cart) > 0:
if len(current_cart["cart"]) > 0:
current_cart["cart"] += [values]
else:
current_cart = {"cart": [values]}
cart = CartRepository.add(user_id, current_cart)
if len(cart["cart"]) > 0:
if len(cart["cart"]["cart"]) > 0:
cart = cart["cart"]["cart"][len(cart["cart"]["cart"]) - 1]
return cart
@classmethod
def get_all(cls, user_id: int):
cart = CartRepository.get(user_id)
print(f"all carts from database: {cart}")
if len(cart["cart"]) > 0:
if len(cart["cart"]["cart"]) > 0:
cart = cart["cart"]["cart"]
return cart
@classmethod
def get(cls, id: int, user_id: int):
carts = cls.get_all(user_id)
current_cart = None
for cart in carts:
if cart["id"] == id:
current_cart = cart
break
return current_cart
@classmethod
def delete(cls, id: int, user_id: int):
carts = cls.get_all(user_id)
cart = cls.get(id, user_id)
if isinstance(carts, list):
carts.remove(cart)
if len(carts) > 0:
CartRepository.add(user_id, {"cart": carts})
else:
CartRepository.add(user_id, {})
return cart

View File

@ -0,0 +1,33 @@
from pydantic import TypeAdapter
from application.dto.CategoryDTO import CategoryDTO
from application.repositories.CategoryRepository import CategoryRepository
from application.services.AbstractService import AbstractService
class CategoryService(AbstractService):
@classmethod
def add(cls, category: CategoryDTO):
category_dictionary = category.to_dictionary()
new_category = CategoryRepository.add(category_dictionary)
return TypeAdapter(CategoryDTO).dump_python(new_category)
@classmethod
def get(cls, id: int):
category = CategoryRepository.get(id)
return TypeAdapter(CategoryDTO).dump_python(category)
@classmethod
def get_all(cls):
categories = CategoryRepository.get_all()
return TypeAdapter(list[CategoryDTO]).dump_python(categories)
@classmethod
def update(cls, id: int, category: CategoryDTO):
category_dictionary = category.to_dictionary()
new_category = CategoryRepository.update(id, category_dictionary)
return TypeAdapter(CategoryDTO).dump_python(new_category)
@classmethod
def delete(cls, id: int):
category = CategoryRepository.delete(id)
return TypeAdapter(CategoryDTO).dump_python(category)

View File

@ -0,0 +1,33 @@
from pydantic import TypeAdapter
from application.dto.ManufacturerDTO import ManufacturerDTO
from application.repositories.ManufacturerRepository import ManufacturerRepository
from application.services.AbstractService import AbstractService
class ManufacturerService(AbstractService):
@classmethod
def add(cls, manufacturer: ManufacturerDTO):
manufacturer_dictionary = manufacturer.to_dictionary()
new_manufacturer = ManufacturerRepository.add(manufacturer_dictionary)
return TypeAdapter(ManufacturerDTO).dump_python(new_manufacturer)
@classmethod
def get(cls, id: int):
manufacturer = ManufacturerRepository.get(id)
return TypeAdapter(ManufacturerDTO).dump_python(manufacturer)
@classmethod
def get_all(cls):
manufacturers = ManufacturerRepository.get_all()
return TypeAdapter(list[ManufacturerDTO]).dump_python(manufacturers)
@classmethod
def update(cls, id: int, manufacturer: ManufacturerDTO):
manufacturer_dictionary = manufacturer.to_dictionary()
new_manufacturer = ManufacturerRepository.update(id, manufacturer_dictionary)
return TypeAdapter(ManufacturerDTO).dump_python(new_manufacturer)
@classmethod
def delete(cls, id: int):
manufacturer = ManufacturerRepository.delete(id)
return TypeAdapter(ManufacturerDTO).dump_python(manufacturer)

View File

@ -0,0 +1,39 @@
from pydantic import TypeAdapter
from application.dto.OrderItemDTO import OrderItemDTO
from application.repositories.PartRepository import PartRepository
from application.repositories.OrderItemRepository import OrderItemRepository
from application.services.AbstractBasicService import AbstractBasicService
class OrderItemService(AbstractBasicService):
@classmethod
def add(cls, order_item: OrderItemDTO):
order_item_dictionary = order_item.to_dictionary()
new_order_item = OrderItemRepository.add(order_item_dictionary)
return TypeAdapter(OrderItemDTO).dump_python(new_order_item)
@classmethod
def get(cls, id: int):
order_item = OrderItemRepository.get(id)
return TypeAdapter(OrderItemDTO).dump_python(order_item)
@classmethod
def get_all(cls, order_id: int):
order_items = OrderItemRepository.get_all(order_id)
return TypeAdapter(list[OrderItemDTO]).dump_python(order_items)
@classmethod
def delete(cls, id: int):
order_item = OrderItemRepository.delete(id)
return TypeAdapter(OrderItemDTO).dump_python(order_item)
@classmethod
def get_all_parts(cls, order_id: int):
order_items = OrderItemRepository.get_all(order_id)
new_order_items = []
for order_item in order_items:
part = PartRepository.get(order_item["part_id"])
new_order_items += [{"id": order_item["id"], "count": order_item["count"],
"price": order_item["price"], "order_id": order_item["order_id"],
"name_part": part["name"]}]
return new_order_items

View File

@ -0,0 +1,54 @@
from pydantic import TypeAdapter
from typing import List
from application.dto.OrderDTO import OrderDTO, OrderDTOInt
from application.dto.OrderItemDTO import OrderItemDTO
from application.repositories.OrderRepository import OrderRepository
from application.repositories.OrderItemRepository import OrderItemRepository
from application.services.AbstractBasicService import AbstractBasicService
class OrderService(AbstractBasicService):
@classmethod
def _to_int(cls, order: OrderDTO):
new_order = OrderDTOInt(
id=order["id"],
order_date=order["order_date"],
total_amount=order["total_amount"],
status=order["status"].value,
user_id=order["user_id"]
)
return new_order
@classmethod
def add(cls, order: OrderDTO):
order_dictionary = order.to_dictionary()
new_order = OrderRepository.add(order_dictionary)
return TypeAdapter(OrderDTO).dump_python(new_order)
@classmethod
def get(cls, id: int):
order = OrderRepository.get(id)
order = cls._to_int(order)
return TypeAdapter(OrderDTOInt).dump_python(order)
@classmethod
def get_all(cls, user_id: int):
orders = OrderRepository.get_all(user_id)
new_orders = []
for order in orders:
new_orders += [cls._to_int(order)]
return TypeAdapter(list[OrderDTOInt]).dump_python(new_orders)
@classmethod
def delete(cls, id: int):
order = OrderRepository.delete(id)
return TypeAdapter(OrderDTO).dump_python(order)
@classmethod
def add_order_items(cls, order: OrderDTO, items: List[OrderItemDTO]):
new_order = cls.add(order)
for item in items:
new_item = {"count": item["count"], "price": item["price"],
"part_id": item["part_id"], "order_id": new_order["id"]}
OrderItemRepository.add(new_item)
return new_order

View File

@ -0,0 +1,194 @@
from pydantic import TypeAdapter
from application.config import es
from application.dto.PartDTO import PartDTO, parts_mapping
from application.repositories.PartRepository import PartRepository
from application.services.ManufacturerService import ManufacturerService
from application.services.AbstractService import AbstractService
class PartService(AbstractService):
@classmethod
def add(cls, part: PartDTO):
part_dictionary = part.to_dictionary()
new_part = PartRepository.add(part_dictionary)
new_part = TypeAdapter(PartDTO).dump_python(new_part)
PartIndexManager.add(part=PartDTO(**new_part))
return new_part
@classmethod
def get(cls, id: int):
part = PartRepository.get(id)
return TypeAdapter(PartDTO).dump_python(part)
@classmethod
def get_all(cls):
parts = PartRepository.get_all()
return TypeAdapter(list[PartDTO]).dump_python(parts)
@classmethod
def update(cls, id: int, part: PartDTO):
part_dictionary = part.to_dictionary()
new_part = PartRepository.update(id, part_dictionary)
PartIndexManager.update(id, part_dictionary)
return TypeAdapter(PartDTO).dump_python(new_part)
@classmethod
def delete(cls, id: int):
part = PartRepository.delete(id)
PartIndexManager.delete(id)
return TypeAdapter(PartDTO).dump_python(part)
@classmethod
def get_all_category_id(cls, category_id: int):
parts = PartRepository.get_all_category_id(category_id)
return TypeAdapter(list[PartDTO]).dump_python(parts)
@classmethod
def get_part_manufacturer(cls, id: int):
part = cls.get(id)
manufacturer = ManufacturerService.get(part["manufacturer_id"])
new_part = {"id": part["id"], "name": part["name"], "description": part["description"],
"feature": part["feature"], "price": part["price"], "category_id": part["category_id"],
"manufacturer": manufacturer}
return new_part
class PartIndexManager:
@classmethod
def get_part(cls, part: dict):
return {
'id': part["id"],
'name': part["name"],
'description': part["description"],
'feature': part["feature"],
'price': part["price"],
'category_id': part["category_id"],
'manufacturer_id': part["manufacturer_id"]
}
@classmethod
def add(cls, part: PartDTO):
doc = {
'id': part.id,
'name': part.name,
'description': part.description,
'feature': part.feature,
'price': part.price,
'category_id': part.category_id,
'manufacturer_id': part.manufacturer_id
}
es.index(index='parts', id=part.id, body=doc)
@classmethod
def search(cls, search_query: str):
try:
query = {
"query": {
"multi_match": {
"query": search_query,
"fields": ["feature", "description", "comment"],
"type": "best_fields",
"fuzziness": "AUTO"
}
}
}
response = es.search(
index="parts,reviews",
body=query,
size=1000
)
search_results = []
for hit in response['hits']['hits']:
source = hit['_source']
if hit['_index'] == 'parts':
item = {
'id': source.get('id'),
'name': source.get('name'),
'description': source.get('description', ''),
'feature': source.get('feature', {}),
'price': source.get('price'),
'category_id': source.get('category_id'),
'manufacturer_id': source.get('manufacturer_id'),
'type': 'part',
'score': hit['_score']
}
else:
item = {
'id': source.get('id'),
'rating': source.get('rating'),
'comment': source.get('comment', ''),
'created_at': source.get('created_at'),
'updated_at': source.get('updated_at'),
'user_id': source.get('user_id'),
'part_id': source.get('part_id'),
'type': 'review',
'score': hit['_score']
}
search_results.append(item)
return search_results
except Exception as e:
raise Exception(f"Error in search_parts_and_reviews: {str(e)}")
@classmethod
def get_parts(cls, result_searches: list):
parts = []
for result_search in result_searches:
found = False
if "part_id" in result_search:
part = PartService.get(result_search["part_id"])
new_part = cls.get_part(part)
else:
new_part = cls.get_part(result_search)
if len(parts) > 0:
for part in parts:
if part["id"] == new_part["id"]:
found = True
break
if not found:
parts += [new_part]
return parts
@classmethod
def update(cls, part_id: int, part):
try:
if not es.exists(index='parts', id=part_id):
return {'error': f'Part with id {part_id} not found'}
update_doc = {
'doc': {
k: v for k, v in part.items()
if k in parts_mapping['mappings']['properties']
}
}
response = es.update(
index='parts',
id=part_id,
body=update_doc
)
if response['result'] == 'updated':
return {'message': f'Part with id {part_id} successfully updated'}
else:
return {'error': f'Failed to update part with id {part_id}'}
except Exception as e:
return {'error': f'Error updating part: {str(e)}'}
@classmethod
def delete(cls, part_id: int):
try:
response = es.delete(index='parts', id=part_id)
if response['result'] == 'deleted':
return {'message': f'Part with id {part_id} successfully deleted'}
else:
return {'error': f'Failed to delete part with id {part_id}'}
except Exception as e:
return {'error': f'Error deleting part: {str(e)}'}

View File

@ -0,0 +1,63 @@
from pydantic import TypeAdapter
from application.config import es
from application.dto.ReviewDTO import ReviewDTO
from application.repositories.ReviewRepository import ReviewRepository
from application.services.AbstractBasicService import AbstractBasicService
from application.services.UserService import UserService
class ReviewService(AbstractBasicService):
@classmethod
def add(cls, review: ReviewDTO):
review_dictionary = review.to_dictionary()
new_review = ReviewRepository.add(review_dictionary)
new_review = TypeAdapter(ReviewDTO).dump_python(new_review)
ReviewIndexManager.add(ReviewDTO(**new_review))
return new_review
@classmethod
def get(cls, id: int):
review = ReviewRepository.get(id)
return TypeAdapter(ReviewDTO).dump_python(review)
@classmethod
def get_all(cls, part_id: int):
reviews = ReviewRepository.get_all(part_id)
new_reviews = []
for review in reviews:
user = UserService.get_user_id(review["user_id"])
new_reviews += [{"id": review["id"], "rating": review["rating"], "comment": review["comment"],
"email": user["email"], "user_name": user["name"], "part_id": review["part_id"]}]
return new_reviews
@classmethod
def delete(cls, id: int):
review = ReviewRepository.delete(id)
ReviewIndexManager.delete(id)
return TypeAdapter(ReviewDTO).dump_python(review)
class ReviewIndexManager:
@classmethod
def add(cls, review: ReviewDTO):
doc = {
'id': review.id,
'rating': review.rating,
'comment': review.comment,
'created_at': review.created_at.isoformat(),
'updated_at': review.updated_at.isoformat(),
'user_id': review.user_id,
'part_id': review.part_id
}
es.index(index='reviews', id=review.id, body=doc)
@classmethod
def delete(cls, review_id: int):
try:
response = es.delete(index='reviews', id=review_id)
if response['result'] == 'deleted':
return {'message': f'Review with id {review_id} successfully deleted'}
else:
return {'error': f'Failed to delete review with id {review_id}'}
except Exception as e:
return {'error': f'Error deleting review: {str(e)}'}

View File

@ -0,0 +1,110 @@
from pydantic import TypeAdapter
from flask_jwt_extended import create_access_token
from datetime import timedelta
from hashlib import sha256
from application.dto.UserDTO import UserDTO, UserDTOInt, UserDTOToken
from application.repositories.UserRepository import UserRepository
class UserService:
@classmethod
def _to_int(cls, user: UserDTO):
new_user = UserDTOInt(
id=user["id"],
email=user["email"],
name=user["name"],
phone=user["phone"],
password_hash=user["password_hash"],
address=user["address"],
role=user["role"].value
)
return TypeAdapter(UserDTOInt).dump_python(new_user)
@classmethod
def _to_user_token(cls, user: UserDTO, token: str):
new_user = UserDTOToken(
id=user["id"],
email=user["email"],
name=user["name"],
role=user["role"].value,
token=token
)
return TypeAdapter(UserDTOToken).dump_python(new_user)
@classmethod
def add_user(cls, user: UserDTO):
user.password_hash = cls.get_hash(user.password_hash)
user_dictionary = user.to_dictionary()
new_user = UserRepository.add_user(user_dictionary)
new_user = TypeAdapter(UserDTOInt).dump_python(new_user)
token = cls.get_token(new_user["id"])
return cls._to_user_token(new_user, token)
@classmethod
def add_default_user(cls, user: UserDTO):
user.password_hash = cls.get_hash(user.password_hash)
user_dictionary = user.to_dictionary()
new_user = UserRepository.add_user(user_dictionary)
return TypeAdapter(UserDTO).dump_python(new_user)
@classmethod
def get_user(cls, email: str, password: str):
new_password = cls.get_hash(password)
user = UserRepository.get_user(email, new_password)
if user is None:
return None
else:
token = cls.get_token(user["id"])
return cls._to_user_token(user, token)
@classmethod
def get_user_id(cls, id: int, flag=False):
user = UserRepository.get_user_id(id)
if user is None:
return None
else:
if flag:
return cls._to_int(user)
else:
return user
@classmethod
def get_all_users_role(cls, users=None):
if users is None:
users = UserRepository.get_all_users_role()
users = TypeAdapter(list[UserDTO]).dump_python(users)
new_users = []
for user in users:
new_users += [cls._to_int(user)]
return TypeAdapter(list[UserDTOInt]).dump_python(new_users)
@classmethod
def get_all_users(cls):
users = UserRepository.get_all_users()
return cls.get_all_users_role(users)
@classmethod
def update_user(cls, id: int, user: UserDTO):
user.password_hash = cls.get_hash(user.password_hash)
user_dictionary = user.to_dictionary()
new_user = UserRepository.update_user(id, user_dictionary)
return cls._to_int(new_user)
@classmethod
def get_hash(cls, string: str):
hash = sha256()
hash.update(str.encode(string))
return hash.hexdigest()
@classmethod
def get_token(cls, id, expire_time=24):
expire_delta = timedelta(expire_time)
additional_claims = {
'sub': str(id)
}
token = create_access_token(
identity=id, expires_delta=expire_delta,
additional_claims=additional_claims
)
return token

47
create_directory_files.py Normal file
View File

@ -0,0 +1,47 @@
import os
import sys
def create_directory(path):
try:
if not os.path.exists(path):
os.mkdir(path)
except OSError as e:
print(f"Не удалось создать директорию {path}: {e}")
else:
print(f"Директория {path} успешно создана")
def create_files(path, names, add_par_file):
for name in names:
new_name = f"{path}\\{name}.py" if add_par_file == "no" else f"{path}\\{name}{add_par_file}.py"
file = open(new_name, "w")
file.close()
return 0
def read_file(name_file):
names = []
with open(name_file, 'r') as file:
for line in file:
names += [line.strip()]
file.close()
return names
def main(name_directory, name_file, add_par_file):
names = read_file(name_file)
path = f"{os.getcwd()}\\application\\{name_directory}"
create_directory(path)
create_files(path, names, add_par_file)
if __name__ == "__main__":
if len(sys.argv) != 4:
print("Usage: python script.py <name_directory> <input_file> <additional_parameter_file>")
sys.exit(1)
name_directory = sys.argv[1]
input_file = sys.argv[2]
add_par_file = sys.argv[3]
main(name_directory, input_file, add_par_file)

47
docker-compose.yml Normal file
View File

@ -0,0 +1,47 @@
version: '3.7'
services:
elasticsearch:
image: elasticsearch:7.17.10
build:
context: ./docker-files
dockerfile: Dockerfile.elasticsearch
container_name: elasticsearch
environment:
- discovery.type=single-node
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
- xpack.security.enabled=false
- bootstrap.memory_lock=true
ulimits:
memlock:
soft: -1
hard: -1
volumes:
- elasticsearch-data:/usr/share/elasticsearch/data
ports:
- "9200:9200"
networks:
- elastic
kibana:
image: kibana:7.17.10
build:
context: ./docker-files
dockerfile: Dockerfile.kibana
container_name: kibana
environment:
- ELASTICSEARCH_HOSTS=http://elasticsearch:9200
ports:
- "5601:5601"
depends_on:
- elasticsearch
networks:
- elastic
volumes:
elasticsearch-data:
driver: local
networks:
elastic:
driver: bridge

View File

@ -0,0 +1,8 @@
FROM elasticsearch:7.17.10
COPY elasticsearch.yml /usr/share/elasticsearch/config/
COPY jvm.options /usr/share/elasticsearch/config/
USER root
RUN elasticsearch-plugin install analysis-icu
USER elasticsearch

View File

@ -0,0 +1,7 @@
FROM kibana:7.17.10
COPY kibana.yml /usr/share/kibana/config/
USER root
RUN kibana-plugin install <plugin-name>
USER kibana

View File

@ -0,0 +1,12 @@
cluster.name: "docker-cluster"
network.host: 0.0.0.0
discovery.type: single-node
xpack.security.enabled: false
bootstrap.memory_lock: true
path.data: /usr/share/elasticsearch/data
path.logs: /usr/share/elasticsearch/logs
http.cors.enabled: true
http.cors.allow-origin: "*"

49
docker-files/jvm.options Normal file
View File

@ -0,0 +1,49 @@
## JVM configuration
################################################################
## IMPORTANT: JVM heap size
################################################################
-Xms512m
-Xmx512m
################################################################
## Expert settings
################################################################
## GC configuration
8-13:-XX:+UseConcMarkSweepGC
8-13:-XX:CMSInitiatingOccupancyFraction=75
8-13:-XX:+UseCMSInitiatingOccupancyOnly
## G1GC Configuration
14-:-XX:+UseG1GC
14-:-XX:G1ReservePercent=25
14-:-XX:InitiatingHeapOccupancyPercent=30
## JVM temporary directory
-Djava.io.tmpdir=${ES_TMPDIR}
## heap dumps
# generate a heap dump when an allocation from the Java heap fails
# heap dumps are created in the working directory of the JVM
-XX:+HeapDumpOnOutOfMemoryError
# specify an alternative path for heap dumps
# ensure the directory exists and has sufficient space
#-XX:HeapDumpPath=/var/lib/elasticsearch
# specify an alternative path for JVM fatal error logs
#-XX:ErrorFile=/var/log/elasticsearch/hs_err_pid%p.log
## JDK 8 GC logging
8:-XX:+PrintGCDetails
8:-XX:+PrintGCDateStamps
8:-XX:+PrintTenuringDistribution
8:-XX:+PrintGCApplicationStoppedTime
8:-Xloggc:/var/log/elasticsearch/gc.log
8:-XX:+UseGCLogFileRotation
8:-XX:NumberOfGCLogFiles=32
8:-XX:GCLogFileSize=64m
# JDK 9+ GC logging
9-:-Xlog:gc*,gc+age=trace,safepoint:file=/var/log/elasticsearch/gc.log:utctime,pid,tags:filecount=32,filesize=64m

4
docker-files/kibana.yml Normal file
View File

@ -0,0 +1,4 @@
server.name: kibana
server.host: "0"
elasticsearch.hosts: [ "http://elasticsearch:9200" ]
monitoring.ui.container.elasticsearch.enabled: true

26
front/.gitignore vendored Normal file
View File

@ -0,0 +1,26 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
.parcel-cache

17
front/index.html Normal file
View File

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<script type="module" src="./node_modules/bootstrap/dist/js/bootstrap.min.js"></script>
<link rel="stylesheet" href="/node_modules/bootstrap/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="/node_modules/@fortawesome/fontawesome-free/css/all.min.css">
<title>Интернет-магазин автозапчастей</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

1365
front/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

22
front/package.json Normal file
View File

@ -0,0 +1,22 @@
{
"name": "internet-auto-parts-store",
"private": true,
"version": "1.0.0",
"type": "module",
"scripts": {
"start": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@fortawesome/fontawesome-free": "^6.2.1",
"bootstrap": "^5.2.2",
"maska": "^3.0.4",
"vue": "^3.2.41",
"vue-router": "^4.1.6"
},
"devDependencies": {
"@vitejs/plugin-vue": "^3.2.0",
"vite": "^3.2.3"
}
}

BIN
front/public/Part.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 609 KiB

19
front/src/App.vue Normal file
View File

@ -0,0 +1,19 @@
<script>
import Header from './components/Header.vue';
export default {
components: {
Header
}
}
</script>
<template>
<Header></Header>
<div class="container">
<router-view></router-view>
</div>
</template>
<style scoped>
</style>

View File

@ -0,0 +1,199 @@
<script>
import DataService from '../services/DataService';
import CatalogMixins from '../mixins/CatalogMixins.js';
export default {
mixins: [
CatalogMixins
],
data() {
return {
getAllUrl: 'cart',
dataUrl: 'cart/',
newItems: [],
path: '/public/Part.jpg',
order_date: ""
}
},
beforeCreate() {
if(localStorage.getItem("token") == null) {
this.$router.push("/signupin");
}
else
if(document.getElementById("user").innerText == "Войти" && localStorage.getItem("token") != null)
document.getElementById("user").innerText = "Выход (" + localStorage.getItem("user").substr(0,5) +"...)";
},
computed: {
totalPrice() {
return this.newItems.reduce((total, item) => {
const price = item.commonPrice;
return total + price;
}, 0);
}
},
methods: {
getItems() {
if (localStorage.getItem("token") != null) {
DataService.getData(this.getAllUrl).then(data => {
this.items = data;
var i, varDict, count = 0;
for (i = 0; i < this.items.length; i++){
varDict = { id: 0, name: "", commonPrice: "", count: "" };
count = this.items[i]["count"];
varDict["id"] = this.items[i]["id"];
varDict["name"] = this.items[i]["part"]["name"];
varDict["commonPrice"]= this.items[i]["part"]["price"] * count;
varDict["count"] = count;
this.newItems.push(varDict);
}
});
}
},
removeItem(id){
if (confirm('Удалить выбранный элемент?')) {
DataService.delete(this.dataUrl + id).then((data) => {
this.getItems();
//this.$router.go(0);
});
}
},
addOrderItems(){
var i, orderItem, orderItems = [];
for (i = 0; i < this.items.length; i++){
orderItem = { count: this.items[i]["count"],
price: this.items[i]["part"]["price"],
part_id: this.items[i]["part"]["id"]};
orderItems.push(orderItem);
}
DataService.create(`orders/orderwithitems?order_date=${this.order_date}&total_amount=${this.totalPrice}&status=0&items=${encodeURIComponent(JSON.stringify(orderItems))}`)
.then(() => {
this.getItems();
this.$router.push("/orders");
});
},
showOrderItems(){
this.modal.header = 'Добавление заказа';
this.modal.confirm = 'Добавить';
this.modalShow = true;
}
}
}
</script>
<template>
<div class="container cart-container" v-if="newItems.length > 0">
<h2 class="mb-4">Корзина запчастей</h2>
<div v-for="(item, index) in newItems" :key="item.id" class="cart-item">
<div class="row align-items-center">
<div class="col-md-2">
<div class="product-image">
<img :src="this.path">
</div>
</div>
<div class="col-md-4">
<h5>{{ item.name }}</h5>
</div>
<div class="col-md-3">
<span class="quantity">Количество: {{ item.count }} шт.</span>
</div>
<div class="col-md-3">
<p class="price mb-0">Сумма: {{ item.commonPrice }} </p>
</div>
<div class="col-md-2 mt-2">
<button class="btn btn-outline-danger btn-sm" @click="removeItem(item.id)">Удалить</button>
</div>
</div>
</div>
<div class="total-section">
<div class="row">
<div class="col-md-6">
<h4>Итого:</h4>
</div>
<div class="col-md-6 text-end">
<h4>{{ totalPrice }} </h4>
</div>
</div>
<button class="btn btn-danger order-btn" @click="showOrderItems">Заказать запчасти</button>
</div>
</div>
<div v-else class="cart-empty">
<p>Корзина пуста</p>
</div>
<Modal
:header="this.modal.header"
:confirm="this.modal.confirm"
v-model:visible="this.modalShow"
@done="addOrderItems">
<div class="col-mb-3">
<label for="Order_date" class="form-label">Дата</label>
<input type="date" class="form-control" id="Order_date" required v-model="this.order_date">
</div>
</Modal>
</template>
<style scoped>
.cart-container {
max-width: 800px;
margin: 30px auto;
padding: 20px;
}
.cart-item {
border: 1px solid #ddd;
border-radius: 8px;
margin-bottom: 15px;
padding: 15px;
background-color: #fff;
transition: transform 0.2s ease;
}
.cart-item:hover {
transform: translateY(-3px);
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
.total-section {
margin-top: 30px;
padding: 20px;
background-color: #f8f9fa;
border-radius: 8px;
}
.order-btn {
width: 100%;
padding: 15px;
font-size: 1.2rem;
margin-top: 20px;
}
.quantity {
font-weight: 500;
color: #555;
}
.price {
font-weight: 500;
color: #333;
}
.cart-empty {
text-align: center;
color: #6c757d;
font-style: italic;
}
.product-image {
width: 80px;
height: 80px;
border-radius: 50%;
overflow: hidden;
margin: 0 auto;
}
.product-image img {
width: 100%;
height: 100%;
object-fit: cover;
}
</style>

View File

@ -0,0 +1,129 @@
<script>
import DataService from '../services/DataService';
import CatalogMixins from '../mixins/CatalogMixins.js';
import PartList from '../components/PartList.vue'
export default {
mixins: [
CatalogMixins
],
components: {
PartList
},
data() {
return {
getAllUrl: 'categories',
path: '/public/Part.jpg',
newItems: [],
parts: [],
query: ""
}
},
beforeCreate() {
if(document.getElementById("user").innerText == "Войти" && localStorage.getItem("token") != null)
document.getElementById("user").innerText = "Выход (" + localStorage.getItem("user").substr(0,5) +"...)";
},
methods: {
getItems() {
DataService.getData(this.getAllUrl).then(data => {
this.items = data;
var i;
this.newItems = [];
for (i = 0; i < this.items.length; i++){
var varDict = { id: 0, name: "", link: ""};
varDict["id"] = this.items[i]["id"];
varDict["name"] = this.items[i]["name"];
varDict["link"] = `parts/${varDict["id"]}`;
this.newItems.push(varDict);
}
});
},
search() {
DataService.getData(`parts/get_part_review?search_query=${this.query}`).then(data => {
this.parts = data;
});
}
}
}
</script>
<template>
<div class="search-container">
<input type="text" class="search-input" placeholder="Введите текст для поиска..." v-model="this.query">
<button class="search-button" @click="search">Найти</button>
<div class="names-container" v-for="(item, index) in this.newItems">
<div class="name-item">
<p><a :href="item['link']">{{ item["name"] }}</a></p>
</div>
</div>
</div>
<PartList
:parts="this.parts" v-if="this.parts.length > 0">
</PartList>
</template>
<style scoped>
.search-container {
width: 500px;
padding: 20px;
border: 2px solid #ccc;
border-radius: 5px;
margin: 20px;
}
.search-input {
width: 100%;
padding: 8px;
margin-bottom: 10px;
border: 1px solid #999;
border-radius: 4px;
box-sizing: border-box;
}
.search-button {
width: 100%;
padding: 8px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
margin-bottom: 15px;
}
.search-button:hover {
background-color: #45a049;
}
.names-container {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.name-item {
flex: 0 0 calc(33.33% - 10px);
box-sizing: border-box;
}
.name-item p {
margin: 0;
padding: 5px;
background-color: #f5f5f5;
border-radius: 3px;
}
.name-item a {
text-decoration: none;
color: #333;
display: block;
padding: 5px;
border: 1px solid #ddd;
border-radius: 3px;
transition: all 0.3s ease;
}
.name-item a:hover {
background-color: #e9e9e9;
border-color: #999;
}
</style>

View File

@ -0,0 +1,90 @@
<script>
export default {
props: {
headers: Array,
items: Array,
selectedItems: Array,
imagePath: String
},
emits: {
dblclick: null
},
methods: {
cardClick(id) {
if (this.isSelected(id)) {
var index = this.selectedItems.indexOf(id);
if (index !== -1) {
this.selectedItems.splice(index, 1);
}
} else {
this.selectedItems.push(id);
}
},
cardDblClick(item) {
this.$emit('dblclick', item);
},
isSelected(id) {
return this.selectedItems.includes(id);
}
}
}
</script>
<template>
<div id="div-items" class = "row" style="justify-content: center;">
<div align="justify" class = "card" v-for="(item, index) in this.items" :id = "'item - ' + item.id"
@click="cardClick(item.id)"
@dblclick="cardDblClick(item)"
:class="{selected: isSelected(item.id)}">
<img class="card-img" :src="imagePath" v-if="typeof(imagePath) === 'string'">
<img class="card-img" :src="imagePath[index]" v-else>
<p v-for="header in this.headers"><b>{{ header.label }}:</b> {{ item[header.name] }} <span v-if="header.name === 'commonPrice'"></span></p>
</div>
</div>
</template>
<style scoped>
.card {
width: 20%;
height: 340px;
margin: 10px;
float: left;
text-align: center;
box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2);
transition: 0.3s;
border-radius: 20px;
overflow: hidden;
-moz-hyphens: auto;
-webkit-hyphens: auto;
-ms-hyphens: auto;
}
.card-img{
margin-top: 4px;
border-radius: 20px;
width: 100%;
height: 100%;
}
.card:hover {
box-shadow: 0 8px 16px 0 rgba(0,0,0,0.2);
cursor: pointer;
}
p {
padding: 0px;
margin: 0px;
font-size: 18px;
text-align: center;
font-family: sans-serif;
font-weight: 500;
}
.selected {
background-color: color-mix(in srgb, #c41e3a 50%, #383838);
opacity: 80%;
}
div {
user-select: none;
}
</style>

View File

@ -0,0 +1,72 @@
<script>
export default {
props: {
headers: Array,
items: Array,
selectedItems: Array
},
emits: {
dblclick: null
},
methods: {
rowClick(id) {
if (this.isSelected(id)) {
var index = this.selectedItems.indexOf(id);
if (index !== -1) {
this.selectedItems.splice(index, 1);
}
} else {
this.selectedItems.push(id);
}
},
rowDblClick(id) {
this.$emit('dblclick', id);
},
isSelected(id) {
return this.selectedItems.includes(id);
},
dataConvert(data) {
return data;
}
}
}
</script>
<template>
<table class="table table-hover">
<thead>
<tr>
<th scope="col">#</th>
<th v-for="header in this.headers"
:id="header.name"
scope="col">{{ header.label }}</th>
</tr>
</thead>
<tbody>
<tr v-for="(item, index) in this.items"
@click="rowClick(item.id)"
@dblclick="rowDblClick(item.id)"
:class="{selected: isSelected(item.id)}">
<th scope="row">{{ index + 1 }}</th>
<td v-for="header in this.headers">
{{ dataConvert(item[header.name]) }}
</td>
</tr>
</tbody>
</table>
</template>
<style scoped>
tbody tr:hover {
cursor: pointer;
}
tr.selected {
background-color: #0000fd;
opacity: 80%;
}
tbody tr {
user-select: none;
}
</style>

View File

@ -0,0 +1,51 @@
<script>
export default {
methods: {
getRoutes() {
var menuList;
menuList = this.$router.options.routes.filter(route => route.meta?.hasOwnProperty('label'));
if (localStorage.getItem("token") == null)
menuList = menuList.filter(item => item.path !== "/orders" && item.path !== "/cart")
return menuList;
},
logout() {
if (document.getElementById("user").innerText == "Войти")
this.$router.push('/signupin');
else {
localStorage.clear();
this.$router.push('/signupin');
document.getElementById("user").innerText = "Войти";
location.reload();
}
}
},
data() {
return {
token: null
}
},
created(){
this.token = localStorage.getItem("token");
}
}
</script>
<template>
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<div class="container">
<a class="navbar-brand" href="/categories">Internet shop parts</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item"
v-for="route in this.getRoutes()">
<router-link class="nav-link" :to="route.path">{{ route.meta.label }}</router-link>
</li>
</ul>
<a id="user" class="btn btn-auth text-white" @click.prevent="logout">Войти</a>
</div>
</div>
</nav>
</template>

View File

@ -0,0 +1,63 @@
<script>
export default {
props: {
header: String,
confirm: String,
visible: Boolean
},
emits: {
done: null,
'update:visible': (value) => {
if (typeof value !== 'boolean') {
throw 'Value is not a boolean';
}
return true;
}
},
methods: {
hide() {
this.$emit('update:visible', false);
},
done() {
if (this.$refs.form.checkValidity()) {
this.$emit('done');
this.hide();
} else {
this.$refs.form.reportValidity();
}
}
}
}
</script>
<template>
<div class="modal fade" tabindex="-1" aria-hidden="true"
:class="{ 'modal-show': this.visible, 'show': this.visible }">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="exampleModalLabel">{{ header }}</h1>
<button type="button" class="btn-close" aria-label="Close"
@click.prevent="hide"></button>
</div>
<div class="modal-body">
<form @submit.prevent="done" ref="form">
<slot></slot>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary"
@click.prevent="hide">Закрыть</button>
<button type="button" class="btn btn-danger"
@click.prevent="done">{{ confirm }}</button>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.modal-show {
display: block;
}
</style>

View File

@ -0,0 +1,158 @@
<script>
import DataService from '../services/DataService';
import CatalogMixins from '../mixins/CatalogMixins.js';
export default {
mixins: [
CatalogMixins
],
data() {
return {
getAllUrl: 'orders',
path: '/public/Part.jpg',
statuses: [
{name: 'in_processing', label: 'собирается'},
{name: 'shipped', label: 'в пути'},
{name: 'delivered', label: 'доставлен'}
]
}
},
beforeCreate() {
if(document.getElementById("user").innerText == "Войти" && localStorage.getItem("token") != null)
document.getElementById("user").innerText = "Выход (" + localStorage.getItem("user").substr(0,5) +"...)";
},
methods: {
getItems() {
DataService.getData(this.getAllUrl).then(data => {
this.items = data;
for(var i = 0; i < this.items.length; i++){
this.items[i].order_date = this.ChangeFormat(this.items[i].order_date);
this.items[i].status = this.statuses[this.items[i].status]["label"];
}
});
},
ChangeFormat(date){
var vde = date.replace(/(\d*)-(\d*)-(\d*)/, '$3.$2.$1').split(".");
var i, de;
const options = {year: 'numeric', month: 'long', day: 'numeric'};
for(i = 0; i < vde.length; i++)
vde[i] = parseInt(vde[i]);
de = new Date(vde[2], vde[1] - 1, vde[0]);
return de.toLocaleString("ru", options);
},
editOrderDblClick(item){
var id = -1;
if(typeof(item) === "number")
id = item;
else
id = item.id;
this.$router.push('/orderitems/' + id);
},
editOrderClick(id){
if (this.isSelected(id)) {
var index = this.selectedItems.indexOf(id);
if (index !== -1) {
this.selectedItems.splice(index, 1);
}
} else {
this.selectedItems.push(id);
}
},
isSelected(id) {
return this.selectedItems.includes(id);
}
}
}
</script>
<template>
<div class="container mt-4">
<div class="order-list" v-if="items.length > 0">
<h3 class="mb-4">Заказы</h3>
<div class="card mb-3 order-card">
<div class="card-body" v-for="(item, index) in this.items" :id = "'item - ' + item.id"
@click="editOrderClick(item.id)"
@dblclick="editOrderDblClick(item)"
:class="{selected: isSelected(item.id)}">
<div class="row align-items-center">
<div class="col-md-3">
<div class="order-date">
<i class="far fa-calendar-alt me-2"></i>
<span>{{ item.order_date }}</span>
</div>
</div>
<div class="col-md-3">
<div class="order-total">
<strong>Сумма:</strong>
<span>{{ item.total_amount }} </span>
</div>
</div>
<div class="col-md-3">
<div class="order-status">
<span class="badge bg-success">{{ item.status }}</span>
</div>
</div>
<div class="col-md-3">
<button class="btn btn-outline-primary btn-sm" @click="editOrderDblClick(item.id)">Подробнее</button>
</div>
</div>
</div>
</div>
</div>
<div v-else class="order-empty">
<p>Нет активных заказов</p>
</div>
</div>
</template>
<style scoped>
.order-card {
transition: all 0.3s ease;
border-radius: 10px;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}
.order-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
}
.order-date {
color: #666;
}
.order-total {
font-size: 1.1em;
}
.order-status .badge {
padding: 8px 12px;
font-size: 0.9em;
}
.selected {
background-color: color-mix(in srgb, #c41e3a 50%, #383838);
opacity: 80%;
}
.selected * {
color: white;
}
.selected .btn-outline-primary {
color: white;
border-color: white;
}
.selected .btn-outline-primary:hover {
background-color: white;
color: #c41e3a;
}
.order-empty {
text-align: center;
color: #6c757d;
font-style: italic;
}
</style>

View File

@ -0,0 +1,153 @@
<script>
import DataService from '../services/DataService';
import CatalogMixins from '../mixins/CatalogMixins.js';
export default {
mixins: [
CatalogMixins
],
data() {
return {
getAllUrl: 'order_items/get_all_order_items_parts',
headers: [
{ name: 'count', label: 'Количество' },
{ name: 'price', label: 'Цена' },
{ name: 'name_part', label: 'Название детали' }
],
path: '/public/Part.jpg'
}
},
beforeCreate() {
if(document.getElementById("user").innerText == "Войти" && localStorage.getItem("token") != null)
document.getElementById("user").innerText = "Выход (" + localStorage.getItem("user").substr(0,5) +"...)";
},
methods: {
getItems() {
DataService.getData(`orders/${this.getId()}`).then(data => {
this.data = data;
this.data.order_date = this.ChangeFormat(this.data.order_date);
});
DataService.getData(`${this.getAllUrl}/${this.getId()}`).then(data => {
this.items = data;
});
},
ChangeFormat(date){
var vde = date.replace(/(\d*)-(\d*)-(\d*)/, '$3.$2.$1').split(".");
var i, de;
const options = {year: 'numeric', month: 'long', day: 'numeric'};
for(i = 0; i < vde.length; i++)
vde[i] = parseInt(vde[i]);
de = new Date(vde[2], vde[1] - 1, vde[0]);
return de.toLocaleString("ru", options);
},
getId(){
var ph, id;
ph = this.$route.path.replace('/orderitems/', '');
if(!isNaN(parseInt(ph)))
id = parseInt(ph);
else
throw "Неверный id!";
return id;
},
deleteOrder(){
if (confirm('Вы действительно хотите отменить заказ?')) {
DataService.delete(`orders/${this.getId()}`).then((data) => {
this.getItems();
this.$router.push("/orders");
});
}
}
}
}
</script>
<template>
<div style="display: flex; justify-content: space-between; gap: 20px;">
<div style="flex: 1;">
<div class="container">
<div class="info-row">
<span class="label">Дата:</span>
<span class="value">{{ data.order_date }}</span>
</div>
<div class="info-row">
<span class="label">Сумма:</span>
<span class="value">{{ data.total_amount }} </span>
</div>
<div class="info-row">
<span class="label">Статус:</span>
<span class="status status-completed" v-if="data.status === '2'">Доставлен</span>
<span class="status status-pending" v-if="data.status === '1'">В обработке</span>
<span class="status status-cancelled" v-if="data.status === '0'">Собирается</span>
</div>
</div>
</div>
<div style="display: flex; align-items: center;">
<button class="btn btn-danger order-btn" @click="deleteOrder">Отменить заказ</button>
</div>
</div>
<h1 align="center"> Запчасти </h1>
<DataCard
:headers="this.headers"
:items="this.items"
:selectedItems="this.selectedItems"
:imagePath="this.path">
</DataCard>
</template>
<style scoped>
.container {
font-family: Arial, sans-serif;
max-width: 400px;
margin: 20px;
padding: 15px;
border: 1px solid #ddd;
border-radius: 5px;
}
.info-row {
display: flex;
justify-content: space-between;
margin-bottom: 10px;
align-items: center;
}
.label {
color: #666;
font-size: 14px;
}
.value {
font-weight: bold;
font-size: 16px;
}
.status {
padding: 5px 10px;
border-radius: 15px;
font-size: 14px;
font-weight: bold;
}
.status-collecting {
background-color: #fff3cd;
color: #856404;
}
.status-in-transit {
background-color: #cce5ff;
color: #004085;
}
.status-delivered {
background-color: #d4edda;
color: #155724;
}
.order-btn {
width: 100%;
padding: 15px;
font-size: 1.2rem;
margin-top: 20px;
}
</style>

View File

@ -0,0 +1,61 @@
<script>
import DataService from '../services/DataService';
import CatalogMixins from '../mixins/CatalogMixins.js';
export default {
mixins: [
CatalogMixins
],
data() {
return {
getAllUrl: 'parts/get_all_parts/',
headers: [
{ name: 'name', label: 'Имя запчасти'},
{ name: 'price', label: 'Цена'}
],
path: '/public/Part.jpg'
}
},
beforeCreate() {
if(document.getElementById("user").innerText == "Войти" && localStorage.getItem("token") != null)
document.getElementById("user").innerText = "Выход (" + localStorage.getItem("user").substr(0,5) +"...)";
},
methods: {
getItems() {
DataService.getData(`${this.getAllUrl}${this.getId()}`).then(data => {
this.items = data;
});
},
getId(){
var ph, id;
ph = this.$route.path.replace('/parts/', '');
if(!isNaN(parseInt(ph)))
id = parseInt(ph);
else
throw "Неверный id!";
return id;
},
editPartDblClick(item){
var id = -1;
if(typeof(item) === "number")
id = item;
else
id = item.id;
this.$router.push('/part/' + id);
}
}
}
</script>
<template>
<div class="container">
<h1 align="center"> Запчасти </h1>
<DataCard
:headers="this.headers"
:items="this.items"
:selectedItems="this.selectedItems"
:imagePath="this.path"
@dblclick="editPartDblClick">
</DataCard>
</div>
</template>

View File

@ -0,0 +1,216 @@
<script>
import DataService from '../services/DataService';
import CatalogMixins from '../mixins/CatalogMixins.js';
import Review from './Review.vue'
export default {
components: {
Review
},
mixins: [
CatalogMixins
],
data() {
return {
getAllUrl: 'parts/get_part_manufacturer/',
dataUrl: 'parts/',
headers: [
{ name: 'name', label: 'Название характеристики' },
{ name: 'description', label: 'Описание' }
],
path: '/public/Part.jpg',
features: [],
part_ids: [],
cart_ids: [],
manufacturerName: "",
country: "",
ifUser: false,
count: 0
}
},
beforeCreate() {
if(document.getElementById("user").innerText == "Войти" && localStorage.getItem("token") != null)
document.getElementById("user").innerText = "Выход (" + localStorage.getItem("user").substr(0,5) +"...)";
},
methods: {
getItems() {
if (localStorage.getItem("token") != null)
this.ifUser = true;
DataService.getData(`${this.getAllUrl}${this.getId()}`).then(data => {
this.data = data;
this.features = this.data["feature"]["features"];
this.manufacturerName = this.data["manufacturer"]["name"];
this.country = this.data["manufacturer"]["country"];
});
DataService.getData("parts").then(data => {
var items = data, i;
for (i = 0; i < items.length; i++)
this.part_ids.push(items[i]["id"]);
});
DataService.getData("cart").then(data => {
var items = data, i;
for (i = 0; i < items.length; i++)
this.cart_ids.push(items[i]["id"]);
});
},
getId(){
var ph, id;
ph = this.$route.path.replace('/part/', '');
if(!isNaN(parseInt(ph)))
id = parseInt(ph);
else
throw "Неверный id!";
return id;
},
getMaxId(){
var i, maxId = -1, cartMaxId = -1;
if (this.part_ids.length > 0)
for (i = 0; i < this.part_ids.length; i++)
if (maxId < this.part_ids[i])
maxId = this.part_ids[i];
if (this.cart_ids.length > 0)
for (i = 0; i < this.cart_ids.length; i++)
if (cartMaxId < this.cart_ids[i])
cartMaxId = this.cart_ids[i];
maxId = maxId > cartMaxId ? maxId : cartMaxId;
return maxId;
},
addCart() {
var part = {id: this.data.id, name: this.data.name, description: this.data.description,
price: this.data.price, category_id: this.getId(), manufacturer_id: this.data["manufacturer"]["id"]};
DataService.create(`cart?id=${this.getMaxId() + 1}&count=${this.count}&feature={"part": ${JSON.stringify(part)}}`)
.then((data) => {
this.$router.push("/cart");
});
},
showEditCart() {
this.modal.header = 'Добавить запчасть в корзину';
this.modal.confirm = 'Добавить';
this.modalShow = true;
}
}
}
</script>
<template>
<div class="container">
<div class="image-container">
<img :src="path" alt="Part Image" class="product-image">
</div>
<div class="form-container">
<div class="form-group">
<label for="Name" class="form-label">Название запчасти</label>
<input type="text" class="form-control" id="Name" readonly v-model="data.name">
</div>
<div class="form-group">
<label for="Description" class="form-label">Описание</label>
<textarea class="form-control" id="Description" readonly v-model="data.description"></textarea>
</div>
<div class="form-group">
<label for="Price" class="form-label">Цена</label>
<input type="number" class="form-control" id="Price" readonly v-model="data.price">
</div>
<div class="form-group">
<label for="Manufacturer" class="form-label">Имя производителя</label>
<input type="text" class="form-control" id="Manufacturer" readonly v-model="this.manufacturerName">
</div>
<div class="form-group">
<label for="Country" class="form-label">Страна</label>
<input type="text" class="form-control" id="Country" readonly v-model="this.country">
</div>
<button class="add-to-cart-btn" @click.prevent="showEditCart" v-if="this.ifUser">Добавить в корзину</button>
</div>
</div>
<h2 align="center"> Характеристики </h2>
<DataTable
:headers="this.headers"
:items="this.features"
:selectedItems="this.selectedItems">
</DataTable>
<Review></Review>
<Modal
:header="this.modal.header"
:confirm="this.modal.confirm"
v-model:visible="this.modalShow"
@done="addCart">
<div class="col-mb-3">
<label for="Count" class="form-label">Количество</label>
<input type="number" class="form-control" id="Count" step="1" min="1" required v-model="this.count">
</div>
</Modal>
</template>
<style scoped>
.container {
display: flex;
padding: 20px;
max-width: 1200px;
margin: 0 auto;
gap: 30px;
}
.image-container {
flex: 1;
height: 100%;
}
.product-image {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
.form-container {
flex: 1;
display: flex;
flex-direction: column;
}
.form-group {
margin-bottom: 15px;
}
.form-label {
display: block;
margin-bottom: 5px;
font-weight: 500;
color: #333;
}
.form-control {
width: 100%;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.form-control:focus {
outline: none;
border-color: #80bdff;
box-shadow: 0 0 0 0.2rem rgba(0,123,255,.25);
}
.add-to-cart-btn {
background-color: #dc3545;
color: white;
border: none;
padding: 12px 24px;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
font-weight: 500;
transition: background-color 0.3s ease;
}
.add-to-cart-btn:hover {
background-color: #c82333;
}
</style>

View File

@ -0,0 +1,49 @@
<script>
import CatalogMixins from '../mixins/CatalogMixins.js';
export default {
mixins: [
CatalogMixins
],
props: {
parts: Array
},
data() {
return {
headers: [
{ name: 'name', label: 'Имя запчасти'},
{ name: 'price', label: 'Цена'}
],
path: '/public/Part.jpg'
}
},
beforeCreate() {
if(document.getElementById("user").innerText == "Войти" && localStorage.getItem("token") != null)
document.getElementById("user").innerText = "Выход (" + localStorage.getItem("user").substr(0,5) +"...)";
},
methods: {
getItems(){},
editPartDblClick(item){
var id = -1;
if(typeof(item) === "number")
id = item;
else
id = item.id;
this.$router.push('/part/' + id);
}
}
}
</script>
<template>
<div class="container">
<h1 align="center"> Запчасти </h1>
<DataCard
:headers="this.headers"
:items="this.parts"
:selectedItems="this.selectedItems"
:imagePath="this.path"
@dblclick="editPartDblClick">
</DataCard>
</div>
</template>

View File

@ -0,0 +1,120 @@
<script>
import DataService from '../services/DataService';
import CatalogMixins from '../mixins/CatalogMixins.js';
import ReviewCard from './ReviewCard.vue'
export default {
components: {
ReviewCard
},
mixins: [
CatalogMixins
],
data() {
return {
getAllUrl: 'reviews/get_all_reviews/',
dataUrl: 'reviews/',
path: '/public/Part.jpg',
ifUser: false
}
},
beforeCreate() {
if(document.getElementById("user").innerText == "Войти" && localStorage.getItem("token") != null)
document.getElementById("user").innerText = "Выход (" + localStorage.getItem("user").substr(0,5) +"...)";
},
methods: {
getItems() {
if (localStorage.getItem("token") != null)
this.ifUser = true;
DataService.getData(`${this.getAllUrl}${this.getId()}`).then(data => {
this.items = data;
});
},
getId(){
var ph, id;
ph = this.$route.path.replace('/part/', '');
if(!isNaN(parseInt(ph)))
id = parseInt(ph);
else
throw "Неверный id!";
return id;
},
addReview(){
this.isEdit = false;
this.data = this.items;
this.modal.header = 'Добавить отзыв';
this.modal.confirm = 'Добавить';
this.modalShow = true;
},
Save(data, md){
var partId = this.getId();
if(md == "create")
return `reviews?comment=${data.comment}&rating=${data.rating}&part_id=${partId}`;
}
}
}
</script>
<template>
<h2 align="center"> Отзывы </h2>
<div class="container">
<div class="button-group mb-4">
<button class="btn btn-success me-2" @click="addReview" v-if="this.ifUser">
<i class="bi bi-plus-circle me-2"></i>
Добавить отзыв
</button>
<button class="btn btn-danger" @click="removeSelectedItems" v-if="this.ifUser">
<i class="bi bi-trash me-2"></i>
Удалить отзыв
</button>
</div>
<ReviewCard
:items="this.items"
:selectedItems="this.selectedItems"
:imagePath="this.path">
</ReviewCard>
<Modal
:header="this.modal.header"
:confirm="this.modal.confirm"
v-model:visible="this.modalShow"
@done="saveItem">
<div class="col-mb-3">
<label for="comment" class="form-label">Комментарий</label>
<textarea class="form-control" id="comment" rows="3" required v-model="data.comment"></textarea>
</div>
<div class="col-mb-3">
<label for="Rating" class="form-label">Рейтинг</label>
<input type="number" class="form-control" id="Rating" step="1" min="1" max="10" required v-model="data.rating">
</div>
</Modal>
</div>
</template>
<style scoped>
.button-group {
max-width: 800px;
margin: 20px auto;
padding: 0 20px;
}
.btn {
padding: 10px 20px;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.btn-success {
background-color: #28a745;
border: none;
}
.btn-danger {
background-color: #dc3545;
border: none;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
}
</style>

View File

@ -0,0 +1,133 @@
<script>
export default {
props: {
items: Array,
selectedItems: Array,
imagePath: String
},
emits: {
dblclick: null
},
methods: {
cardClick(id) {
if (this.isSelected(id)) {
var index = this.selectedItems.indexOf(id);
if (index !== -1) {
this.selectedItems.splice(index, 1);
}
} else {
this.selectedItems.push(id);
}
},
isSelected(id) {
return this.selectedItems.includes(id);
}
}
}
</script>
<template>
<div class="reviews-container">
<div v-for="(item, index) in this.items" class="review-card" :id = "'item - ' + item.id"
@click="cardClick(item.id)"
:class="{selected: isSelected(item.id)}">
<div class="review-left">
<img :src="imagePath" alt="Avatar" class="user-avatar">
</div>
<div class="review-right">
<div class="rating">
<span class="rating-number">{{ item["rating"] }}</span>
</div>
<div class="comment">
{{ item["comment"] }}
</div>
<div class="user-info">
<span class="username">{{ item["user_name"] }}</span>
<span class="email">{{ item["email"] }}</span>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.reviews-container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.review-card {
display: flex;
margin-bottom: 20px;
padding: 20px;
border-radius: 10px;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}
.selected {
background-color: color-mix(in srgb, #ffffff 90%, #808080);
opacity: 80%;
}
.review-left {
flex: 0 0 150px;
margin-right: 20px;
}
.user-avatar {
width: 150px;
height: 150px;
border-radius: 50%;
object-fit: cover;
}
.review-right {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.rating {
display: flex;
justify-content: flex-start;
margin-bottom: 15px;
}
.rating-number {
width: 50px;
height: 50px;
display: flex;
align-items: center;
justify-content: center;
border: 3px solid #333;
border-radius: 50%;
font-size: 24px;
font-weight: bold;
}
.comment {
flex: 1;
text-align: center;
margin: 15px 0;
font-size: 16px;
line-height: 1.5;
}
.user-info {
display: flex;
align-items: center;
gap: 15px;
margin-top: 15px;
}
.username {
font-weight: bold;
color: #333;
}
.email {
color: #666;
}
</style>

View File

@ -0,0 +1,348 @@
<script>
import DataService from '../services/DataService';
export default {
methods: {
formatPhoneNumber(e) {
let phoneNumber = e.target.value.replace(/\D/g, '');
if (phoneNumber.length > 0) {
if (phoneNumber[0] === '8') {
phoneNumber = '7' + phoneNumber.slice(1);
} else if (phoneNumber[0] !== '7') {
phoneNumber = '7' + phoneNumber;
}
}
phoneNumber = phoneNumber.substring(0, 11);
let formattedPhone = '';
if (phoneNumber.length > 0) {
formattedPhone = '+7';
if (phoneNumber.length > 1) {
formattedPhone += ' (' + phoneNumber.substring(1, 4);
}
if (phoneNumber.length > 4) {
formattedPhone += ') ' + phoneNumber.substring(4, 7);
}
if (phoneNumber.length > 7) {
formattedPhone += '-' + phoneNumber.substring(7, 9);
}
if (phoneNumber.length > 9) {
formattedPhone += '-' + phoneNumber.substring(9, 11);
}
}
this.data.phone = formattedPhone;
},
getUserInfo () {
if (!localStorage.getItem("token")){
localStorage.removeItem("user");
}
},
async login (login, password) {
const requestParams = {
method: "GET",
headers: {
"Content-Type": "application/json",
}
};
var str = `?email=${login}&password=${password}`;
const response = await fetch(this.hostURL + "/users/getuser" + str, requestParams);
var result = await response.text();
if (response.status === 200) {
result = JSON.parse(result);
if (result["id"] == 1)
localStorage.setItem("id", result["id"]);
localStorage.setItem("token", result["token"]);
localStorage.setItem("user", login);
this.$router.push("/categories");
location.reload();
window.location.href = "/categories";
} else {
if (localStorage.getItem("id") != null)
localStorage.removeItem("id");
localStorage.removeItem("token");
localStorage.removeItem("user");
document.getElementById('errorSignIn').hidden = false;
alert(result);
}
},
loginForm () {
this.login(this.loginInput.value, this.passwordInput.value).then(() => {
this.loginInput.value = "";
this.passwordInput.value = "";
this.getUserInfo();
});
},
async createUser() {
const requestParams = {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(this.data)
};
var data = this.data; var response_json = "";
var str = `?email=${data.email}&name=${data.name}&phone=${data.phone}&password_hash=${data.password_hash}&address=${data.address}&role=1`;
const response = await fetch(this.hostURL + "/users" + str, requestParams);
if (response.text !== "error") {
var result = await response.text();
response_json = JSON.parse(result);
localStorage.setItem("token", response_json["token"]);
document.getElementById('tab-1').checked = true;
} else {
document.getElementById('error').hidden = false;
}
}
},
data() {
return {
hostURL: "http://localhost:5001",
loginInput: undefined,
passwordInput: undefined,
loginButton: undefined,
data: {}
}
},
mounted() {
this.loginInput = document.getElementById("email");
this.passwordInput = document.getElementById("password");
this.loginButton = document.getElementById("loginBtn");
this.getUserInfo();
}
}
</script>
<template>
<div class="login-wrap">
<div class="login-html">
<input id="tab-1" type="radio" name="tab" class="sign-in" checked><label for="tab-1" class="tab">Вход</label>
<input id="tab-2" type="radio" name="tab" class="sign-up"><label for="tab-2" class="tab">Регистрация</label>
<div class="login-form">
<form id="loginForm" @submit.prevent="loginForm" ref="form">
<div class="sign-in-htm">
<div class="group form-label">
<label for="email" class="label">Электронная почта</label>
<p><span class="fontawesome-user badge badge-secondary"></span><input id="email" type="text" class="input form-control" required></p>
</div>
<div class="group">
<label for="password" class="label form-label">Пароль</label>
<p><span class="fontawesome-lock badge badge-secondary"></span><input id="password" type="password" class="input form-control" required></p>
</div>
<div class="group">
<input id="loginBtn" type="submit" class="btn button" value="Войти">
</div>
<div id="errorSignIn" hidden class="alert alert-danger" role="alert">
Пользователь не найден!
</div>
</div>
</form>
<form @submit.prevent="createUser" ref="form">
<div class="sign-up-htm">
<div class="group">
<label for="userEmail" class="label form-label">Электронная почта</label>
<p><span class="fontawesome-user badge badge-secondary"></span><input id="userEmail" type="text" class="input form-control" required autofocus maxlength="64" v-model="data.email"></p>
</div>
<div class="group">
<label for="userName" class="label form-label">ФИО</label>
<p><span class="fontawesome-user badge badge-secondary"></span><input id="userName" type="text" class="input form-control" required autofocus maxlength="64" v-model="data.name"></p>
</div>
<div class="group">
<label for="password_hash" class="label form-label">Пароль</label>
<p><span class="fontawesome-lock badge badge-secondary"></span><input id="password_hash" type="password" class="input form-control" required minlength="6" maxlength="64" v-model="data.password_hash"></p>
</div>
<div class="group">
<label for="userPhone" class="label form-label">Телефон</label>
<p><span class="fontawesome-user badge badge-secondary"></span><input id="userPhone" type="text" class="input form-control" required autofocus maxlength="20" v-model="data.phone" @input="formatPhoneNumber"></p>
</div>
<div class="group">
<label for="userAddress" class="label form-label">Адрес</label>
<p><span class="fontawesome-lock badge badge-secondary"></span><input id="userAddress" type="text" class="input form-control" required minlength="6" maxlength="64" v-model="data.address"></p>
</div>
<div class="group">
<input type="submit" class="btn button" value="Зарегистрироваться">
</div>
<div class="hr"></div>
<div class="foot-lnk">
<label for="tab-1">Уже зарегистрирован?</label>
</div>
<div id="errorSignUp" hidden class="alert alert-danger" role="alert">
Error!
</div>
</div>
</form>
</div>
</div>
</div>
</template>
<style scoped>
[class*="fontawesome-"]:before {
font-family: 'FontAwesome', sans-serif;
}
input[type="tel"] {
padding: 8px;
font-size: 16px;
border: 1px solid #ddd;
border-radius: 4px;
width: 200px;
}
input[type="tel"]:focus {
outline: none;
border-color: #007bff;
box-shadow: 0 0 0 2px rgba(0,123,255,.25);
}
input[type="tel"]::placeholder {
color: #999;
}
.login-wrap{
width:100%;
margin:auto;
max-width:800px;
min-height:696px;
position:relative;
box-shadow:0 12px 15px 0 rgba(0,0,0,.24),0 17px 50px 0 rgba(0,0,0,.19);
}
.login-html{
width:100%;
height:100%;
position:absolute;
padding:90px 70px 50px 70px;
background-color: #2c3338;
color: #606468;
}
.login-html .sign-in-htm,
.login-html .sign-up-htm{
top:0;
left:0;
right:0;
bottom:0;
position:absolute;
transform:rotateY(180deg);
backface-visibility:hidden;
transition:all .4s linear;
}
.login-html .sign-in,
.login-html .sign-up,
.login-form .group .check{
display:none;
}
.login-html .tab,
.login-form .group .label,
.login-form .group .button{
text-transform:uppercase;
}
.login-html .tab{
font-size:22px;
margin-right:15px;
padding-bottom:5px;
margin:0 15px 10px 0;
display:inline-block;
border-bottom:2px solid transparent;
}
.login-html .sign-in:checked + .tab,
.login-html .sign-up:checked + .tab{
color:#fff;
border-color:#ea4c88;
}
.login-form{
min-height:345px;
position:relative;
perspective:1000px;
transform-style:preserve-3d;
}
.login-form .group{
margin-bottom:15px;
}
.login-form .group .label{
width:100%;
color:#fff;
display:block;
}
.login-form .group .button{
border:none;
padding:15px 20px;
border-radius:25px;
}
.login-form .group .label{
color:#aaa;
font-size:12px;
}
.login-html .sign-in:checked + .tab + .sign-up + .tab + .login-form .sign-in-htm{
transform:rotate(0);
}
.login-html .sign-up:checked + .tab + .login-form .sign-up-htm{
transform:rotate(0);
}
.form-control:focus {
box-shadow: 0 0 0 0.2rem rgba(234, 76, 136, 0.25);
}
.login-form .group span {
background-color: #363b41;
border-radius: 3px 0px 0px 3px;
-moz-border-radius: 3px 0px 0px 3px;
-webkit-border-radius: 3px 0px 0px 3px;
color: #606468;
display: block;
float: left;
height: 50px;
line-height: 50px;
text-align: center;
width: 8%;
}
.login-form .group .input {
border: none;
font-family: 'Open Sans', Arial, sans-serif;
font-size: 14px;
line-height: 1.5em;
padding: 0;
-webkit-appearance: none;
height: 50px;
outline:none;
}
.login-form .group .input {
background-color: #3b4148;
border-radius: 0px 3px 3px 0px;
-moz-border-radius: 0px 3px 3px 0px;
-webkit-border-radius: 0px 3px 3px 0px;
color: #606468;
margin-bottom: 1em;
padding: 0 16px;
width: 92%;
}
.login-form .group .input:focus {
color:#fff;
}
.login-form .group .button {
border-radius: 3px;
-moz-border-radius: 3px;
-webkit-border-radius: 3px;
background-color: #ea4c88;
color: #eee;
font-weight: bold;
text-transform: uppercase;
cursor:pointer;
width: 100%;
}
.login-form .group .button:hover {
background-color: #d44179;
}
</style>

View File

@ -0,0 +1,95 @@
<script>
export default {
props: {
},
emits: {
add: null,
edit: null,
remove: null
},
methods: {
add() {
this.$emit('add');
},
edit() {
this.$emit('edit');
},
remove() {
this.$emit('remove');
}
}
}
</script>
<template>
<div class="btn-group mt-3" role="group">
<button type="button" class="btn btn-custom btn-gray" @click.prevent="add">
<i class="fas fa-plus-circle me-2"></i>Добавить
</button>
<button type="button" class="btn btn-custom btn-blue" @click.prevent="edit">
<i class="fas fa-edit me-2"></i>Изменить
</button>
<button type="button" class="btn btn-custom btn-red" @click.prevent="remove">
<i class="fas fa-trash-alt me-2"></i>Удалить
</button>
</div>
</template>
<style scoped>
.btn {
min-width: 140px;
}
.btn-group {
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
border-radius: 8px;
overflow: hidden;
}
.btn-custom {
padding: 10px 20px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
transition: all 0.3s ease;
border: none;
}
.btn-red {
background-color: #8B0000;
color: white;
}
.btn-blue {
background-color: #00008B;
color: white;
}
.btn-gray {
background-color: #4A4A4A;
color: white;
}
.btn-custom:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
.btn-red:hover {
background-color: #A52A2A;
}
.btn-blue:hover {
background-color: #0000CD;
}
.btn-gray:hover {
background-color: #696969;
}
.btn-custom:active {
transform: translateY(0);
box-shadow: none;
}
</style>

30
front/src/main.js Normal file
View File

@ -0,0 +1,30 @@
import { createApp } from 'vue'
import { createRouter, createWebHistory } from 'vue-router'
import './style.css'
import App from './App.vue'
import SignUpIn from "./components/SignUpIn.vue"
import Category from "./components/Category.vue"
import Part from "./components/Part.vue"
import PartDefine from "./components/PartDefine.vue"
import Cart from "./components/Cart.vue"
import Order from "./components/Order.vue"
import OrderItems from "./components/OrderItems.vue"
const routes = [
{ path: '/', redirect: '/categories' },
{ path: '/categories', component: Category, meta: { label: 'Категории' } },
{ path: '/orders', component: Order, meta: { label: 'Заказы' } },
{ path: '/cart', component: Cart, meta: { label: 'Корзина' } },
{ path: '/orderitems/:orderId(\\d+)', component: OrderItems },
{ path: '/parts/:categoryId(\\d+)', component: Part },
{ path: '/part/:partId(\\d+)', component: PartDefine },
{ path: '/signupin', component: SignUpIn}
]
const router = createRouter({
history: createWebHistory(),
linkActiveClass: 'active',
routes
})
createApp(App).use(router).mount('#app')

View File

@ -0,0 +1,101 @@
import ToolBar from '../components/ToolBar.vue';
import DataCard from '../components/DataCard.vue';
import DataTable from '../components/DataTable.vue';
import Modal from '../components/Modal.vue';
import DataService from '../services/DataService';
const CatalogMixin = {
components: {
ToolBar, Modal, DataCard, DataTable
},
data() {
return {
getAllUrl: undefined,
dataUrl: undefined,
headers: [],
items: [],
selectedItems: [],
modal: {
header: undefined,
confirm: undefined,
},
modalShow: false,
data: undefined,
isEdit: false
}
},
created() {
this.getItems();
this.data = this.items;
},
methods: {
showAddModal() {
this.isEdit = false;
this.data = this.items;
this.modal.header = 'Добавление элемента';
this.modal.confirm = 'Добавить';
this.modalShow = true;
},
showEditModal() {
if (this.selectedItems.length === 0) {
return;
}
this.showEditModalDblClick(this.selectedItems[0]);
},
showEditModalDblClick(editId) {
if(typeof(editId) != 'number')
editId = editId.id;
DataService.getData(this.dataUrl + editId)
.then(data => {
this.data = data;
this.isEdit = true;
this.modal.header = 'Редактирование элемента';
this.modal.confirm = 'Сохранить';
this.modalShow = true;
});
},
saveItem() {
var dataUrlSave = '';
if (!this.isEdit) {
dataUrlSave = this.Save(this.data, "create");
DataService.create(dataUrlSave)
.then(() => {
this.getItems();
});
} else {
dataUrlSave = this.Save(this.data, "update");
DataService.update(this.dataUrl + this.data.id, dataUrlSave)
.then(() => {
this.getItems();
document.querySelectorAll("div").forEach(div => {
div.classList.remove("selected");
});
});
}
},
removeSelectedItems() {
if (this.selectedItems.length === 0) {
return;
}
if (confirm('Удалить выбранные элементы?')) {
const promises = [];
const self = this;
this.selectedItems.forEach(item => {
promises.push(DataService.delete(this.dataUrl + item));
});
Promise.all(promises).then((results) => {
results.forEach(function (id) {
const index = self.selectedItems.indexOf(id);
if (index === - 1) {
return;
}
self.selectedItems.splice(index, 1);
});
this.getItems();
});
}
}
}
}
export default CatalogMixin;

View File

@ -0,0 +1,58 @@
export default class DataService {
static host = "http://localhost:5001/";
static async getData(str){
var response, data, requestParams;
if (localStorage.getItem("token") != null)
requestParams = {
method: "GET",
headers: {
"Authorization": "Bearer " + localStorage.getItem("token")
}
};
else
requestParams = {
method: "GET"
};
response = await fetch(this.host + str, requestParams);
data = await response.json();
return data;
}
static async create(str) {
const requestParams = {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer " + localStorage.getItem("token")
}
};
const response = await fetch(this.host + str, requestParams);
return await response.json();
}
static async update(str, values) {
const requestParams = {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
"Authorization": "Bearer " + localStorage.getItem("token")
},
body:JSON.stringify(values)
};
const response = await fetch(this.host + str, requestParams);
return await response.json();
}
static async delete(str) {
var response;
const requestParams = {
method: "DELETE",
headers: {
"Authorization": "Bearer " + localStorage.getItem("token")
}
};
response = await fetch(this.host + str, requestParams);
return await response.json();
}
}

58
front/src/style.css Normal file
View File

@ -0,0 +1,58 @@
header nav {
background-color: #3c3c3c;
}
@media (min-width: 768px) {
header nav {
height: 64px;
}
}
header nav a:hover {
text-decoration: underline;
}
footer {
background-color: #9c9c9c;
height: 32px;
color: #ffffff;
}
h1 {
font-size: 1.5em;
}
h2 {
font-size: 1.25em;
}
h3 {
font-size: 1.1em;
}
.navbar-brand {
font-weight: bold;
font-size: 1.5rem;
color: grey !important;
background-color: #f8f9fa;
padding: 5px 15px;
border-radius: 20px;
border: 2px solid red;
}
.nav-link {
font-size: 1.1rem;
}
.btn-auth {
background-color: #FF0000;
border-color: #FF0000;
transition: all 0.3s ease;
border-radius: 25px;
padding: 8px 20px;
}
.btn-auth:hover {
background-color: #003d82;
border-color: #003d82;
}

7
front/vite.config.js Normal file
View File

@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()]
})

26
front_admin/.gitignore vendored Normal file
View File

@ -0,0 +1,26 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
.parcel-cache

17
front_admin/index.html Normal file
View File

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<script type="module" src="./node_modules/bootstrap/dist/js/bootstrap.min.js"></script>
<link rel="stylesheet" href="/node_modules/bootstrap/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="/node_modules/@fortawesome/fontawesome-free/css/all.min.css">
<title>Интернет-магазин автозапчастей</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

1354
front_admin/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

21
front_admin/package.json Normal file
View File

@ -0,0 +1,21 @@
{
"name": "internet-auto-parts-store",
"private": true,
"version": "1.0.0",
"type": "module",
"scripts": {
"start": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@fortawesome/fontawesome-free": "^6.2.1",
"bootstrap": "^5.2.2",
"vue": "^3.2.41",
"vue-router": "^4.1.6"
},
"devDependencies": {
"vite": "^3.2.3",
"@vitejs/plugin-vue": "^3.2.0"
}
}

BIN
front_admin/public/Part.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 609 KiB

19
front_admin/src/App.vue Normal file
View File

@ -0,0 +1,19 @@
<script>
import Header from './components/Header.vue';
export default {
components: {
Header
}
}
</script>
<template>
<Header></Header>
<div class="container">
<router-view></router-view>
</div>
</template>
<style scoped>
</style>

View File

@ -0,0 +1,73 @@
<script>
import DataService from '../services/DataService';
import CatalogMixins from '../mixins/CatalogMixins.js';
export default {
mixins: [
CatalogMixins
],
data() {
return {
getAllUrl: 'categories',
dataUrl: 'categories/',
headers: [
{ name: 'name', label: 'Имя категории'},
{ name: 'description', label: 'Описание'}
],
path: '/public/Part.jpg'
}
},
beforeCreate() {
if(localStorage.getItem("token") == null) {
this.$router.push("/signin");
}
else
if(document.getElementById("user") != null)
document.getElementById("user").innerText = "Выход (" + localStorage.getItem("user").substr(0,5) +"...)";
},
methods: {
getItems() {
if(localStorage.getItem("token") != null){
DataService.getData(this.getAllUrl).then(data => {
this.items = data;
});
}
},
addCategory(){
this.$router.push("/createcategory");
},
editCategory(){
if (this.selectedItems.length === 0) {
return;
}
this.editCategoryDblClick(this.selectedItems[0]);
},
editCategoryDblClick(item){
var id = -1;
if(typeof(item) === "number")
id = item;
else
id = item.id;
this.$router.push('updatecategory/' + id);
}
}
}
</script>
<template>
<div class="container">
<h1 align="center"> Каталог категорий </h1>
<DataCard
:headers="this.headers"
:items="this.items"
:selectedItems="this.selectedItems"
:imagePath="this.path"
@dblclick="editCategoryDblClick">
</DataCard>
</div>
<ToolBar
@add="addCategory"
@edit="editCategory"
@remove="removeSelectedItems">
</ToolBar>
</template>

View File

@ -0,0 +1,207 @@
<script>
import DataService from '../services/DataService';
import CatalogMixins from '../mixins/CatalogMixins.js';
export default {
mixins: [
CatalogMixins
],
data() {
return {
getAllUrl: 'categories',
dataUrl: 'categories/',
headers: [
{ name: 'name', label: 'Название характеристики' },
{ name: 'description', label: 'Описание' }
],
path: '/public/Part.jpg',
isEditCategory: false,
features: [],
name_feature: "",
description_feature: "",
id: 1
}
},
beforeCreate() {
if(localStorage.getItem("token") == null) {
this.$router.push("/signin");
}
else
if(document.getElementById("user") != null)
document.getElementById("user").innerText = "Выход (" + localStorage.getItem("user").substr(0,5) +"...)";
},
methods: {
getItems(){
var ph = this.$route.path;
if(ph.indexOf("updatecategory") > 0){
this.nameButton = "Сохранить";
DataService.getData(this.dataUrl + this.getId())
.then(data => {
this.data = data;
this.isEditCategory = true;
if (this.data.parent_category_id == null)
this.data.parent_category_id = 0;
this.features = this.data["feature"]["features"];
});
}
else
this.nameButton = "Добавить";
},
Save(data, md){
if(md == "create")
return `categories?name=${data.name}&description=${data.description}&feature={"features": ${JSON.stringify(this.features)}}&parent_category_id=${data.parent_category_id}`;
const dataUp = {name: `${data.name}`,
description: `${data.description}`,
feature: `{"features": ${JSON.stringify(this.features)}}`,
parent_category_id: data.parent_category_id
};
return dataUp;
},
getId(){
var ph, id;
ph = this.$route.path.replace('/updatecategory/', '');
if(!isNaN(parseInt(ph)))
id = parseInt(ph);
else
throw "Неверный id!";
return id;
},
async saveCategory(){
var dataUrlSave = '';
if (!this.isEditCategory) {
dataUrlSave = this.Save(this.data, "create");
DataService.create(dataUrlSave)
.then(() => {
this.$router.push("/categories");
});
}
else {
dataUrlSave = this.Save(this.data, "update");
DataService.update(this.dataUrl + this.data.id, dataUrlSave)
.then(() => {
this.$router.push("/categories");
});
}
},
AddFeature(){
this.name_feature = "";
this.description_feature = "";
this.isEdit = false;
this.modal.header = 'Добавление характеристики';
this.modal.confirm = 'Добавить';
this.modalShow = true;
},
UpdateFeature(){
if (this.selectedItems.length === 0) {
return;
}
this.UpdateFeatureDblClick(this.selectedItems[0]);
},
UpdateFeatureDblClick(editId){
var feature = this.getFeatureId(editId);
if(Object.keys(feature).length > 0){
this.id = feature["id"];
this.name_feature = feature["name"];
this.description_feature = feature["description"];
}
this.isEdit = true;
this.modal.header = 'Редактирование элемента';
this.modal.confirm = 'Сохранить';
this.modalShow = true;
},
DeleteFeatures(){
var i, j;
for (i = 0; i <= this.selectedItems.length; i++){
j = 0;
while (j < this.features.length && this.features[j]["id"] != this.selectedItems[i])
j++;
if (j < this.features.length){
this.features.splice(j, 1);
this.selectedItems.splice(i, 1);
i--;
}
}
},
saveFeature(){
var id, maxId, i = 0;
if (this.isEdit){
while (i < this.features.length && this.features[i]["id"] != this.id)
i++;
if (i < this.features.length)
this.features[i] = {id: this.id, name: this.name_feature, description: this.description_feature};
}
else {
maxId = this.getMaxId();
id = maxId > this.id ? maxId + 1 : this.id;
this.features.push({id: id, name: this.name_feature, description: this.description_feature});
this.id++;
}
this.name_feature = "";
this.description_feature = "";
},
getMaxId(){
var i, maxId = -1;
if (this.features.length > 0)
for (i = 0; i < this.features.length; i++)
if (maxId < this.features[i]["id"])
maxId = this.features[i]["id"];
return maxId;
},
getFeatureId(featureId){
var i = 0;
while (i < this.features.length && this.features[i]["id"] != featureId) i++;
if (i < this.features.length)
return this.features[i];
return {};
}
}
}
</script>
<template>
<div>
<form @submit.prevent="saveCategory" ref="form">
<div class="col-mb-3" style="margin: 0 0 15px 15px;">
<label for="Name" class="form-label">Имя категории</label>
<input type="text" class="form-control" id="Name" required v-model="data.name">
</div>
<div class="col-mb-3" style="margin: 0 0 15px 15px;">
<label for="Description" class="form-label">Описание</label>
<textarea class="form-control" id="Description" rows="3" required v-model="data.description"></textarea>
</div>
<div class="col-mb-3" style="margin: 0 0 15px 15px;">
<label for="Parent_category_id" class="form-label">Идентификатор категории</label>
<input type="number" class="form-control" id="Parent_category_id" step="1" min="0" required v-model="data.parent_category_id">
</div>
<div class="d-grid gap-2 col-mb-3" style="margin: 0 0 15px 15px;">
<button type="submit" id="save" class="btn btn-danger btn-lg">
{{ this.nameButton }}
</button>
</div>
<ToolBar
@add="AddFeature"
@edit="UpdateFeature"
@remove="DeleteFeatures">
</ToolBar>
<DataTable
:headers="this.headers"
:items="this.features"
:selectedItems="this.selectedItems"
@dblclick="UpdateFeatureDblClick">
</DataTable>
</form>
</div>
<Modal
:header="this.modal.header"
:confirm="this.modal.confirm"
v-model:visible="this.modalShow"
@done="saveFeature">
<div class="col-mb-3">
<label for="Name_feature" class="form-label">Название характеристики</label>
<input type="text" class="form-control" id="Name_feature" required v-model="this.name_feature">
</div>
<div class="col-mb-3">
<label for="Description_feature" class="form-label">Описание</label>
<input type="text" class="form-control" id="Description_feature" required v-model="this.description_feature">
</div>
</Modal>
</template>

View File

@ -0,0 +1,90 @@
<script>
export default {
props: {
headers: Array,
items: Array,
selectedItems: Array,
imagePath: String
},
emits: {
dblclick: null
},
methods: {
cardClick(id) {
if (this.isSelected(id)) {
var index = this.selectedItems.indexOf(id);
if (index !== -1) {
this.selectedItems.splice(index, 1);
}
} else {
this.selectedItems.push(id);
}
},
cardDblClick(item) {
this.$emit('dblclick', item);
},
isSelected(id) {
return this.selectedItems.includes(id);
}
}
}
</script>
<template>
<div id="div-items" class = "row" style="justify-content: center;">
<div align="justify" class = "card" v-for="(item, index) in this.items" :id = "'item - ' + item.id"
@click="cardClick(item.id)"
@dblclick="cardDblClick(item)"
:class="{selected: isSelected(item.id)}">
<img class="card-img" :src="imagePath" v-if="typeof(imagePath) === 'string'">
<img class="card-img" :src="imagePath[index]" v-else>
<p v-for="header in this.headers"><b>{{ header.label}}:</b> {{item[header.name]}} <span v-if="header.name === 'commonPrice'"></span></p>
</div>
</div>
</template>
<style scoped>
.card {
width: 20%;
height: 340px;
margin: 10px;
float: left;
text-align: center;
box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2);
transition: 0.3s;
border-radius: 20px;
overflow: hidden;
-moz-hyphens: auto;
-webkit-hyphens: auto;
-ms-hyphens: auto;
}
.card-img{
margin-top: 4px;
border-radius: 20px;
width: 100%;
height: 100%;
}
.card:hover {
box-shadow: 0 8px 16px 0 rgba(0,0,0,0.2);
cursor: pointer;
}
p {
padding: 0px;
margin: 0px;
font-size: 18px;
text-align: center;
font-family: sans-serif;
font-weight: 500;
}
.selected {
background-color: color-mix(in srgb, #c41e3a 50%, #383838);
opacity: 80%;
}
div {
user-select: none;
}
</style>

View File

@ -0,0 +1,72 @@
<script>
export default {
props: {
headers: Array,
items: Array,
selectedItems: Array
},
emits: {
dblclick: null
},
methods: {
rowClick(id) {
if (this.isSelected(id)) {
var index = this.selectedItems.indexOf(id);
if (index !== -1) {
this.selectedItems.splice(index, 1);
}
} else {
this.selectedItems.push(id);
}
},
rowDblClick(id) {
this.$emit('dblclick', id);
},
isSelected(id) {
return this.selectedItems.includes(id);
},
dataConvert(data) {
return data;
}
}
}
</script>
<template>
<table class="table table-hover">
<thead>
<tr>
<th scope="col">#</th>
<th v-for="header in this.headers"
:id="header.name"
scope="col">{{ header.label }}</th>
</tr>
</thead>
<tbody>
<tr v-for="(item, index) in this.items"
@click="rowClick(item.id)"
@dblclick="rowDblClick(item.id)"
:class="{selected: isSelected(item.id)}">
<th scope="row">{{ index + 1 }}</th>
<td v-for="header in this.headers">
{{ dataConvert(item[header.name]) }}
</td>
</tr>
</tbody>
</table>
</template>
<style scoped>
tbody tr:hover {
cursor: pointer;
}
tr.selected {
background-color: #0000fd;
opacity: 80%;
}
tbody tr {
user-select: none;
}
</style>

View File

@ -0,0 +1,43 @@
<script>
export default {
methods: {
getRoutes() {
return this.$router.options.routes.filter(route => route.meta?.hasOwnProperty('label'));
},
logout() {
localStorage.clear();
this.$router.push('/signin');
document.getElementById("user").innerText = "Выход ()";
location.reload();
}
},
data() {
return {
token: null
}
},
created(){
this.token = localStorage.getItem("token");
}
}
</script>
<template>
<nav class="navbar navbar-expand-lg navbar-light bg-light" v-if="this.token !== null">
<div class="container">
<a class="navbar-brand" href="/categories">Internet shop parts</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item"
v-for="route in this.getRoutes()">
<router-link class="nav-link" :to="route.path">{{ route.meta.label }}</router-link>
</li>
</ul>
<a id="user" class="btn btn-auth text-white" @click.prevent="logout">Войти</a>
</div>
</div>
</nav>
</template>

View File

@ -0,0 +1,76 @@
<script>
import DataService from '../services/DataService';
import CatalogMixins from '../mixins/CatalogMixins.js';
export default {
mixins: [
CatalogMixins
],
data() {
return {
getAllUrl: 'manufacturers',
dataUrl: 'manufacturers/',
headers: [
{ name: 'name', label: 'Название производителя'},
{ name: 'country', label: 'Страна'}
],
path: '/public/Part.jpg'
}
},
beforeCreate() {
if(localStorage.getItem("token") == null) {
this.$router.push("/signin");
}
else
if(document.getElementById("user") != null)
document.getElementById("user").innerText = "Выход (" + localStorage.getItem("user").substr(0,5) +"...)";
},
methods: {
getItems() {
if(localStorage.getItem("token") != null)
DataService.getData(this.getAllUrl).then(data => {
this.items = data;
});
},
Save(data, md){
if(md == "create")
return `manufacturers?name=${data.name}&country=${data.country}`;
const dataUp = {name: `${data.name}`,
country: `${data.country}`
};
return dataUp;
}
}
}
</script>
<template>
<div class="container">
<h1 align="center"> Каталог производителей </h1>
<DataCard
:headers="this.headers"
:items="this.items"
:selectedItems="this.selectedItems"
:imagePath="this.path"
@dblclick="showEditModalDblClick">
</DataCard>
</div>
<ToolBar
@add="showAddModal"
@edit="showEditModal"
@remove="removeSelectedItems">
</ToolBar>
<Modal
:header="this.modal.header"
:confirm="this.modal.confirm"
v-model:visible="this.modalShow"
@done="saveItem">
<div class="col-mb-3">
<label for="Name" class="form-label">Имя производителя</label>
<input type="text" class="form-control" id="Name" required v-model="data.name">
</div>
<div class="col-mb-3">
<label for="Country" class="form-label">Страна</label>
<input type="text" class="form-control" id="Country" required v-model="data.country">
</div>
</Modal>
</template>

View File

@ -0,0 +1,63 @@
<script>
export default {
props: {
header: String,
confirm: String,
visible: Boolean
},
emits: {
done: null,
'update:visible': (value) => {
if (typeof value !== 'boolean') {
throw 'Value is not a boolean';
}
return true;
}
},
methods: {
hide() {
this.$emit('update:visible', false);
},
done() {
if (this.$refs.form.checkValidity()) {
this.$emit('done');
this.hide();
} else {
this.$refs.form.reportValidity();
}
}
}
}
</script>
<template>
<div class="modal fade" tabindex="-1" aria-hidden="true"
:class="{ 'modal-show': this.visible, 'show': this.visible }">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="exampleModalLabel">{{ header }}</h1>
<button type="button" class="btn-close" aria-label="Close"
@click.prevent="hide"></button>
</div>
<div class="modal-body">
<form @submit.prevent="done" ref="form">
<slot></slot>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary"
@click.prevent="hide">Закрыть</button>
<button type="button" class="btn btn-danger"
@click.prevent="done">{{ confirm }}</button>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.modal-show {
display: block;
}
</style>

View File

@ -0,0 +1,74 @@
<script>
import DataService from '../services/DataService';
import CatalogMixins from '../mixins/CatalogMixins.js';
export default {
mixins: [
CatalogMixins
],
data() {
return {
getAllUrl: 'parts',
dataUrl: 'parts/',
headers: [
{ name: 'name', label: 'Имя запчасти'},
{ name: 'description', label: 'Описание'},
{ name: 'price', label: 'Цена'}
],
path: '/public/Part.jpg'
}
},
beforeCreate() {
if(localStorage.getItem("token") == null) {
this.$router.push("/signin");
}
else
if(document.getElementById("user") != null)
document.getElementById("user").innerText = "Выход (" + localStorage.getItem("user").substr(0,5) +"...)";
},
methods: {
getItems() {
if(localStorage.getItem("token") != null){
DataService.getData(this.getAllUrl).then(data => {
this.items = data;
});
}
},
addPart(){
this.$router.push("/createpart");
},
editPart(){
if (this.selectedItems.length === 0) {
return;
}
this.editPartDblClick(this.selectedItems[0]);
},
editPartDblClick(item){
var id = -1;
if(typeof(item) === "number")
id = item;
else
id = item.id;
this.$router.push('updatepart/' + id);
}
}
}
</script>
<template>
<div class="container">
<h1 align="center"> Каталог запчастей </h1>
<DataCard
:headers="this.headers"
:items="this.items"
:selectedItems="this.selectedItems"
:imagePath="this.path"
@dblclick="editPartDblClick">
</DataCard>
</div>
<ToolBar
@add="addPart"
@edit="editPart"
@remove="removeSelectedItems">
</ToolBar>
</template>

Some files were not shown because too many files have changed in this diff Show More