Compare commits
No commits in common. "master" and "release" have entirely different histories.
162
.gitignore
vendored
162
.gitignore
vendored
@ -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
3
.idea/.gitignore
generated
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
14
.idea/Internet_auto_parts_store.iml
generated
Normal file
14
.idea/Internet_auto_parts_store.iml
generated
Normal 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>
|
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal 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
10
.idea/misc.xml
generated
Normal 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
8
.idea/modules.xml
generated
Normal 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
6
.idea/vcs.xml
generated
Normal 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>
|
28
README.md
28
README.md
@ -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
26
application/config.py
Normal 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'])
|
83
application/controllers/CartController.py
Normal file
83
application/controllers/CartController.py
Normal 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)}")
|
127
application/controllers/CategoryController.py
Normal file
127
application/controllers/CategoryController.py
Normal 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)}")
|
95
application/controllers/ManufacturerController.py
Normal file
95
application/controllers/ManufacturerController.py
Normal 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)}")
|
149
application/controllers/OrderController.py
Normal file
149
application/controllers/OrderController.py
Normal 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)}")
|
116
application/controllers/OrderItemController.py
Normal file
116
application/controllers/OrderItemController.py
Normal 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)}")
|
190
application/controllers/PartController.py
Normal file
190
application/controllers/PartController.py
Normal 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)}")
|
104
application/controllers/ReviewController.py
Normal file
104
application/controllers/ReviewController.py
Normal 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)}")
|
178
application/controllers/UserController.py
Normal file
178
application/controllers/UserController.py
Normal 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
15
application/database.py
Normal 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
|
21
application/dto/CategoryDTO.py
Normal file
21
application/dto/CategoryDTO.py
Normal 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"})
|
19
application/dto/ManufacturerDTO.py
Normal file
19
application/dto/ManufacturerDTO.py
Normal 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"})
|
27
application/dto/OrderDTO.py
Normal file
27
application/dto/OrderDTO.py
Normal 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)
|
21
application/dto/OrderItemDTO.py
Normal file
21
application/dto/OrderItemDTO.py
Normal 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"})
|
38
application/dto/PartDTO.py
Normal file
38
application/dto/PartDTO.py
Normal 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"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
39
application/dto/ReviewDTO.py
Normal file
39
application/dto/ReviewDTO.py
Normal 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"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
37
application/dto/UserDTO.py
Normal file
37
application/dto/UserDTO.py
Normal 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
55
application/main.py
Normal 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)
|
18
application/models/Categories.py
Normal file
18
application/models/Categories.py
Normal 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"
|
||||||
|
)
|
15
application/models/Manufacturers.py
Normal file
15
application/models/Manufacturers.py
Normal 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"
|
||||||
|
)
|
28
application/models/OrderItems.py
Normal file
28
application/models/OrderItems.py
Normal 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")
|
||||||
|
)
|
35
application/models/Orders.py
Normal file
35
application/models/Orders.py
Normal 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"),
|
||||||
|
)
|
37
application/models/Parts.py
Normal file
37
application/models/Parts.py
Normal 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")
|
||||||
|
)
|
29
application/models/Reviews.py
Normal file
29
application/models/Reviews.py
Normal 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")
|
||||||
|
)
|
31
application/models/Users.py
Normal file
31
application/models/Users.py
Normal 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"
|
||||||
|
)
|
17
application/repositories/AbstractBasicRepository.py
Normal file
17
application/repositories/AbstractBasicRepository.py
Normal 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
|
27
application/repositories/AbstractRepository.py
Normal file
27
application/repositories/AbstractRepository.py
Normal 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
|
21
application/repositories/CartRepository.py
Normal file
21
application/repositories/CartRepository.py
Normal 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()
|
48
application/repositories/CategoryRepository.py
Normal file
48
application/repositories/CategoryRepository.py
Normal 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
|
48
application/repositories/ManufacturerRepository.py
Normal file
48
application/repositories/ManufacturerRepository.py
Normal 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
|
40
application/repositories/OrderItemRepository.py
Normal file
40
application/repositories/OrderItemRepository.py
Normal 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
|
40
application/repositories/OrderRepository.py
Normal file
40
application/repositories/OrderRepository.py
Normal 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
|
56
application/repositories/PartRepository.py
Normal file
56
application/repositories/PartRepository.py
Normal 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()
|
41
application/repositories/ReviewRepository.py
Normal file
41
application/repositories/ReviewRepository.py
Normal 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
|
||||||
|
|
54
application/repositories/UserRepository.py
Normal file
54
application/repositories/UserRepository.py
Normal 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)
|
6
application/services/AbstractBasicService.py
Normal file
6
application/services/AbstractBasicService.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from abc import ABC
|
||||||
|
from application.repositories.AbstractBasicRepository import AbstractBasicRepository
|
||||||
|
|
||||||
|
|
||||||
|
class AbstractBasicService(AbstractBasicRepository, ABC):
|
||||||
|
pass
|
6
application/services/AbstractService.py
Normal file
6
application/services/AbstractService.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from abc import ABC
|
||||||
|
from application.repositories.AbstractRepository import AbstractRepository
|
||||||
|
|
||||||
|
|
||||||
|
class AbstractService(AbstractRepository, ABC):
|
||||||
|
pass
|
51
application/services/CartService.py
Normal file
51
application/services/CartService.py
Normal 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
|
||||||
|
|
33
application/services/CategoryService.py
Normal file
33
application/services/CategoryService.py
Normal 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)
|
33
application/services/ManufacturerService.py
Normal file
33
application/services/ManufacturerService.py
Normal 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)
|
39
application/services/OrderItemService.py
Normal file
39
application/services/OrderItemService.py
Normal 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
|
54
application/services/OrderService.py
Normal file
54
application/services/OrderService.py
Normal 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
|
194
application/services/PartService.py
Normal file
194
application/services/PartService.py
Normal 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)}'}
|
63
application/services/ReviewService.py
Normal file
63
application/services/ReviewService.py
Normal 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)}'}
|
110
application/services/UserService.py
Normal file
110
application/services/UserService.py
Normal 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
47
create_directory_files.py
Normal 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
47
docker-compose.yml
Normal 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
|
8
docker-files/Dockerfile.elasticsearch
Normal file
8
docker-files/Dockerfile.elasticsearch
Normal 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
|
7
docker-files/Dockerfile.kibana
Normal file
7
docker-files/Dockerfile.kibana
Normal 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
|
12
docker-files/elasticsearch.yml
Normal file
12
docker-files/elasticsearch.yml
Normal 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
49
docker-files/jvm.options
Normal 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
4
docker-files/kibana.yml
Normal 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
26
front/.gitignore
vendored
Normal 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
17
front/index.html
Normal 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
1365
front/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
22
front/package.json
Normal file
22
front/package.json
Normal 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
BIN
front/public/Part.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 609 KiB |
19
front/src/App.vue
Normal file
19
front/src/App.vue
Normal 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>
|
199
front/src/components/Cart.vue
Normal file
199
front/src/components/Cart.vue
Normal 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>
|
129
front/src/components/Category.vue
Normal file
129
front/src/components/Category.vue
Normal 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>
|
90
front/src/components/DataCard.vue
Normal file
90
front/src/components/DataCard.vue
Normal 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>
|
72
front/src/components/DataTable.vue
Normal file
72
front/src/components/DataTable.vue
Normal 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>
|
51
front/src/components/Header.vue
Normal file
51
front/src/components/Header.vue
Normal 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>
|
63
front/src/components/Modal.vue
Normal file
63
front/src/components/Modal.vue
Normal 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>
|
158
front/src/components/Order.vue
Normal file
158
front/src/components/Order.vue
Normal 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>
|
153
front/src/components/OrderItems.vue
Normal file
153
front/src/components/OrderItems.vue
Normal 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>
|
61
front/src/components/Part.vue
Normal file
61
front/src/components/Part.vue
Normal 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>
|
216
front/src/components/PartDefine.vue
Normal file
216
front/src/components/PartDefine.vue
Normal 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>
|
49
front/src/components/PartList.vue
Normal file
49
front/src/components/PartList.vue
Normal 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>
|
120
front/src/components/Review.vue
Normal file
120
front/src/components/Review.vue
Normal 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>
|
133
front/src/components/ReviewCard.vue
Normal file
133
front/src/components/ReviewCard.vue
Normal 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>
|
348
front/src/components/SignUpIn.vue
Normal file
348
front/src/components/SignUpIn.vue
Normal 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>
|
95
front/src/components/ToolBar.vue
Normal file
95
front/src/components/ToolBar.vue
Normal 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
30
front/src/main.js
Normal 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')
|
101
front/src/mixins/CatalogMixins.js
Normal file
101
front/src/mixins/CatalogMixins.js
Normal 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;
|
58
front/src/services/DataService.js
Normal file
58
front/src/services/DataService.js
Normal 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
58
front/src/style.css
Normal 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
7
front/vite.config.js
Normal 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
26
front_admin/.gitignore
vendored
Normal 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
17
front_admin/index.html
Normal 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
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
21
front_admin/package.json
Normal 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
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
19
front_admin/src/App.vue
Normal 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>
|
73
front_admin/src/components/Category.vue
Normal file
73
front_admin/src/components/Category.vue
Normal 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>
|
207
front_admin/src/components/CategoryChange.vue
Normal file
207
front_admin/src/components/CategoryChange.vue
Normal 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>
|
90
front_admin/src/components/DataCard.vue
Normal file
90
front_admin/src/components/DataCard.vue
Normal 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>
|
72
front_admin/src/components/DataTable.vue
Normal file
72
front_admin/src/components/DataTable.vue
Normal 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>
|
43
front_admin/src/components/Header.vue
Normal file
43
front_admin/src/components/Header.vue
Normal 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>
|
76
front_admin/src/components/Manufacturer.vue
Normal file
76
front_admin/src/components/Manufacturer.vue
Normal 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>
|
63
front_admin/src/components/Modal.vue
Normal file
63
front_admin/src/components/Modal.vue
Normal 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>
|
74
front_admin/src/components/Part.vue
Normal file
74
front_admin/src/components/Part.vue
Normal 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
Loading…
x
Reference in New Issue
Block a user