Merge remote-tracking branch 'origin/main'
This commit is contained in:
@@ -29,3 +29,5 @@ REFRESH_TOKEN_LIFETIME = 5000
|
|||||||
SMS_API_KEY = ''
|
SMS_API_KEY = ''
|
||||||
|
|
||||||
VAPID_PRIVATE_KEY = 'NajogmGTsGsZ_dfURrjUpgsm5fui-s5AzruBQgMh_I4'
|
VAPID_PRIVATE_KEY = 'NajogmGTsGsZ_dfURrjUpgsm5fui-s5AzruBQgMh_I4'
|
||||||
|
|
||||||
|
OPENAI_API_KEY = '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'
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 5.1.2 on 2025-02-22 16:46
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('account', '0023_alter_securitybreachattemptmodel_city_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='user',
|
||||||
|
name='birth_date',
|
||||||
|
field=models.DateField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -47,7 +47,7 @@ class User(AbstractBaseUser, PermissionsMixin):
|
|||||||
('زن', 'زن')
|
('زن', 'زن')
|
||||||
)
|
)
|
||||||
gender = models.CharField(choices=gender_option, max_length=20, verbose_name='جنسیت')
|
gender = models.CharField(choices=gender_option, max_length=20, verbose_name='جنسیت')
|
||||||
birth_date = models.DateField()
|
birth_date = models.DateField(blank=True, null=True)
|
||||||
date_joined = models.DateTimeField(auto_now_add=True, verbose_name='تاریخ ثبتنام')
|
date_joined = models.DateTimeField(auto_now_add=True, verbose_name='تاریخ ثبتنام')
|
||||||
otp_hash = models.CharField(max_length=64, null=True, blank=True, verbose_name='کد یک بار مصرف')
|
otp_hash = models.CharField(max_length=64, null=True, blank=True, verbose_name='کد یک بار مصرف')
|
||||||
otp_expiry = models.DateTimeField(null=True, blank=True, verbose_name='تاریخ تمام شدن کد یک بار مصرف')
|
otp_expiry = models.DateTimeField(null=True, blank=True, verbose_name='تاریخ تمام شدن کد یک بار مصرف')
|
||||||
|
|||||||
@@ -39,10 +39,10 @@ class SendOTPView(APIView):
|
|||||||
try:
|
try:
|
||||||
user, created = User.objects.get_or_create(phone=phone)
|
user, created = User.objects.get_or_create(phone=phone)
|
||||||
otp = user.set_otp()
|
otp = user.set_otp()
|
||||||
message = f"کد یک بار مصرف : {otp}"
|
message = f"""به فروشگاه هی ملز خوش اومدی!!❤️🤖
|
||||||
|
کد یک بار مصرف شما :
|
||||||
|
Code: {otp}"""
|
||||||
sms_api = ghasedak_sms.Ghasedak(api_key="4dc844abd4409fe247ec73831aed2498ad3749c1945660cc252654371756b966vafe5d9LGgMbnfGn")
|
sms_api = ghasedak_sms.Ghasedak(api_key="1227eaaddcba72bcb0169b37032cf16ae9ac6ed8b3b7c2768b74e2ee351d1b52gyRe3AGomZRPTNEd")
|
||||||
|
|
||||||
# response = sms_api.send_single_sms(ghasedak_sms.SendSingleSmsInput(message=message, receptor=phone, line_number='30005006006908', send_date='', client_reference_id=''))
|
# response = sms_api.send_single_sms(ghasedak_sms.SendSingleSmsInput(message=message, receptor=phone, line_number='30005006006908', send_date='', client_reference_id=''))
|
||||||
# print(response)
|
# print(response)
|
||||||
@@ -53,8 +53,7 @@ class SendOTPView(APIView):
|
|||||||
ghasedak_sms.SendSingleSmsInput(
|
ghasedak_sms.SendSingleSmsInput(
|
||||||
message=message,
|
message=message,
|
||||||
receptor=phone,
|
receptor=phone,
|
||||||
line_number='50001212124889',
|
line_number='30005006004095',
|
||||||
send_date='',
|
|
||||||
client_reference_id=str(user.pk)
|
client_reference_id=str(user.pk)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -64,9 +63,8 @@ class SendOTPView(APIView):
|
|||||||
if response['statusCode'] == 200:
|
if response['statusCode'] == 200:
|
||||||
return Response({'detail': 'OTP sent successfully'}, status=status.HTTP_200_OK)
|
return Response({'detail': 'OTP sent successfully'}, status=status.HTTP_200_OK)
|
||||||
else:
|
else:
|
||||||
print('remmber to remove #TODO')
|
|
||||||
print(response)
|
print(response)
|
||||||
return Response({'detail': f'OTP sent successfully {otp}'}, status=status.HTTP_200_OK)
|
return Response({'detail': f'مشکلی در ارسال کد رخ داد'}, status=status.HTTP_200_OK)
|
||||||
# return Response({'detail': response, 'otp_code': otp}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
# return Response({'detail': response, 'otp_code': otp}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
except User.DoesNotExist:
|
except User.DoesNotExist:
|
||||||
|
|||||||
+178
-147
@@ -1,5 +1,4 @@
|
|||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
# from http.cookiejar import debug
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
import os
|
import os
|
||||||
@@ -7,15 +6,21 @@ from django.templatetags.static import static
|
|||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
# Load environment variables
|
||||||
load_dotenv(".env.local")
|
load_dotenv(".env.local")
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# General Configuration
|
||||||
|
# ==============================================================================
|
||||||
DOMAIN = os.getenv("DOMAIN")
|
DOMAIN = os.getenv("DOMAIN")
|
||||||
API_DOMAIN = os.getenv("API_DOMAIN")
|
API_DOMAIN = os.getenv("API_DOMAIN")
|
||||||
OPENAI_API_KEY = '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'
|
|
||||||
# TODO update telegram bot token
|
|
||||||
TELEGRAM_BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN")
|
|
||||||
|
|
||||||
# TODO update email bullshit
|
# API Keys and Tokens
|
||||||
|
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
|
||||||
|
TELEGRAM_BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN")
|
||||||
|
VAPID_PRIVATE_KEY = os.getenv("VAPID_PRIVATE_KEY")
|
||||||
|
|
||||||
|
# Email Configuration
|
||||||
EMAIL_BACKEND = os.getenv("EMAIL_BACKEND")
|
EMAIL_BACKEND = os.getenv("EMAIL_BACKEND")
|
||||||
EMAIL_HOST = os.getenv("EMAIL_HOST")
|
EMAIL_HOST = os.getenv("EMAIL_HOST")
|
||||||
EMAIL_PORT = 587
|
EMAIL_PORT = 587
|
||||||
@@ -24,17 +29,16 @@ EMAIL_HOST_USER = os.getenv("EMAIL_HOST_USER")
|
|||||||
EMAIL_HOST_PASSWORD = os.getenv("EMAIL_HOST_PASSWORD")
|
EMAIL_HOST_PASSWORD = os.getenv("EMAIL_HOST_PASSWORD")
|
||||||
DEFAULT_FROM_EMAIL = os.getenv("SECRET_KEY")
|
DEFAULT_FROM_EMAIL = os.getenv("SECRET_KEY")
|
||||||
|
|
||||||
|
# Security and Debugging
|
||||||
SECRET_KEY = os.getenv("SECRET_KEY")
|
SECRET_KEY = os.getenv("SECRET_KEY")
|
||||||
DEBUG = True
|
DEBUG = True
|
||||||
BASE_DIR = Path(__file__).resolve().parent.parent.parent
|
BASE_DIR = Path(__file__).resolve().parent.parent.parent
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
VAPID_PRIVATE_KEY = os.getenv('VAPID_PRIVATE_KEY')
|
# Application Definition
|
||||||
|
# ==============================================================================
|
||||||
# Application definition
|
|
||||||
|
|
||||||
INSTALLED_APPS = [
|
INSTALLED_APPS = [
|
||||||
# unfold theme
|
# Unfold Theme
|
||||||
"unfold",
|
"unfold",
|
||||||
"unfold.contrib.filters",
|
"unfold.contrib.filters",
|
||||||
"unfold.contrib.forms",
|
"unfold.contrib.forms",
|
||||||
@@ -42,37 +46,159 @@ INSTALLED_APPS = [
|
|||||||
"unfold.contrib.import_export",
|
"unfold.contrib.import_export",
|
||||||
"unfold.contrib.guardian",
|
"unfold.contrib.guardian",
|
||||||
"unfold.contrib.simple_history",
|
"unfold.contrib.simple_history",
|
||||||
# django
|
# Django Core
|
||||||
'django.contrib.admin',
|
"django.contrib.admin",
|
||||||
'django.contrib.auth',
|
"django.contrib.auth",
|
||||||
'django.contrib.contenttypes',
|
"django.contrib.contenttypes",
|
||||||
'django.contrib.sessions',
|
"django.contrib.sessions",
|
||||||
'django.contrib.messages',
|
"django.contrib.messages",
|
||||||
'django.contrib.staticfiles',
|
"django.contrib.staticfiles",
|
||||||
# thired party apps
|
# Third-Party Apps
|
||||||
'corsheaders',
|
"storages",
|
||||||
'rest_framework',
|
"corsheaders",
|
||||||
'drf_spectacular',
|
"rest_framework",
|
||||||
'django_cleanup.apps.CleanupConfig',
|
"drf_spectacular",
|
||||||
'django_filters',
|
"django_cleanup.apps.CleanupConfig",
|
||||||
'rest_framework_simplejwt',
|
"django_filters",
|
||||||
'rest_framework_simplejwt.token_blacklist',
|
"rest_framework_simplejwt",
|
||||||
'rest_framework.authtoken',
|
"rest_framework_simplejwt.token_blacklist",
|
||||||
'import_export',
|
"rest_framework.authtoken",
|
||||||
|
"import_export",
|
||||||
"django_jalali",
|
"django_jalali",
|
||||||
# custom apps
|
# Custom Apps
|
||||||
'product',
|
"product",
|
||||||
'account',
|
"account",
|
||||||
'ticket',
|
"ticket",
|
||||||
'chat',
|
"chat",
|
||||||
'order',
|
"order",
|
||||||
'home',
|
"home",
|
||||||
'blog',
|
"blog",
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# Middleware Configuration
|
||||||
|
# ==============================================================================
|
||||||
|
MIDDLEWARE = [
|
||||||
|
"django.middleware.security.SecurityMiddleware",
|
||||||
|
"whitenoise.middleware.WhiteNoiseMiddleware",
|
||||||
|
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||||
|
"corsheaders.middleware.CorsMiddleware",
|
||||||
|
"django.middleware.common.CommonMiddleware",
|
||||||
|
"django.middleware.csrf.CsrfViewMiddleware",
|
||||||
|
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||||
|
"django.contrib.messages.middleware.MessageMiddleware",
|
||||||
|
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||||
|
]
|
||||||
|
|
||||||
|
ROOT_URLCONF = "core.urls"
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# Template Configuration
|
||||||
|
# ==============================================================================
|
||||||
|
TEMPLATES = [
|
||||||
|
{
|
||||||
|
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||||
|
"DIRS": [BASE_DIR / "templates"],
|
||||||
|
"APP_DIRS": True,
|
||||||
|
"OPTIONS": {
|
||||||
|
"context_processors": [
|
||||||
|
"django.template.context_processors.debug",
|
||||||
|
"django.template.context_processors.request",
|
||||||
|
"django.contrib.auth.context_processors.auth",
|
||||||
|
"django.contrib.messages.context_processors.messages",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
WSGI_APPLICATION = "core.wsgi.application"
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# Authentication and Password Validation
|
||||||
|
# ==============================================================================
|
||||||
|
AUTH_PASSWORD_VALIDATORS = [
|
||||||
|
{
|
||||||
|
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
LANGUAGE_CODE = "fa"
|
||||||
|
TIME_ZONE = "UTC"
|
||||||
|
USE_I18N = True
|
||||||
|
USE_L10N = True
|
||||||
|
USE_TZ = True
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# Static Files Configuration
|
||||||
|
# ==============================================================================
|
||||||
|
STATICFILES_DIRS = [
|
||||||
|
os.path.join(BASE_DIR, "custom_static"),
|
||||||
|
BASE_DIR / "core" / "static",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
|
||||||
|
|
||||||
|
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
||||||
|
AUTH_USER_MODEL = "account.User"
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# REST Framework Configuration
|
||||||
|
# ==============================================================================
|
||||||
|
REST_FRAMEWORK = {
|
||||||
|
# "DEFAULT_FILTER_BACKENDS": ["django_filters.rest_framework.DjangoFilterBackend"],
|
||||||
|
"DEFAULT_RENDERER_CLASSES": [
|
||||||
|
"rest_framework.renderers.JSONRenderer",
|
||||||
|
"rest_framework.renderers.BrowsableAPIRenderer",
|
||||||
|
],
|
||||||
|
"DEFAULT_AUTHENTICATION_CLASSES": (
|
||||||
|
"rest_framework_simplejwt.authentication.JWTAuthentication",
|
||||||
|
),
|
||||||
|
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
|
||||||
|
# "DEFAULT_PERMISSION_CLASSES": [
|
||||||
|
# "rest_framework.permissions.IsAuthenticated",
|
||||||
|
# ],
|
||||||
|
}
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# Simple JWT Configuration
|
||||||
|
# ==============================================================================
|
||||||
|
SIMPLE_JWT = {
|
||||||
|
"ACCESS_TOKEN_LIFETIME": timedelta(days=1),
|
||||||
|
"REFRESH_TOKEN_LIFETIME": timedelta(days=7),
|
||||||
|
"ROTATE_REFRESH_TOKENS": True,
|
||||||
|
"BLACKLIST_AFTER_ROTATION": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# Spectacular (API Documentation) Configuration
|
||||||
|
# ==============================================================================
|
||||||
|
SPECTACULAR_SETTINGS = {
|
||||||
|
"TITLE": os.getenv("SITE_TITLE"),
|
||||||
|
"DESCRIPTION": os.getenv("SITE_TITLE"),
|
||||||
|
"VERSION": "2.0.0",
|
||||||
|
"SERVE_INCLUDE_SCHEMA": False,
|
||||||
|
"COMPONENT_SPLIT_REQUEST": True,
|
||||||
|
"SWAGGER_UI_SETTINGS": {
|
||||||
|
"persistAuthorization": True,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# Persian Datetime Configuration
|
||||||
|
# ==============================================================================
|
||||||
JALALI_SETTINGS = {
|
JALALI_SETTINGS = {
|
||||||
# JavaScript static files for the admin Jalali date widget
|
|
||||||
"ADMIN_JS_STATIC_FILES": [
|
"ADMIN_JS_STATIC_FILES": [
|
||||||
"admin/jquery.ui.datepicker.jalali/scripts/jquery-1.10.2.min.js",
|
"admin/jquery.ui.datepicker.jalali/scripts/jquery-1.10.2.min.js",
|
||||||
"admin/jquery.ui.datepicker.jalali/scripts/jquery.ui.core.js",
|
"admin/jquery.ui.datepicker.jalali/scripts/jquery.ui.core.js",
|
||||||
@@ -81,7 +207,6 @@ JALALI_SETTINGS = {
|
|||||||
"admin/jquery.ui.datepicker.jalali/scripts/jquery.ui.datepicker-cc-fa.js",
|
"admin/jquery.ui.datepicker.jalali/scripts/jquery.ui.datepicker-cc-fa.js",
|
||||||
"admin/main.js",
|
"admin/main.js",
|
||||||
],
|
],
|
||||||
# CSS static files for the admin Jalali date widget
|
|
||||||
"ADMIN_CSS_STATIC_FILES": {
|
"ADMIN_CSS_STATIC_FILES": {
|
||||||
"all": [
|
"all": [
|
||||||
"admin/jquery.ui.datepicker.jalali/themes/base/jquery-ui.min.css",
|
"admin/jquery.ui.datepicker.jalali/themes/base/jquery-ui.min.css",
|
||||||
@@ -91,113 +216,19 @@ JALALI_SETTINGS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
MIDDLEWARE = [
|
# ==============================================================================
|
||||||
'django.middleware.security.SecurityMiddleware',
|
# AWS S3 setting for production
|
||||||
"whitenoise.middleware.WhiteNoiseMiddleware",
|
# ==============================================================================
|
||||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
AWS_ACCESS_KEY_ID = 'mtiSN2JWjWgyfr2u'
|
||||||
'corsheaders.middleware.CorsMiddleware',
|
AWS_SECRET_ACCESS_KEY = 'ZGmOM6ekLJEswJS1kOLp49B8DQ3GT0HZ'
|
||||||
'django.middleware.common.CommonMiddleware',
|
AWS_STORAGE_BUCKET_NAME = 'c262408'
|
||||||
'django.middleware.csrf.CsrfViewMiddleware',
|
AWS_S3_ENDPOINT_URL = 'https://parspack.net'
|
||||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
AWS_S3_REGION_NAME = 'default'
|
||||||
'django.contrib.messages.middleware.MessageMiddleware',
|
AWS_S3_SIGNATURE_VERSION = 's3'
|
||||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
AWS_S3_ADDRESSING_STYLE = 'virtual'
|
||||||
]
|
AWS_QUERYSTRING_AUTH = True
|
||||||
|
AWS_DEFAULT_ACL = None
|
||||||
ROOT_URLCONF = 'core.urls'
|
AWS_S3_OBJECT_PARAMETERS = {
|
||||||
|
'CacheControl': 'max-age=86400',
|
||||||
TEMPLATES = [
|
|
||||||
{
|
|
||||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
|
||||||
'DIRS': [BASE_DIR / "templates"],
|
|
||||||
'APP_DIRS': True,
|
|
||||||
'OPTIONS': {
|
|
||||||
'context_processors': [
|
|
||||||
'django.template.context_processors.debug',
|
|
||||||
'django.template.context_processors.request',
|
|
||||||
'django.contrib.auth.context_processors.auth',
|
|
||||||
'django.contrib.messages.context_processors.messages',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
WSGI_APPLICATION = 'core.wsgi.application'
|
|
||||||
|
|
||||||
|
|
||||||
AUTH_PASSWORD_VALIDATORS = [
|
|
||||||
{
|
|
||||||
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
LANGUAGE_CODE = 'en-us'
|
|
||||||
|
|
||||||
TIME_ZONE = 'UTC'
|
|
||||||
|
|
||||||
USE_I18N = True
|
|
||||||
|
|
||||||
USE_TZ = True
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
STATICFILES_DIRS = [
|
|
||||||
os.path.join(BASE_DIR, 'custom_static'),
|
|
||||||
BASE_DIR / "core" / "static"
|
|
||||||
]
|
|
||||||
|
|
||||||
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
|
|
||||||
|
|
||||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
|
||||||
|
|
||||||
AUTH_USER_MODEL = 'account.User'
|
|
||||||
|
|
||||||
|
|
||||||
REST_FRAMEWORK = {
|
|
||||||
# 'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend'],
|
|
||||||
'DEFAULT_RENDERER_CLASSES': [
|
|
||||||
'rest_framework.renderers.JSONRenderer',
|
|
||||||
'rest_framework.renderers.BrowsableAPIRenderer',
|
|
||||||
|
|
||||||
],
|
|
||||||
'DEFAULT_AUTHENTICATION_CLASSES': (
|
|
||||||
|
|
||||||
'rest_framework_simplejwt.authentication.JWTAuthentication',
|
|
||||||
|
|
||||||
),
|
|
||||||
'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
|
|
||||||
# 'DEFAULT_PERMISSION_CLASSES': [
|
|
||||||
# 'rest_framework.permissions.IsAuthenticated',
|
|
||||||
# ],
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SIMPLE_JWT = {
|
|
||||||
'ACCESS_TOKEN_LIFETIME': timedelta(days=1),
|
|
||||||
'REFRESH_TOKEN_LIFETIME': timedelta(days=7),
|
|
||||||
'ROTATE_REFRESH_TOKENS': True,
|
|
||||||
'BLACKLIST_AFTER_ROTATION': True,
|
|
||||||
}
|
|
||||||
|
|
||||||
SPECTACULAR_SETTINGS = {
|
|
||||||
'TITLE': os.getenv("SITE_TITLE"),
|
|
||||||
'DESCRIPTION': os.getenv("SITE_TITLE"),
|
|
||||||
'VERSION': '2.0.0',
|
|
||||||
'SERVE_INCLUDE_SCHEMA': False,
|
|
||||||
'COMPONENT_SPLIT_REQUEST': True
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def environment_callback(request):
|
|
||||||
return ["نسخه ی توسعه", "success"]
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
from .base import *
|
from .base import *
|
||||||
from .unfold_conf import *
|
from .unfold_conf import *
|
||||||
|
|
||||||
|
|
||||||
ALLOWED_HOSTS = ['127.0.0.1', 'localhost', DOMAIN, API_DOMAIN]
|
ALLOWED_HOSTS = ['127.0.0.1', 'localhost', DOMAIN, API_DOMAIN]
|
||||||
CSRF_TRUSTED_ORIGINS = [
|
CSRF_TRUSTED_ORIGINS = [
|
||||||
f"https://{DOMAIN}",
|
f"https://{DOMAIN}",
|
||||||
@@ -31,7 +33,16 @@ DATABASES = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MEDIA_URL = '/shop_media/'
|
STORAGES = {
|
||||||
|
"default": {
|
||||||
|
"BACKEND": 'core.storages.MediaStorage',
|
||||||
|
},
|
||||||
|
"staticfiles": {
|
||||||
|
"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
MEDIA_URL = 'https://c262408.parspack.net/'
|
||||||
MEDIA_ROOT = '/app/media'
|
MEDIA_ROOT = '/app/media'
|
||||||
|
|
||||||
STATIC_URL = '/shop_static/'
|
STATIC_URL = '/shop_static/'
|
||||||
|
|||||||
@@ -28,7 +28,9 @@ UNFOLD = {
|
|||||||
"LOGIN": {
|
"LOGIN": {
|
||||||
"image": lambda request: static("favicon.png"),
|
"image": lambda request: static("favicon.png"),
|
||||||
},
|
},
|
||||||
|
"STYLES": [
|
||||||
|
lambda request: static("rtl.css"),
|
||||||
|
],
|
||||||
|
|
||||||
"BORDER_RADIUS": "20px",
|
"BORDER_RADIUS": "20px",
|
||||||
"SHOW_HISTORY": True,
|
"SHOW_HISTORY": True,
|
||||||
@@ -80,7 +82,7 @@ UNFOLD = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
"SIDEBAR": {
|
"SIDEBAR": {
|
||||||
"show_search": True,
|
"show_search": False,
|
||||||
"show_all_applications": True,
|
"show_all_applications": True,
|
||||||
"navigation": [
|
"navigation": [
|
||||||
{
|
{
|
||||||
@@ -263,3 +265,6 @@ UNFOLD = {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def environment_callback(request):
|
||||||
|
return ["نسخه ی توسعه", "success"]
|
||||||
@@ -0,0 +1,310 @@
|
|||||||
|
/* Base RTL adjustments */
|
||||||
|
[dir="rtl"] body {
|
||||||
|
direction: rtl;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Headers and titles */
|
||||||
|
[dir="rtl"] .header,
|
||||||
|
[dir="rtl"] h1,
|
||||||
|
[dir="rtl"] h2,
|
||||||
|
[dir="rtl"] h3,
|
||||||
|
[dir="rtl"] .branding h1 {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Float adjustments */
|
||||||
|
[dir="rtl"] .float-left {
|
||||||
|
float: right !important;
|
||||||
|
}
|
||||||
|
[dir="rtl"] .float-right {
|
||||||
|
float: left !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Margins and paddings */
|
||||||
|
[dir="rtl"] .margin-left-10 {
|
||||||
|
margin-right: 10px !important;
|
||||||
|
margin-left: 0 !important;
|
||||||
|
}
|
||||||
|
[dir="rtl"] .margin-right-10 {
|
||||||
|
margin-left: 10px !important;
|
||||||
|
margin-right: 0 !important;
|
||||||
|
}
|
||||||
|
[dir="rtl"] .padding-left-15 {
|
||||||
|
padding-right: 15px !important;
|
||||||
|
padding-left: 0 !important;
|
||||||
|
}
|
||||||
|
[dir="rtl"] .padding-right-15 {
|
||||||
|
padding-left: 15px !important;
|
||||||
|
padding-right: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form elements */
|
||||||
|
[dir="rtl"] .aligned label {
|
||||||
|
padding: 0 0 3px 1em;
|
||||||
|
float: right !important;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
[dir="rtl"] .form-row {
|
||||||
|
direction: rtl;
|
||||||
|
}
|
||||||
|
[dir="rtl"] .form-row .field-box {
|
||||||
|
float: right;
|
||||||
|
margin-right: 0;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
[dir="rtl"] input,
|
||||||
|
[dir="rtl"] select,
|
||||||
|
[dir="rtl"] textarea {
|
||||||
|
direction: rtl;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Buttons and submit row */
|
||||||
|
[dir="rtl"] .submit-row {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
[dir="rtl"] .submit-row input,
|
||||||
|
[dir="rtl"] .button {
|
||||||
|
margin-left: 0;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Inline groups (e.g., tabular or stacked inlines) */
|
||||||
|
[dir="rtl"] .inline-group {
|
||||||
|
direction: rtl;
|
||||||
|
}
|
||||||
|
[dir="rtl"] .inline-related h3 {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
[dir="rtl"] .inline-related .inline_label {
|
||||||
|
float: right;
|
||||||
|
padding-right: 0;
|
||||||
|
padding-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tables */
|
||||||
|
[dir="rtl"] table {
|
||||||
|
direction: rtl;
|
||||||
|
}
|
||||||
|
[dir="rtl"] th,
|
||||||
|
[dir="rtl"] td {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
[dir="rtl"] .sortoptions {
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Navigation and sidebar (Unfold-specific) */
|
||||||
|
[dir="rtl"] .unfold-sidebar {
|
||||||
|
right: unset;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
[dir="rtl"] .unfold-main {
|
||||||
|
margin-left: 0;
|
||||||
|
margin-right: 260px; /* Adjust based on sidebar width */
|
||||||
|
}
|
||||||
|
[dir="rtl"] .unfold-nav {
|
||||||
|
direction: rtl;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
[dir="rtl"] .unfold-nav li {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
[dir="rtl"] .unfold-nav .dropdown-menu {
|
||||||
|
right: unset;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Breadcrumbs */
|
||||||
|
[dir="rtl"] .breadcrumbs {
|
||||||
|
direction: rtl;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
[dir="rtl"] .breadcrumbs a {
|
||||||
|
margin-right: 0;
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Filters (right sidebar) */
|
||||||
|
[dir="rtl"] #changelist-filter {
|
||||||
|
float: left;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
[dir="rtl"] #changelist-filter h3 {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
[dir="rtl"] #changelist-filter li {
|
||||||
|
padding-right: 0;
|
||||||
|
padding-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Miscellaneous */
|
||||||
|
[dir="rtl"] .object-tools {
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
[dir="rtl"] .paginator {
|
||||||
|
direction: rtl;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Enhanced RTL adjustments for Unfold navigation links (specific to navbar) */
|
||||||
|
[dir="rtl"] .unfold-nav a {
|
||||||
|
direction: rtl;
|
||||||
|
justify-content: flex-end; /* Align flex items to the right */
|
||||||
|
}
|
||||||
|
|
||||||
|
[dir="rtl"] .unfold-nav .flex {
|
||||||
|
flex-direction: row-reverse !important; /* Reverse the order of flex items (icon, text, badge) for RTL */
|
||||||
|
}
|
||||||
|
|
||||||
|
[dir="rtl"] .unfold-nav .material-symbols-outlined {
|
||||||
|
margin-right: 0 !important; /* Remove default right margin */
|
||||||
|
margin-left: 0.75rem !important; /* Equivalent to Tailwind’s mr-3 in RTL (12px or 0.75rem) */
|
||||||
|
order: 2 !important; /* Place icon after text in flex direction */
|
||||||
|
}
|
||||||
|
|
||||||
|
[dir="rtl"] .unfold-nav .text-sm {
|
||||||
|
margin-right: 0.5rem !important; /* Space between text and badge (equivalent to Tailwind ml-2 in RTL) */
|
||||||
|
text-align: right;
|
||||||
|
direction: rtl;
|
||||||
|
order: 1 !important; /* Place text before badge in flex direction */
|
||||||
|
}
|
||||||
|
|
||||||
|
[dir="rtl"] .unfold-nav .bg-red-600 {
|
||||||
|
margin-left: 0 !important; /* Remove default left margin */
|
||||||
|
margin-right: 0 !important; /* No margin needed on right unless spacing is required */
|
||||||
|
order: 0 !important; /* Place badge first in flex direction (on the right in RTL) */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure text alignment and direction for Persian */
|
||||||
|
[dir="rtl"] .unfold-nav .text-sm {
|
||||||
|
text-align: right;
|
||||||
|
direction: rtl;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* RTL adjustments for navbar headers and expandable sections (including arrow icon) */
|
||||||
|
[dir="rtl"] .unfold-nav h2 {
|
||||||
|
direction: rtl;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
[dir="rtl"] .unfold-nav .flex-row {
|
||||||
|
flex-direction: row-reverse !important; /* Reverse flex direction for headers */
|
||||||
|
}
|
||||||
|
|
||||||
|
[dir="rtl"] .unfold-nav .material-symbols-outlined.ml-auto {
|
||||||
|
margin-left: 0 !important; /* Remove default left margin (Tailwind ml-auto) */
|
||||||
|
margin-right: auto !important; /* Push to the right in RTL */
|
||||||
|
transform: rotate(180deg) !important; /* Flip chevron_right for RTL (pointing left) */
|
||||||
|
order: 999 !important; /* Ensure it’s the last item in the flex order, on the right */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* RTL adjustments for search bar and other navbar elements */
|
||||||
|
[dir="rtl"] #nav-filter {
|
||||||
|
direction: rtl;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
[dir="rtl"] .unfold-nav input[type="search"] {
|
||||||
|
padding-right: 0.75rem !important; /* Adjust padding for RTL */
|
||||||
|
padding-left: 2rem !important; /* Space for the search icon on the left */
|
||||||
|
}
|
||||||
|
|
||||||
|
[dir="rtl"] .unfold-nav .material-symbols-outlined.pl-3 {
|
||||||
|
padding-left: 0 !important; /* Remove padding-left */
|
||||||
|
padding-right: 0.75rem !important; /* Add padding-right for RTL */
|
||||||
|
}
|
||||||
|
|
||||||
|
[dir="rtl"] .mr-3 {
|
||||||
|
margin-left: .75rem !important;
|
||||||
|
margin-right: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* badge fix */
|
||||||
|
|
||||||
|
[dir="rtl"] .bg-red-600 {
|
||||||
|
margin-left: 0rem !important;
|
||||||
|
margin-right: .5rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* colapse fix */
|
||||||
|
/* [dir="rtl"] .ml-auto {
|
||||||
|
margin-left: 0rem !important;
|
||||||
|
margin-right: 8rem !important;
|
||||||
|
} */
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[dir="rtl"] .absolute.bottom-0.left-0.rounded.top-0 {
|
||||||
|
left: auto;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* log out fix */
|
||||||
|
|
||||||
|
[dir="rtl"] nav.absolute.bg-white.border.flex.flex-col.leading-none.py-1.-right-2.rounded.shadow-lg.top-7.w-52.z-50.dark\:bg-base-800.dark\:border-base-700 {
|
||||||
|
right: auto; /* Remove -right-2 effect */
|
||||||
|
left: 0; /* Anchor to right edge (left in RTL) */
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* filter sprator fix */
|
||||||
|
|
||||||
|
[dir="rtl"] ul.dark\:bg-base-900.border.border-base-200.flex.min-w-20.rounded.shadow-sm.text-font-default-light.dark\:border-base-700.dark\:text-font-default-dark.w-full li {
|
||||||
|
border-right: none;
|
||||||
|
border-left: 1px solid #404040; /* Matches border-base-200 */
|
||||||
|
}
|
||||||
|
|
||||||
|
[dir="rtl"] ul.dark\:bg-base-900.border.border-base-200.flex.min-w-20.rounded.shadow-sm.text-font-default-light.dark\:border-base-700.dark\:text-font-default-dark.w-full li:last-child {
|
||||||
|
border-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode override */
|
||||||
|
[dir="rtl"] .dark ul.dark\:bg-base-900.border.border-base-200.flex.min-w-20.rounded.shadow-sm.text-font-default-light.dark\:border-base-700.dark\:text-font-default-dark.w-full li {
|
||||||
|
border-left: 1px solid #374151; /* Matches dark:border-base-700 */
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* import export sprator fix */
|
||||||
|
|
||||||
|
/* Desktop RTL: Swap right borders to left borders */
|
||||||
|
[dir="rtl"] ul.border.flex.flex-col.font-medium.mb-4.mt-2.rounded.shadow-sm.md\:flex-row.md\:mb-2.md\:mt-0.dark\:border-base-700.max-md\:w-full li.md\:border-r {
|
||||||
|
border-right: none; /* Remove md:border-r */
|
||||||
|
border-left: 1px solid #404040; /* Add left border, matching border-base-200 default */
|
||||||
|
}
|
||||||
|
|
||||||
|
[dir="rtl"] ul.border.flex.flex-col.font-medium.mb-4.mt-2.rounded.shadow-sm.md\:flex-row.md\:mb-2.md\:mt-0.dark\:border-base-700.max-md\:w-full li:last-child {
|
||||||
|
border-left: 0; /* No left border on last item */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode for desktop RTL */
|
||||||
|
[dir="rtl"] .dark ul.border.flex.flex-col.font-medium.mb-4.mt-2.rounded.shadow-sm.md\:flex-row.md\:mb-2.md\:mt-0.dark\:border-base-700.max-md\:w-full li.md\:border-r {
|
||||||
|
border-left: 1px solid #374151; /* Matches dark:border-base-700 */
|
||||||
|
}
|
||||||
|
|
||||||
|
[dir="rtl"] h2.font-semibold.flex.flex-row.group.items-center.mb-1.mx-3.py-1\.5.px-3.select-none.text-font-important-light.text-sm.dark\:text-font-important-dark.cursor-pointer.hover\:text-primary-600.dark\:hover\:text-primary-500 {
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
[dir="rtl"] span.material-symbols-outlined.ml-auto.text-base-400.transition-all.group-hover\:text-primary-600.dark\:group-hover\:text-primary-500 {
|
||||||
|
margin: 0 !important;
|
||||||
|
rotate: 90deg !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[dir="rtl"] div.overflow-hidden.relative.px-2.py-1.text-sm {
|
||||||
|
background: rgb(var(--color-primary-950));
|
||||||
|
border-radius: var(--border-radius, 6px);
|
||||||
|
padding: 8px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[dir="rtl"] div.flex.flex-row.relative.z-20 {
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
[dir="rtl"] strong.font-semibold.text-font-important-light.ml-auto.dark\:text-font-important-dark {
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
from storages.backends.s3boto3 import S3Boto3Storage
|
||||||
|
|
||||||
|
|
||||||
|
class MediaStorage(S3Boto3Storage):
|
||||||
|
location = 'media'
|
||||||
|
default_acl = 'private'
|
||||||
@@ -16,8 +16,8 @@ urlpatterns = [
|
|||||||
# path('auth/', include('djoser.urls.jwt')),
|
# path('auth/', include('djoser.urls.jwt')),
|
||||||
|
|
||||||
path('home', HomeView.as_view()),
|
path('home', HomeView.as_view()),
|
||||||
path('token/', CustomTokenObtainPairView.as_view(), name='token_obtain_pair'),
|
path('token', CustomTokenObtainPairView.as_view(), name='token_obtain_pair'),
|
||||||
path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
|
path('token/refresh', TokenRefreshView.as_view(), name='token_refresh'),
|
||||||
path('admin/', FakeAdminLoginView.as_view()), # Fake admin
|
path('admin/', FakeAdminLoginView.as_view()), # Fake admin
|
||||||
path('secret-admin/', admin.site.urls), # Real admin
|
path('secret-admin/', admin.site.urls), # Real admin
|
||||||
path('schema/', SpectacularAPIView.as_view(), name='schema'),
|
path('schema/', SpectacularAPIView.as_view(), name='schema'),
|
||||||
|
|||||||
+11
-11
@@ -51,10 +51,10 @@ def random_data():
|
|||||||
],
|
],
|
||||||
"kpi": [
|
"kpi": [
|
||||||
{
|
{
|
||||||
"title": "IPhone 16 Pro Max",
|
"title": "گوشی Iphone 16 pro",
|
||||||
"metric": f"${intcomma(f"{random.uniform(1000, 9999):.02f}")}",
|
"metric": f"${intcomma(f"{random.uniform(1000, 9999):.02f}")}",
|
||||||
"footer": mark_safe(
|
"footer": mark_safe(
|
||||||
f'<strong class="text-green-700 font-semibold dark:text-green-400">+{intcomma(f"{random.uniform(1, 9):.02f}")}%</strong> progress from last week'
|
f'<strong class="text-green-700 font-semibold dark:text-green-400">+{intcomma(f"{random.uniform(1, 9):.02f}")}%</strong> درصد فروش کل'
|
||||||
),
|
),
|
||||||
"chart": json.dumps(
|
"chart": json.dumps(
|
||||||
{
|
{
|
||||||
@@ -64,23 +64,23 @@ def random_data():
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Macbook Pro M3",
|
"title": "لپ تاپ Macbook Pro M3",
|
||||||
"metric": f"${intcomma(f"{random.uniform(1000, 9999):.02f}")}",
|
"metric": f"${intcomma(f"{random.uniform(1000, 9999):.02f}")}",
|
||||||
"footer": mark_safe(
|
"footer": mark_safe(
|
||||||
f'<strong class="text-green-700 font-semibold dark:text-green-400">+{intcomma(f"{random.uniform(1, 9):.02f}")}%</strong> progress from last week'
|
f'<strong class="text-green-700 font-semibold dark:text-green-400">+{intcomma(f"{random.uniform(1, 9):.02f}")}%</strong> درصد فروش کل'
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Apple Watch 8",
|
"title": "ساعت هوشمند Apple Watch 8",
|
||||||
"metric": f"${intcomma(f"{random.uniform(1000, 9999):.02f}")}",
|
"metric": f"${intcomma(f"{random.uniform(1000, 9999):.02f}")}",
|
||||||
"footer": mark_safe(
|
"footer": mark_safe(
|
||||||
f'<strong class="text-green-700 font-semibold dark:text-green-400">+{intcomma(f"{random.uniform(1, 9):.02f}")}%</strong> progress from last week'
|
f'<strong class="text-green-700 font-semibold dark:text-green-400">+{intcomma(f"{random.uniform(1, 9):.02f}")}%</strong> درصد فروش کل'
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"progress": [
|
"progress": [
|
||||||
{
|
{
|
||||||
"title": "📱 Phone and Mobile",
|
"title": "📱 موبایل و گوشی",
|
||||||
"description": "$2,499.99",
|
"description": "$2,499.99",
|
||||||
"value": 20,
|
"value": 20,
|
||||||
},
|
},
|
||||||
@@ -155,10 +155,10 @@ def random_data():
|
|||||||
),
|
),
|
||||||
"performance": [
|
"performance": [
|
||||||
{
|
{
|
||||||
"title": _("Last week revenue"),
|
"title": 'فروش ماه اخیر',
|
||||||
"metric": "$1,234.56",
|
"metric": "$1,234.56",
|
||||||
"footer": mark_safe(
|
"footer": mark_safe(
|
||||||
'<strong class="text-green-600 font-medium">+3.14%</strong> progress from last week'
|
'<strong class="text-green-600 font-medium">+3.14%</strong> درصد فروش کل'
|
||||||
),
|
),
|
||||||
"chart": json.dumps(
|
"chart": json.dumps(
|
||||||
{
|
{
|
||||||
@@ -173,10 +173,10 @@ def random_data():
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": _("Last week expenses"),
|
"title": 'مخارج ماه اخیر',
|
||||||
"metric": "$1,234.56",
|
"metric": "$1,234.56",
|
||||||
"footer": mark_safe(
|
"footer": mark_safe(
|
||||||
'<strong class="text-green-600 font-medium">+3.14%</strong> progress from last week'
|
'<strong class="text-green-600 font-medium">+3.14%</strong> درصد فروش کل'
|
||||||
),
|
),
|
||||||
"chart": json.dumps(
|
"chart": json.dumps(
|
||||||
{
|
{
|
||||||
|
|||||||
+11
-9
@@ -59,21 +59,23 @@ class OrderModel(models.Model):
|
|||||||
print('didnt send')
|
print('didnt send')
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
def total_with_discount(self):
|
def discount(self):
|
||||||
total_with_item_discount = sum(item.total_with_discount() for item in self.items.all())
|
pass
|
||||||
if self.discount_code:
|
# total_with_item_discount = sum(item.total_with_discount() for item in self.items.all())
|
||||||
if not self.discount_code.is_valid():
|
# if self.discount_code:
|
||||||
raise DiscountNotAvailableError('این کد تخفیف دیگر معتبر نیست')
|
# if not self.discount_code.is_valid():
|
||||||
discount_percent = self.discount_code.percent
|
# raise DiscountNotAvailableError('این کد تخفیف دیگر معتبر نیست')
|
||||||
return total_with_item_discount * ((100 - discount_percent) / 100)
|
# discount_percent = self.discount_code.percent
|
||||||
return total_with_item_discount
|
# return total_with_item_discount * ((100 - discount_percent) / 100)
|
||||||
|
# return total_with_item_discount
|
||||||
|
|
||||||
|
|
||||||
def tax(self):
|
def tax(self):
|
||||||
return self.total_without_tax() * 0.2
|
return self.total_without_tax() * 0.2
|
||||||
|
|
||||||
def total(self):
|
def total(self):
|
||||||
return self.total_with_discount() + self.tax()
|
pass
|
||||||
|
# return self.total_with_discount() + self.tax()
|
||||||
|
|
||||||
def remove_order_item(self, item_pk, quantity):
|
def remove_order_item(self, item_pk, quantity):
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -8,7 +8,20 @@ class OrderItemSerailzier(serializers.ModelSerializer):
|
|||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
read_only_fields = ('order', 'product')
|
read_only_fields = ('order', 'product')
|
||||||
|
|
||||||
class OrderModelSerializer(serializers.ModelSerializer):
|
class CartSerializer(serializers.ModelSerializer):
|
||||||
|
items = OrderItemSerailzier(many=True)
|
||||||
class Meta:
|
class Meta:
|
||||||
model = OrderModel
|
model = OrderModel
|
||||||
fields = ['address', 'created_at', 'is_paid', 'status', 'discount_code']
|
fields = ['address', 'created_at', 'is_paid', 'status', 'discount_code', 'items']
|
||||||
|
|
||||||
|
|
||||||
|
class OrderSerializer(serializers.ModelSerializer):
|
||||||
|
count = serializers.SerializerMethodField()
|
||||||
|
images = serializers.SerializerMethodField()
|
||||||
|
class Meta:
|
||||||
|
model = OrderModel
|
||||||
|
fields = ['address', 'created_at', 'is_paid', 'status', 'discount_code', "images", "count", "id"]
|
||||||
|
def get_count(self, obj):
|
||||||
|
return obj.items.all().count()
|
||||||
|
def get_images(self, obj):
|
||||||
|
return ["a" , "b" , "c"]
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
from django.conf.urls.static import static
|
from django.conf.urls.static import static
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.urls import path, include
|
from django.urls import path, include
|
||||||
from .views import CartItemViews, CartView
|
from .views import CartItemViews, CartView, OrderlistView
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
path('list', OrderlistView.as_view(), name='order-list'),
|
||||||
path('cart', CartView.as_view()),
|
path('cart', CartView.as_view()),
|
||||||
path('cart/item/<int:pk>', CartItemViews.as_view(), name='change-item-cart'),
|
path('cart/item/<int:pk>', CartItemViews.as_view(), name='change-item-cart'),
|
||||||
path('payment', CartView.as_view()),
|
# path('payment', CartView.as_view()),
|
||||||
path('', CartView.as_view()),
|
# path('', CartView.as_view()),
|
||||||
]
|
]
|
||||||
|
|||||||
+17
-7
@@ -4,7 +4,7 @@ from rest_framework.views import APIView, Response
|
|||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from product.models import ProductVariant
|
from product.models import ProductVariant
|
||||||
from rest_framework.permissions import IsAuthenticated
|
from rest_framework.permissions import IsAuthenticated
|
||||||
from .serializers import OrderItemSerailzier
|
from .serializers import *
|
||||||
# from cart.models import
|
# from cart.models import
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from .models import OrderItemModel, OrderModel
|
from .models import OrderItemModel, OrderModel
|
||||||
@@ -53,11 +53,6 @@ class CartItemViews(APIView):
|
|||||||
user=request.user,
|
user=request.user,
|
||||||
status='CART'
|
status='CART'
|
||||||
)
|
)
|
||||||
# order_item, created = OrderItemModel.objects.get_or_create(
|
|
||||||
# order=cart_order,
|
|
||||||
# product=product_variant,
|
|
||||||
# defaults={'quantity': request.data.get('quantity', 1)}
|
|
||||||
# )
|
|
||||||
order_item = get_object_or_404(OrderItemModel, order=cart_order, product=product_variant)
|
order_item = get_object_or_404(OrderItemModel, order=cart_order, product=product_variant)
|
||||||
order_item.delete()
|
order_item.delete()
|
||||||
return Response({'detail': f'محصول {product_variant.product.name} از سبد خرید پاک شد'}, status=status.HTTP_204_NO_CONTENT)
|
return Response({'detail': f'محصول {product_variant.product.name} از سبد خرید پاک شد'}, status=status.HTTP_204_NO_CONTENT)
|
||||||
@@ -65,5 +60,20 @@ class CartItemViews(APIView):
|
|||||||
|
|
||||||
|
|
||||||
class CartView(APIView):
|
class CartView(APIView):
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
serializer_class = CartSerializer
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
return Response({'detail': 'این بخش در حال توسعه می باشد تا اماده شدن این بخش به نقاشی خود ادامه دهید'}, status=status.HTTP_404_NOT_FOUND)
|
user = request.user
|
||||||
|
cart_instance, created = OrderModel.objects.get_or_create(user=user, status='CART')
|
||||||
|
cart_ser = self.serializer_class(instance=cart_instance, context={'request': request})
|
||||||
|
return Response(cart_ser.data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
|
class OrderlistView(APIView):
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
serializer_class = OrderSerializer
|
||||||
|
def get(self, request):
|
||||||
|
user = request.user
|
||||||
|
orders = OrderModel.objects.filter(user=user).exclude(status="CART")
|
||||||
|
orders_ser = self.serializer_class(instance=orders, many=True, context={'request': request})
|
||||||
|
return Response(orders_ser.data, status=status.HTTP_200_OK)
|
||||||
@@ -170,14 +170,14 @@ class AllProductsView(APIView):
|
|||||||
if search_query:
|
if search_query:
|
||||||
products = products.filter(Q(name__icontains=search_query) | Q(description__icontains=search_query))
|
products = products.filter(Q(name__icontains=search_query) | Q(description__icontains=search_query))
|
||||||
|
|
||||||
# Price filters
|
# # Price filters
|
||||||
price_gte = request.query_params.get('price_gte', None)
|
# price_gte = request.query_params.get('price_gte', None)
|
||||||
price_lte = request.query_params.get('price_lte', None)
|
# price_lte = request.query_params.get('price_lte', None)
|
||||||
|
|
||||||
if price_gte:
|
# if price_gte:
|
||||||
products = products.filter(variants__min_price__gte=price_gte)
|
# products = products.filter(variants__min_price__gte=price_gte)
|
||||||
if price_lte:
|
# if price_lte:
|
||||||
products = products.filter(variants__min_price__lte=price_lte)
|
# products = products.filter(variants__min_price__lte=price_lte)
|
||||||
|
|
||||||
# Sorting
|
# Sorting
|
||||||
sort_by = request.query_params.get('sort', None)
|
sort_by = request.query_params.get('sort', None)
|
||||||
|
|||||||
@@ -5,22 +5,33 @@ annotated-types==0.7.0
|
|||||||
anyio==4.6.0
|
anyio==4.6.0
|
||||||
asgiref==3.8.1
|
asgiref==3.8.1
|
||||||
attrs==24.2.0
|
attrs==24.2.0
|
||||||
|
az-iranian-bank-gateways==2.0.5
|
||||||
|
beautifulsoup4==4.12.3
|
||||||
|
boto3==1.36.26
|
||||||
|
botocore==1.36.26
|
||||||
branca==0.8.1
|
branca==0.8.1
|
||||||
certifi==2024.8.30
|
certifi==2024.8.30
|
||||||
cffi==1.17.1
|
cffi==1.17.1
|
||||||
charset-normalizer==3.3.2
|
charset-normalizer==3.3.2
|
||||||
|
colorama==0.4.6
|
||||||
cryptography==44.0.1
|
cryptography==44.0.1
|
||||||
defusedxml==0.8.0rc2
|
defusedxml==0.8.0rc2
|
||||||
diff-match-patch==20230430
|
diff-match-patch==20230430
|
||||||
distro==1.9.0
|
distro==1.9.0
|
||||||
Django==5.1.2
|
Django==5.1.2
|
||||||
|
django-admin-interface==0.28.5
|
||||||
|
django-admin-persian-fonts==0.2
|
||||||
django-cleanup==8.1.0
|
django-cleanup==8.1.0
|
||||||
|
django-colorfield==0.11.0
|
||||||
django-cors-headers==4.4.0
|
django-cors-headers==4.4.0
|
||||||
|
django-cron==0.6.0
|
||||||
django-dbbackup==4.2.1
|
django-dbbackup==4.2.1
|
||||||
|
django-dirtyfields==1.9.3
|
||||||
django-filter==24.3
|
django-filter==24.3
|
||||||
django-import-export==4.1.1
|
django-import-export==4.1.1
|
||||||
django-iranian-cities==1.0.2
|
django-iranian-cities==1.0.2
|
||||||
django-jalali==7.3.0
|
django-jalali==7.3.0
|
||||||
|
django-storages==1.14.5
|
||||||
django-unfold==0.48.0
|
django-unfold==0.48.0
|
||||||
djangorestframework==3.15.2
|
djangorestframework==3.15.2
|
||||||
djangorestframework-simplejwt==5.3.1
|
djangorestframework-simplejwt==5.3.1
|
||||||
@@ -28,6 +39,7 @@ djoser==2.3.1
|
|||||||
dnspython==2.7.0
|
dnspython==2.7.0
|
||||||
drf-spectacular==0.27.2
|
drf-spectacular==0.27.2
|
||||||
email_validator==2.2.0
|
email_validator==2.2.0
|
||||||
|
et-xmlfile==1.1.0
|
||||||
factory_boy==3.3.1
|
factory_boy==3.3.1
|
||||||
Faker==28.4.1
|
Faker==28.4.1
|
||||||
folium==0.19.4
|
folium==0.19.4
|
||||||
@@ -43,29 +55,40 @@ httpcore==1.0.5
|
|||||||
httpx==0.27.2
|
httpx==0.27.2
|
||||||
idna==3.10
|
idna==3.10
|
||||||
inflection==0.5.1
|
inflection==0.5.1
|
||||||
|
isodate==0.6.1
|
||||||
jalali_core==1.0.0
|
jalali_core==1.0.0
|
||||||
jdatetime==5.0.0
|
jdatetime==5.0.0
|
||||||
Jinja2==3.1.5
|
Jinja2==3.1.5
|
||||||
jiter==0.8.2
|
jiter==0.8.2
|
||||||
|
jmespath==1.0.1
|
||||||
jsonschema==4.23.0
|
jsonschema==4.23.0
|
||||||
jsonschema-specifications==2024.10.1
|
jsonschema-specifications==2024.10.1
|
||||||
|
lxml==5.2.2
|
||||||
|
MarkupPy==1.14
|
||||||
MarkupSafe==3.0.2
|
MarkupSafe==3.0.2
|
||||||
maxminddb==2.6.2
|
maxminddb==2.6.2
|
||||||
multidict==6.1.0
|
multidict==6.1.0
|
||||||
numpy==2.2.3
|
numpy==2.2.3
|
||||||
oauthlib==3.2.2
|
oauthlib==3.2.2
|
||||||
|
odfpy==1.4.1
|
||||||
openai==1.58.1
|
openai==1.58.1
|
||||||
|
openpyxl==3.1.2
|
||||||
pillow==10.4.0
|
pillow==10.4.0
|
||||||
|
platformdirs==4.2.2
|
||||||
|
propcache==0.2.0
|
||||||
psutil==6.0.0
|
psutil==6.0.0
|
||||||
psycopg2-binary==2.9.10
|
psycopg2-binary==2.9.10
|
||||||
py-vapid==1.9.2
|
py-vapid==1.9.2
|
||||||
pycparser==2.22
|
pycparser==2.22
|
||||||
|
pycryptodome==3.20.0
|
||||||
pydantic==2.10.6
|
pydantic==2.10.6
|
||||||
pydantic_core==2.27.2
|
pydantic_core==2.27.2
|
||||||
PyJWT==2.10.1
|
PyJWT==2.10.1
|
||||||
pyTelegramBotAPI==4.23.0
|
pyTelegramBotAPI==4.23.0
|
||||||
python-dateutil==2.9.0.post0
|
python-dateutil==2.9.0.post0
|
||||||
|
python-decouple==3.8
|
||||||
python-dotenv==1.0.1
|
python-dotenv==1.0.1
|
||||||
|
python-slugify==8.0.4
|
||||||
python-telegram-bot==21.6
|
python-telegram-bot==21.6
|
||||||
python3-openid==3.2.0
|
python3-openid==3.2.0
|
||||||
pytz==2024.2
|
pytz==2024.2
|
||||||
@@ -73,20 +96,29 @@ pywebpush==2.0.3
|
|||||||
PyYAML==6.0.2
|
PyYAML==6.0.2
|
||||||
referencing==0.35.1
|
referencing==0.35.1
|
||||||
requests==2.32.3
|
requests==2.32.3
|
||||||
|
requests-file==2.1.0
|
||||||
requests-oauthlib==2.0.0
|
requests-oauthlib==2.0.0
|
||||||
|
requests-toolbelt==1.0.0
|
||||||
rpds-py==0.20.0
|
rpds-py==0.20.0
|
||||||
|
s3transfer==0.11.2
|
||||||
setuptools==75.1.0
|
setuptools==75.1.0
|
||||||
six==1.16.0
|
six==1.16.0
|
||||||
sniffio==1.3.1
|
sniffio==1.3.1
|
||||||
social-auth-app-django==5.4.2
|
social-auth-app-django==5.4.2
|
||||||
social-auth-core==4.5.4
|
social-auth-core==4.5.4
|
||||||
|
soupsieve==2.5
|
||||||
sqlparse==0.5.1
|
sqlparse==0.5.1
|
||||||
tablib==3.5.0
|
tablib==3.5.0
|
||||||
|
telebot==0.0.5
|
||||||
|
text-unidecode==1.3
|
||||||
tqdm==4.67.1
|
tqdm==4.67.1
|
||||||
typing_extensions==4.12.2
|
typing_extensions==4.12.2
|
||||||
tzdata==2024.1
|
tzdata==2024.1
|
||||||
uritemplate==4.1.1
|
uritemplate==4.1.1
|
||||||
urllib3==2.2.3
|
urllib3==2.2.3
|
||||||
whitenoise==6.7.0
|
whitenoise==6.7.0
|
||||||
|
xlrd==2.0.1
|
||||||
|
xlwt==1.3.0
|
||||||
xyzservices==2025.1.0
|
xyzservices==2025.1.0
|
||||||
yarl==1.11.1
|
yarl==1.11.1
|
||||||
|
zeep==4.2.1
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
{% extends "admin/base.html" %}
|
{% extends "admin/base.html" %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
|
|
||||||
|
{% block html_attrs %}
|
||||||
|
lang="fa" dir="rtl"
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
{% block extrastyle %}{{ block.super }}<link rel="stylesheet" type="text/css" href="{% static 'override.css' %}" />
|
{% block extrastyle %}{{ block.super }}<link rel="stylesheet" type="text/css" href="{% static 'override.css' %}" />
|
||||||
<link rel="stylesheet" type="text/css" href="{% static 'fonts.css' %}" />
|
<link rel="stylesheet" type="text/css" href="{% static 'fonts.css' %}" />
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -5,6 +5,10 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% load i18n unfold %}
|
{% load i18n unfold %}
|
||||||
|
|
||||||
|
{% block html_attrs %}
|
||||||
|
lang="fa" dir="rtl"
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block breadcrumbs %}{% endblock %}
|
{% block breadcrumbs %}{% endblock %}
|
||||||
|
|
||||||
{% block title %}
|
{% block title %}
|
||||||
@@ -30,7 +34,7 @@
|
|||||||
|
|
||||||
<div class="flex flex-col gap-8 lg:flex-row">
|
<div class="flex flex-col gap-8 lg:flex-row">
|
||||||
{% for stats in kpi %}
|
{% for stats in kpi %}
|
||||||
{% component "unfold/components/card.html" with class="lg:w-1/3" label=_("Last 7 days") footer=stats.footer %}
|
{% component "unfold/components/card.html" with class="lg:w-1/3" footer=stats.footer %}
|
||||||
{% component "unfold/components/text.html" %}
|
{% component "unfold/components/text.html" %}
|
||||||
{{ stats.title }}
|
{{ stats.title }}
|
||||||
{% endcomponent %}
|
{% endcomponent %}
|
||||||
@@ -44,12 +48,12 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
{% component "unfold/components/card.html" with title=_("Product performance in last 28 days") %}
|
{% component "unfold/components/card.html" with title='بازدید های وبسایت در ماه اخیر' %}
|
||||||
{% component "unfold/components/chart/bar.html" with data=chart height=320 %}{% endcomponent %}
|
{% component "unfold/components/chart/bar.html" with data=chart height=320 %}{% endcomponent %}
|
||||||
{% endcomponent %}
|
{% endcomponent %}
|
||||||
|
|
||||||
<div class="flex flex-col gap-8 lg:flex-row">
|
<div class="flex flex-col gap-8 lg:flex-row">
|
||||||
{% component "unfold/components/card.html" with class="lg:w-1/2" title=_("The most trending products in last 2 weeks") %}
|
{% component "unfold/components/card.html" with class="lg:w-1/2" title='محبوب ترین دسته بندی ها' %}
|
||||||
{% component "unfold/components/title.html" with class="mb-2" %}
|
{% component "unfold/components/title.html" with class="mb-2" %}
|
||||||
$1,234,567.89
|
$1,234,567.89
|
||||||
{% endcomponent %}
|
{% endcomponent %}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
<div class="flex lg:flex-row lg:items-center">
|
<div class="flex lg:flex-row lg:items-center">
|
||||||
{% component "unfold/components/flex.html" with class="flex-col gap-4 lg:flex-row" %}
|
{% component "unfold/components/flex.html" with class="flex-col gap-4 lg:flex-row" %}
|
||||||
{% component "unfold/components/button.html" with href="/admin/order/ordermodel/" %}
|
{% component "unfold/components/button.html" with href="/secret-admin/order/ordermodel/" %}
|
||||||
نمایش سفارشات
|
نمایش سفارشات
|
||||||
{% endcomponent %}
|
{% endcomponent %}
|
||||||
{% endcomponent %}
|
{% endcomponent %}
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
|
|
||||||
<div class="flex lg:flex-row lg:items-center">
|
<div class="flex lg:flex-row lg:items-center">
|
||||||
{% component "unfold/components/flex.html" with class="flex-col gap-4 lg:flex-row" %}
|
{% component "unfold/components/flex.html" with class="flex-col gap-4 lg:flex-row" %}
|
||||||
{% component "unfold/components/button.html" with href="/admin/ticket/ticket/" %}
|
{% component "unfold/components/button.html" with href="/secret-admin/ticket/ticket/" %}
|
||||||
نمایش تیکت ها
|
نمایش تیکت ها
|
||||||
{% endcomponent %}
|
{% endcomponent %}
|
||||||
{% endcomponent %}
|
{% endcomponent %}
|
||||||
|
|||||||
@@ -59,3 +59,7 @@ class MessageAdmin(ModelAdmin, ImportExportModelAdmin):
|
|||||||
def content_display(self, obj):
|
def content_display(self, obj):
|
||||||
return obj.content[0:35] + '...'
|
return obj.content[0:35] + '...'
|
||||||
content_display.short_description = 'محتوای پیام'
|
content_display.short_description = 'محتوای پیام'
|
||||||
|
|
||||||
|
@admin.register(Attachment)
|
||||||
|
class AttachmentAdmin(ModelAdmin, ImportExportModelAdmin):
|
||||||
|
list_display = ['name', 'uploaded_by']
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 5.1.2 on 2025-02-21 18:35
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('ticket', '0011_ticket_content'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='ticket',
|
||||||
|
name='status',
|
||||||
|
field=models.CharField(choices=[('in_progress', 'در حال پردازش'), ('resolved', 'حل شده'), ('closed', 'بسته')], default='open', max_length=20, verbose_name='وضعیت تیکت'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# Generated by Django 5.1.2 on 2025-02-22 22:50
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('ticket', '0012_alter_ticket_status'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Attachment',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('file', models.FileField(upload_to='attachments')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('size', models.PositiveIntegerField(blank=True, null=True)),
|
||||||
|
('name', models.CharField(blank=True, max_length=400, null=True)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='message',
|
||||||
|
name='attachments',
|
||||||
|
field=models.ManyToManyField(blank=True, related_name='messages', to='ticket.attachment'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='ticket',
|
||||||
|
name='attachments',
|
||||||
|
field=models.ManyToManyField(blank=True, related_name='tickets', to='ticket.attachment'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# Generated by Django 5.1.2 on 2025-02-23 18:28
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('ticket', '0013_attachment_message_attachments_ticket_attachments'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='attachment',
|
||||||
|
name='uploaded_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -3,10 +3,26 @@ from account.models import User
|
|||||||
from order.models import OrderModel
|
from order.models import OrderModel
|
||||||
from django_jalali.db import models as jmodels
|
from django_jalali.db import models as jmodels
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class Attachment(models.Model):
|
||||||
|
file = models.FileField(upload_to='attachments')
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
size = models.PositiveIntegerField(null=True, blank=True)
|
||||||
|
name = models.CharField(max_length=400, null=True, blank=True)
|
||||||
|
uploaded_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
|
||||||
|
def __str__(self):
|
||||||
|
return self.file.name
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
if self.file:
|
||||||
|
self.size = self.file.size
|
||||||
|
self.name = self.file.name
|
||||||
|
super(Attachment, self).save(*args, **kwargs)
|
||||||
|
|
||||||
class Ticket(models.Model):
|
class Ticket(models.Model):
|
||||||
objects = jmodels.jManager()
|
objects = jmodels.jManager()
|
||||||
STATUS_CHOICES = [
|
STATUS_CHOICES = [
|
||||||
('open', 'باز'),
|
|
||||||
('in_progress', 'در حال پردازش'),
|
('in_progress', 'در حال پردازش'),
|
||||||
('resolved', 'حل شده'),
|
('resolved', 'حل شده'),
|
||||||
('closed', 'بسته'),
|
('closed', 'بسته'),
|
||||||
@@ -29,6 +45,7 @@ class Ticket(models.Model):
|
|||||||
created_at = jmodels.jDateTimeField(auto_now_add=True, verbose_name='ساخته شده در')
|
created_at = jmodels.jDateTimeField(auto_now_add=True, verbose_name='ساخته شده در')
|
||||||
updated_at = jmodels.jDateTimeField(auto_now=True, verbose_name='اپدیت شده در')
|
updated_at = jmodels.jDateTimeField(auto_now=True, verbose_name='اپدیت شده در')
|
||||||
order = models.ForeignKey(OrderModel ,blank=True, null=True, on_delete=models.SET_NULL)
|
order = models.ForeignKey(OrderModel ,blank=True, null=True, on_delete=models.SET_NULL)
|
||||||
|
attachments = models.ManyToManyField(Attachment, related_name='tickets', blank=True)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.subject
|
return self.subject
|
||||||
@@ -45,6 +62,7 @@ class Message(models.Model):
|
|||||||
sender = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name='ارسال کننده')
|
sender = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name='ارسال کننده')
|
||||||
content = models.TextField(verbose_name='محتوای پیام')
|
content = models.TextField(verbose_name='محتوای پیام')
|
||||||
created_at = jmodels.jDateTimeField(auto_now_add=True, verbose_name='ساخته شده در')
|
created_at = jmodels.jDateTimeField(auto_now_add=True, verbose_name='ساخته شده در')
|
||||||
|
attachments = models.ManyToManyField(Attachment, related_name='messages', blank=True)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"Message by {self.sender.full_name} on {self.ticket.subject}"
|
return f"Message by {self.sender.full_name} on {self.ticket.subject}"
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from .models import Ticket, Message
|
from .models import Ticket, Message, Attachment
|
||||||
from django.utils.timezone import localtime
|
from django.utils.timezone import localtime
|
||||||
from account.serializers import ProfileSerializer
|
from account.serializers import ProfileSerializer
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class MessageSerializer(serializers.ModelSerializer):
|
class MessageSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Message
|
model = Message
|
||||||
@@ -17,8 +20,23 @@ class TicketSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class TicketListSerializer(serializers.ModelSerializer):
|
class TicketListSerializer(serializers.ModelSerializer):
|
||||||
admin = ProfileSerializer(read_only=True)
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Ticket
|
model = Ticket
|
||||||
exclude = ('customer', )
|
exclude = ('customer', 'admin', 'order', 'content')
|
||||||
read_only_fields = ('status', 'admin', )
|
read_only_fields = ('status',)
|
||||||
|
|
||||||
|
|
||||||
|
class AttachmentSerializer(serializers.ModelSerializer):
|
||||||
|
file = serializers.FileField(write_only=True)
|
||||||
|
link = serializers.SerializerMethodField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Attachment
|
||||||
|
fields = ['id', 'name', 'file','link' , 'created_at', 'size']
|
||||||
|
read_only_fields = ('size', 'name', )
|
||||||
|
|
||||||
|
def get_link(self, obj):
|
||||||
|
request = self.context.get('request')
|
||||||
|
if request is not None:
|
||||||
|
return request.build_absolute_uri(obj.file.url)
|
||||||
|
return obj.file.url
|
||||||
+7
-11
@@ -1,15 +1,11 @@
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
from .views import (
|
from . import views
|
||||||
TicketCreateView,
|
|
||||||
TicketListView,
|
|
||||||
TicketDetailView,
|
|
||||||
MessageCreateView,
|
|
||||||
UpdateTicketStatusView
|
|
||||||
)
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('create', TicketCreateView.as_view(), name='ticket-create'),
|
path('create', views.TicketCreateView.as_view(), name='ticket-create'),
|
||||||
path('', TicketListView.as_view(), name='ticket-list'),
|
path('', views.TicketListView.as_view(), name='ticket-list'),
|
||||||
path('<int:pk>', TicketDetailView.as_view(), name='ticket-detail'),
|
path('<int:pk>', views.TicketDetailView.as_view(), name='ticket-detail'),
|
||||||
path('message/<int:pk>', MessageCreateView.as_view(), name='message-create'),
|
path('message/create', views.MessageCreateView.as_view(), name='message-create'),
|
||||||
|
path('attachment/create', views.AttachmentUploadView.as_view(), name='attachment-upload'),
|
||||||
|
path('attachment/delete/<int:pk>', views.AttachmentDeleteView.as_view(), name='attachment-upload'),
|
||||||
]
|
]
|
||||||
+64
-3
@@ -1,10 +1,44 @@
|
|||||||
from rest_framework import generics, permissions
|
from rest_framework import generics, permissions
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
from .models import Ticket, Message
|
from .models import Ticket, Message, Attachment
|
||||||
from .serializers import TicketListSerializer, MessageSerializer, TicketSerializer
|
from .serializers import TicketListSerializer, MessageSerializer, TicketSerializer, AttachmentSerializer
|
||||||
from utils.pagination import StructurePagination
|
from utils.pagination import StructurePagination
|
||||||
from drf_spectacular.utils import extend_schema, OpenApiParameter, OpenApiTypes
|
from drf_spectacular.utils import extend_schema, OpenApiParameter, OpenApiTypes
|
||||||
|
from rest_framework.permissions import IsAuthenticated
|
||||||
|
from rest_framework.parsers import MultiPartParser, FormParser
|
||||||
|
from drf_spectacular.utils import extend_schema, OpenApiParameter, OpenApiExample, OpenApiTypes, OpenApiResponse
|
||||||
|
from rest_framework import status
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
|
|
||||||
|
|
||||||
|
class AttachmentDeleteView(APIView):
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
serializer_class = [AttachmentSerializer]
|
||||||
|
def delete(self, request, pk):
|
||||||
|
attachment_instance = get_object_or_404(Attachment, pk=pk)
|
||||||
|
if attachment_instance.uploaded_by == request.user:
|
||||||
|
attachment_instance.delete()
|
||||||
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
return Response({'detail': 'این فایل توسط شما اپلود نشده'})
|
||||||
|
|
||||||
|
class AttachmentUploadView(APIView):
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
parser_classes = [MultiPartParser, FormParser]
|
||||||
|
|
||||||
|
@extend_schema(
|
||||||
|
request=AttachmentSerializer,
|
||||||
|
responses={201: AttachmentSerializer},
|
||||||
|
description="upload an attachment (file).",
|
||||||
|
)
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
serializer = AttachmentSerializer(data=request.data, context={'request': request})
|
||||||
|
if serializer.is_valid():
|
||||||
|
serializer.save(uploaded_by=request.user)
|
||||||
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class TicketCreateView(generics.CreateAPIView):
|
class TicketCreateView(generics.CreateAPIView):
|
||||||
queryset = Ticket.objects.all()
|
queryset = Ticket.objects.all()
|
||||||
@@ -33,7 +67,26 @@ class TicketListView(APIView):
|
|||||||
description="افستش",
|
description="افستش",
|
||||||
required=False,
|
required=False,
|
||||||
type=OpenApiTypes.INT,
|
type=OpenApiTypes.INT,
|
||||||
)
|
),
|
||||||
|
OpenApiParameter(
|
||||||
|
name="filter",
|
||||||
|
description=(
|
||||||
|
"filter results by one of the following fields:\n"
|
||||||
|
"`in_progress`, `closed`, `resolved`."
|
||||||
|
),
|
||||||
|
required=False,
|
||||||
|
type=OpenApiTypes.STR,
|
||||||
|
),
|
||||||
|
OpenApiParameter(
|
||||||
|
name="sort",
|
||||||
|
description=(
|
||||||
|
"Sort results by one of the following fields:\n"
|
||||||
|
" `created_at`, `-created_at`."
|
||||||
|
"\nPrefix with `-` for descending order."
|
||||||
|
),
|
||||||
|
required=False,
|
||||||
|
type=OpenApiTypes.STR,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
responses={
|
responses={
|
||||||
200: TicketListSerializer(many=True),
|
200: TicketListSerializer(many=True),
|
||||||
@@ -42,6 +95,14 @@ class TicketListView(APIView):
|
|||||||
)
|
)
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
tickets = Ticket.objects.filter(customer=request.user)
|
tickets = Ticket.objects.filter(customer=request.user)
|
||||||
|
filter_by = request.query_params.get('filter', None)
|
||||||
|
sort = request.query_params.get('sort', None)
|
||||||
|
if filter_by:
|
||||||
|
tickets.filter(status=str(filter_by))
|
||||||
|
if sort:
|
||||||
|
if sort not in ['created_at', '-created_at']:
|
||||||
|
return Response({'detail': 'wrong sort paramter'}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
tickets.order_by(sort)
|
||||||
paginator = self.pagination_class()
|
paginator = self.pagination_class()
|
||||||
paginated_tickets = paginator.paginate_queryset(tickets, request)
|
paginated_tickets = paginator.paginate_queryset(tickets, request)
|
||||||
tickets_ser = self.serializer_class(instance=paginated_tickets, many=True, context={'request': request})
|
tickets_ser = self.serializer_class(instance=paginated_tickets, many=True, context={'request': request})
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
# Load environment variables
|
||||||
|
. /etc/profile
|
||||||
|
|
||||||
|
# Debugging output
|
||||||
|
echo "Cron environment:" >> /var/log/cron/cron.log
|
||||||
|
env >> /var/log/cron/cron.log
|
||||||
|
|
||||||
|
|
||||||
|
echo "Starting backup at $(date)" >> /var/log/cron/cron.log
|
||||||
|
|
||||||
|
export PGPASSWORD=$PG_PASSWORD
|
||||||
|
|
||||||
|
TELEGRAM_API="https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendDocument"
|
||||||
|
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
||||||
|
BACKUP_FILE="backup_heymlz_shop${TIMESTAMP}.sql"
|
||||||
|
ZIP_FILE="backup_heymlz_shop${TIMESTAMP}.zip"
|
||||||
|
|
||||||
|
|
||||||
|
pg_dump -h $PG_HOST -p $PG_PORT -U $PG_USER -d $PG_DATABASE -w > /backups/$BACKUP_FILE
|
||||||
|
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "Backup failed!" >> /var/log/cron/cron.log
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
zip -j /backups/$ZIP_FILE /backups/$BACKUP_FILE
|
||||||
|
curl -F chat_id=$TELEGRAM_CHAT_ID \
|
||||||
|
-F document=@/backups/$ZIP_FILE \
|
||||||
|
$TELEGRAM_API
|
||||||
|
|
||||||
|
curl -F chat_id=$TELEGRAM_CHAT_ID2 \
|
||||||
|
-F document=@/backups/$ZIP_FILE \
|
||||||
|
$TELEGRAM_API
|
||||||
|
|
||||||
|
ls -t /backups/*.zip | tail -n +4 | xargs rm -f
|
||||||
|
ls -t /backups/*.sql | tail -n +4 | xargs rm -f
|
||||||
|
echo "Backup completed at $(date)" >> /var/log/cron/cron.log
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
0 3 * * * /app/backup.sh >> /var/log/cron/cron.log 2>&1
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
FROM alpine:3.19
|
||||||
|
|
||||||
|
RUN apk add --no-cache postgresql-client curl zip tzdata
|
||||||
|
RUN ln -sf /usr/share/zoneinfo/UTC /etc/localtime
|
||||||
|
RUN mkdir -p /var/log/cron /backups
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY backup.sh .
|
||||||
|
COPY crontab /etc/crontabs/root
|
||||||
|
|
||||||
|
RUN chmod +x backup.sh && \
|
||||||
|
chmod 0644 /etc/crontabs/root && \
|
||||||
|
touch /var/log/cron/cron.log
|
||||||
|
|
||||||
|
CMD ["busybox", "crond", "-f", "-L", "/var/log/cron/cron.log"]
|
||||||
+19
-2
@@ -42,11 +42,28 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- default
|
- default
|
||||||
|
|
||||||
|
db-backup:
|
||||||
|
build:
|
||||||
|
context: ./backup
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
environment:
|
||||||
|
- PG_HOST=db
|
||||||
|
- PG_PORT=5432
|
||||||
|
- PG_DATABASE=hshop
|
||||||
|
- PG_USER=byeto
|
||||||
|
- PG_PASSWORD=vuhbyq-cypMu0-sirbon
|
||||||
|
- TELEGRAM_BOT_TOKEN=7068288679:AAGecMnyt9A6R78OQu8nQeISMK1LepX718g
|
||||||
|
- TELEGRAM_CHAT_ID=1198382521
|
||||||
|
- TELEGRAM_CHAT_ID2=5115366609
|
||||||
|
volumes:
|
||||||
|
- backups:/backups
|
||||||
|
networks:
|
||||||
|
- default
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
postgres_data:
|
postgres_data:
|
||||||
media_data:
|
media_data:
|
||||||
|
backups:
|
||||||
networks:
|
networks:
|
||||||
default:
|
default:
|
||||||
@@ -6,11 +6,19 @@ import { useToast } from "~/composables/global/useToast";
|
|||||||
// types
|
// types
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
modelValue: File[];
|
modelValue: {
|
||||||
|
id: number;
|
||||||
|
file_link: string;
|
||||||
|
date: string;
|
||||||
|
size: number;
|
||||||
|
name: string;
|
||||||
|
}[];
|
||||||
|
loading?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Emits = {
|
type Emits = {
|
||||||
"update:modelValue": [value: any];
|
"update:modelValue": [value: any];
|
||||||
|
change: [value: File];
|
||||||
};
|
};
|
||||||
|
|
||||||
// props
|
// props
|
||||||
@@ -50,7 +58,8 @@ const onDrop = (files: File[] | null) => {
|
|||||||
} else {
|
} else {
|
||||||
if (modelValue.value.length + files.length <= 3) {
|
if (modelValue.value.length + files.length <= 3) {
|
||||||
files?.forEach((item) => {
|
files?.forEach((item) => {
|
||||||
emit("update:modelValue", [...modelValue.value, item]);
|
emit("change", item);
|
||||||
|
resetFileDialog();
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
addToast({
|
addToast({
|
||||||
@@ -78,22 +87,27 @@ const { isOverDropZone } = useDropZone(dropZoneRef, {
|
|||||||
dataTypes: ["image/jpeg", "image/png", "image/jpg"],
|
dataTypes: ["image/jpeg", "image/png", "image/jpg"],
|
||||||
});
|
});
|
||||||
|
|
||||||
const { open: openDialog, onChange } = useFileDialog({
|
const {
|
||||||
|
open: openDialog,
|
||||||
|
onChange,
|
||||||
|
reset: resetFileDialog,
|
||||||
|
} = useFileDialog({
|
||||||
accept: "image/*",
|
accept: "image/*",
|
||||||
directory: false,
|
directory: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
onChange((files: any) => {
|
onChange((files: any) => {
|
||||||
let arr: File[] = [];
|
let arr: File[] = [];
|
||||||
Object.keys(files).forEach((item) => {
|
Object.keys(files).forEach((_, index) => {
|
||||||
arr.push(files[item]);
|
arr.push(files[index]);
|
||||||
});
|
});
|
||||||
onDrop(arr);
|
onDrop(arr);
|
||||||
});
|
});
|
||||||
|
|
||||||
const deleteFile = (index: number) => {
|
const removeAttachment = (id: number) => {
|
||||||
|
let target = modelValue.value.findIndex((i) => i.id == id);
|
||||||
const clone = [...modelValue.value];
|
const clone = [...modelValue.value];
|
||||||
clone.splice(index, 1);
|
clone.splice(target, 1);
|
||||||
emit("update:modelValue", clone);
|
emit("update:modelValue", clone);
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@@ -103,10 +117,22 @@ const deleteFile = (index: number) => {
|
|||||||
<div
|
<div
|
||||||
ref="dropZoneRef"
|
ref="dropZoneRef"
|
||||||
@click="openDialog"
|
@click="openDialog"
|
||||||
class="bg-slate-50 flex-col-center w-full transition-all text-black/50 gap-3 h-[20rem] border border-dashed rounded-xl cursor-pointer select-none"
|
class="bg-slate-50 relative flex-col-center w-full transition-all text-black/50 gap-3 h-[20rem] border border-dashed rounded-xl cursor-pointer select-none"
|
||||||
:class="isOverDropZone ? 'border-black' : ' border-slate-300'"
|
:class="{
|
||||||
|
'border-black': isOverDropZone,
|
||||||
|
' border-slate-300': !isOverDropZone,
|
||||||
|
'pointer-events-none': loading,
|
||||||
|
}"
|
||||||
>
|
>
|
||||||
<Icon name="bi:file-earmark-arrow-down" size="32" />
|
<Icon
|
||||||
|
:name="
|
||||||
|
loading
|
||||||
|
? 'svg-spinners:ring-resize'
|
||||||
|
: 'bi:file-earmark-arrow-down'
|
||||||
|
"
|
||||||
|
size="32"
|
||||||
|
:class="loading ? '' : ''"
|
||||||
|
/>
|
||||||
<p class="font-bold text-dynamic-primary text-sm lg:text-[1rem]">
|
<p class="font-bold text-dynamic-primary text-sm lg:text-[1rem]">
|
||||||
برای آپلود کلیک کنید یا فایل خود را اینجا بیاندازید
|
برای آپلود کلیک کنید یا فایل خود را اینجا بیاندازید
|
||||||
</p>
|
</p>
|
||||||
@@ -121,24 +147,12 @@ const deleteFile = (index: number) => {
|
|||||||
v-auto-animate
|
v-auto-animate
|
||||||
class="flex flex-row-reverse items-center justify-between w-full px-2 animate__animated animate__fadeIn"
|
class="flex flex-row-reverse items-center justify-between w-full px-2 animate__animated animate__fadeIn"
|
||||||
>
|
>
|
||||||
<li class="w-full flex items-center">
|
<NewAttachment
|
||||||
<div class="flex justify-end w-9/12">
|
:id="item.id"
|
||||||
<p class="text-sm text-black">{{ item.name }}</p>
|
:size="item.size"
|
||||||
</div>
|
:name="item.name"
|
||||||
<div class="w-2/12">
|
@delete="removeAttachment"
|
||||||
<p class="text-sm text-black">
|
/>
|
||||||
{{ (item.size / 1024).toFixed(2) }}KB
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="w-1/12">
|
|
||||||
<Icon
|
|
||||||
name="ci:close"
|
|
||||||
class="**:stroke-red-500 cursor-pointer pb-1"
|
|
||||||
@click="deleteFile(index)"
|
|
||||||
size="28"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|
||||||
// types
|
// types
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -16,18 +15,14 @@ defineProps<Props>();
|
|||||||
|
|
||||||
// state
|
// state
|
||||||
|
|
||||||
const params : any = inject("params");
|
const params: any = inject("params");
|
||||||
|
|
||||||
const page = ref(params?.page ? Number(params.page) : 1);
|
// computed
|
||||||
|
|
||||||
// watch
|
const page = computed({
|
||||||
|
get: () => (params?.page ? Number(params.page) : 1),
|
||||||
watch(
|
set: (value: number) => (params.page = value),
|
||||||
() => page.value,
|
});
|
||||||
(newPage) => {
|
|
||||||
params.page = newPage;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -47,7 +42,11 @@ watch(
|
|||||||
<PaginationNext
|
<PaginationNext
|
||||||
class="w-9 h-9 ml-4 flex items-center justify-center cursor-pointer hover:bg-slate-100 transition disabled:opacity-50 disabled:cursor-not-allowed rounded-lg"
|
class="w-9 h-9 ml-4 flex items-center justify-center cursor-pointer hover:bg-slate-100 transition disabled:opacity-50 disabled:cursor-not-allowed rounded-lg"
|
||||||
>
|
>
|
||||||
<Icon name="ci:chevron-right" class="**:fill-back" size="18px" />
|
<Icon
|
||||||
|
name="ci:chevron-right"
|
||||||
|
class="**:fill-back"
|
||||||
|
size="18px"
|
||||||
|
/>
|
||||||
</PaginationNext>
|
</PaginationNext>
|
||||||
|
|
||||||
<template v-for="(page, index) in items">
|
<template v-for="(page, index) in items">
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ defineProps<Props>();
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="w-full flex-col flex-grow py-[12rem] gap-6 border-2 border-slate-200 border-dashed size-full rounded-100 flex-center"
|
class="w-full flex-col flex-grow py-[12rem] gap-6 border-2 border-slate-200 border-dashed size-full rounded-xl flex-center"
|
||||||
>
|
>
|
||||||
<Icon :name="icon" size="50" class="**:fill-gray-500" />
|
<Icon :name="icon" size="50" class="**:fill-gray-500" />
|
||||||
<span class="text-lg text-gray-500"> {{ title }} </span>
|
<span class="text-lg text-gray-500"> {{ title }} </span>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|
||||||
// import
|
// import
|
||||||
|
|
||||||
import type { ToastOptions } from "~/composables/global/useToast";
|
import type { ToastOptions } from "~/composables/global/useToast";
|
||||||
@@ -9,9 +8,9 @@ import { useToast } from "~/composables/global/useToast";
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
id: number;
|
id: number;
|
||||||
message: string,
|
message: string;
|
||||||
options: ToastOptions
|
options: ToastOptions;
|
||||||
}
|
};
|
||||||
|
|
||||||
// props
|
// props
|
||||||
|
|
||||||
@@ -24,7 +23,7 @@ const { destroyToast } = useToast();
|
|||||||
|
|
||||||
const open = ref(true);
|
const open = ref(true);
|
||||||
|
|
||||||
// method
|
// methods
|
||||||
|
|
||||||
const onSwipeEnd = () => {
|
const onSwipeEnd = () => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -41,37 +40,39 @@ const statusIcon = computed(() => {
|
|||||||
case "success":
|
case "success":
|
||||||
return {
|
return {
|
||||||
name: "duo-icons:check-circle",
|
name: "duo-icons:check-circle",
|
||||||
class: "**:fill-success-500 [filter:drop-shadow(0_0_10px_var(--color-success-500))]"
|
class: "**:fill-success-500 [filter:drop-shadow(0_0_10px_var(--color-success-500))]",
|
||||||
};
|
};
|
||||||
case "error":
|
case "error":
|
||||||
return {
|
return {
|
||||||
name: "duo-icons:alert-triangle",
|
name: "duo-icons:alert-triangle",
|
||||||
class: "**:fill-danger-500 [filter:drop-shadow(0_0_10px_var(--color-danger-500))]"
|
class: "**:fill-danger-500 [filter:drop-shadow(0_0_10px_var(--color-danger-500))]",
|
||||||
};
|
};
|
||||||
case "info":
|
case "info":
|
||||||
return {
|
return {
|
||||||
name: "duo-icons:info",
|
name: "duo-icons:info",
|
||||||
class: "**:fill-cyan-500 [filter:drop-shadow(0_0_10px_var(--color-cyan-500))]"
|
class: "**:fill-cyan-500 [filter:drop-shadow(0_0_10px_var(--color-cyan-500))]",
|
||||||
};
|
};
|
||||||
case "warning":
|
case "warning":
|
||||||
return {
|
return {
|
||||||
name: "duo-icons:alert-octagon",
|
name: "duo-icons:alert-octagon",
|
||||||
class: "**:fill-warning-500 [filter:drop-shadow(0_0_10px_var(--color-warning-500))]"
|
class: "**:fill-warning-500 [filter:drop-shadow(0_0_10px_var(--color-warning-500))]",
|
||||||
};
|
};
|
||||||
default:
|
default:
|
||||||
return {
|
return {
|
||||||
name: "duo-icons:info",
|
name: "duo-icons:info",
|
||||||
class: "**:fill-slate-500 [filter:drop-shadow(0_0_10px_var(--color-slate-500))]"
|
class: "**:fill-slate-500 [filter:drop-shadow(0_0_10px_var(--color-slate-500))]",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// watch
|
// watch
|
||||||
|
|
||||||
watch(() => open.value, (value) => {
|
watch(
|
||||||
if (!value) onSwipeEnd();
|
() => open.value,
|
||||||
});
|
(value) => {
|
||||||
|
if (!value) onSwipeEnd();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// lifecycle
|
// lifecycle
|
||||||
|
|
||||||
@@ -80,7 +81,6 @@ onMounted(() => {
|
|||||||
open.value = false;
|
open.value = false;
|
||||||
}, options.value.duration ?? 4000);
|
}, options.value.duration ?? 4000);
|
||||||
});
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -92,7 +92,7 @@ onMounted(() => {
|
|||||||
:class="options.description ? 'rounded-150' : 'rounded-full'"
|
:class="options.description ? 'rounded-150' : 'rounded-full'"
|
||||||
>
|
>
|
||||||
<ToastTitle
|
<ToastTitle
|
||||||
:class="[ { 'mb-1.5' : options.description } ]"
|
:class="[{ 'mb-1.5': options.description }]"
|
||||||
class="[grid-area:_title] font-medium text-slate-600 text-sm flex items-center gap-2"
|
class="[grid-area:_title] font-medium text-slate-600 text-sm flex items-center gap-2"
|
||||||
>
|
>
|
||||||
<Icon :name="statusIcon.name" :class="statusIcon.class" size="24" />
|
<Icon :name="statusIcon.name" :class="statusIcon.class" size="24" />
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
|
||||||
// types
|
// types
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
selectedSlide: number;
|
selectedSlide: number;
|
||||||
slides: ProductImage[]
|
slides: ProductImage[];
|
||||||
}
|
};
|
||||||
|
|
||||||
// props
|
// props
|
||||||
|
|
||||||
@@ -23,17 +22,18 @@ const selectedSlideDetail = computed(() => {
|
|||||||
})!;
|
})!;
|
||||||
});
|
});
|
||||||
|
|
||||||
// method
|
// methods
|
||||||
|
|
||||||
const changeSlide = (id: number) => {
|
const changeSlide = (id: number) => {
|
||||||
emit("update:selectedSlide", id);
|
emit("update:selectedSlide", id);
|
||||||
};
|
};
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col relative gap-6">
|
<div class="flex flex-col relative gap-6">
|
||||||
<div class="bg-white brightness-[97%] w-full relative aspect-square overflow-hidden rounded-200">
|
<div
|
||||||
|
class="bg-white brightness-[97%] w-full relative aspect-square overflow-hidden rounded-200"
|
||||||
|
>
|
||||||
<Transition name="zoom" mode="out-in">
|
<Transition name="zoom" mode="out-in">
|
||||||
<img
|
<img
|
||||||
:key="selectedSlideDetail.id"
|
:key="selectedSlideDetail.id"
|
||||||
@@ -47,7 +47,11 @@ const changeSlide = (id: number) => {
|
|||||||
<div
|
<div
|
||||||
@click="changeSlide(slide.id)"
|
@click="changeSlide(slide.id)"
|
||||||
v-for="slide in slides"
|
v-for="slide in slides"
|
||||||
:class="selectedSlide === slide.id ? '!ring-black' : 'ring-transparent'"
|
:class="
|
||||||
|
selectedSlide === slide.id
|
||||||
|
? '!ring-black'
|
||||||
|
: 'ring-transparent'
|
||||||
|
"
|
||||||
class="active:scale-95 hover:ring-slate-200 transition-all cursor-pointer brightness-[97%] bg-white aspect-square ring-2 ring-offset-4 rounded-200 w-full overflow-hidden relative"
|
class="active:scale-95 hover:ring-slate-200 transition-all cursor-pointer brightness-[97%] bg-white aspect-square ring-2 ring-offset-4 rounded-200 w-full overflow-hidden relative"
|
||||||
:key="slide.id"
|
:key="slide.id"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -29,17 +29,17 @@ const {
|
|||||||
isPending: isChatPending,
|
isPending: isChatPending,
|
||||||
isFetchingNextPage: isNextChatPagePending,
|
isFetchingNextPage: isNextChatPagePending,
|
||||||
hasNextPage: hasMoreChat,
|
hasNextPage: hasMoreChat,
|
||||||
fetchNextPage: loadMoreChat
|
fetchNextPage: loadMoreChat,
|
||||||
} = useGetChat(id, isOpen);
|
} = useGetChat(id, isOpen);
|
||||||
const isCreateMessagePending = useIsMutating({
|
const isCreateMessagePending = useIsMutating({
|
||||||
mutationKey: [MUTATION_KEYS.create_chat]
|
mutationKey: [MUTATION_KEYS.create_chat],
|
||||||
});
|
});
|
||||||
|
|
||||||
const canLoadMoreChat = ref(false);
|
const canLoadMoreChat = ref(false);
|
||||||
|
|
||||||
const isChatScrollLocked = useScrollLock(chatContainerEl);
|
const isChatScrollLocked = useScrollLock(chatContainerEl);
|
||||||
const { y: chatContainerScrollY } = useScroll(chatContainerEl, {
|
const { y: chatContainerScrollY } = useScroll(chatContainerEl, {
|
||||||
behavior: "smooth"
|
behavior: "smooth",
|
||||||
});
|
});
|
||||||
|
|
||||||
useInfiniteScroll(
|
useInfiniteScroll(
|
||||||
@@ -56,11 +56,11 @@ useInfiniteScroll(
|
|||||||
distance: 10,
|
distance: 10,
|
||||||
direction: "top",
|
direction: "top",
|
||||||
throttle: 1000,
|
throttle: 1000,
|
||||||
canLoadMore: () => canLoadMoreChat.value
|
canLoadMore: () => canLoadMoreChat.value,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// method
|
// methods
|
||||||
|
|
||||||
const scrollToBottom = () => {
|
const scrollToBottom = () => {
|
||||||
chatContainerScrollY.value = chatContainerEl.value?.scrollHeight ?? 0;
|
chatContainerScrollY.value = chatContainerEl.value?.scrollHeight ?? 0;
|
||||||
@@ -116,7 +116,7 @@ whenever(
|
|||||||
}, 2000);
|
}, 2000);
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
once: true
|
once: true,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
@@ -131,16 +131,15 @@ whenever(
|
|||||||
|
|
||||||
<template v-if="isLoggedIn">
|
<template v-if="isLoggedIn">
|
||||||
<Transition name="zoom" mode="out-in">
|
<Transition name="zoom" mode="out-in">
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="!isChatPending"
|
v-if="!isChatPending"
|
||||||
class="p-4.5 h-full flex flex-col justify-between gap-4"
|
class="p-4.5 h-full flex flex-col justify-between gap-4"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
:style="{
|
:style="{
|
||||||
maskImage:
|
maskImage:
|
||||||
'linear-gradient(to top, transparent, black 5%, black, black)',
|
'linear-gradient(to top, transparent, black 5%, black, black)',
|
||||||
}"
|
}"
|
||||||
class="hide-scrollbar flex flex-col py-7 gap-6 h-full overflow-y-auto"
|
class="hide-scrollbar flex flex-col py-7 gap-6 h-full overflow-y-auto"
|
||||||
ref="chatContainerEl"
|
ref="chatContainerEl"
|
||||||
>
|
>
|
||||||
@@ -148,7 +147,10 @@ whenever(
|
|||||||
v-if="hasMoreChat"
|
v-if="hasMoreChat"
|
||||||
class="py-2 flex items-center justify-center"
|
class="py-2 flex items-center justify-center"
|
||||||
>
|
>
|
||||||
<Icon name="svg-spinners:3-dots-fade" size="24" />
|
<Icon
|
||||||
|
name="svg-spinners:3-dots-fade"
|
||||||
|
size="24"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<ChatMessage
|
<ChatMessage
|
||||||
v-for="(message, index) in chatMessages"
|
v-for="(message, index) in chatMessages"
|
||||||
@@ -173,7 +175,6 @@ whenever(
|
|||||||
<ChatInput />
|
<ChatInput />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
class="w-full h-full flex items-center justify-center absolute inset-0"
|
class="w-full h-full flex items-center justify-center absolute inset-0"
|
||||||
@@ -182,7 +183,10 @@ whenever(
|
|||||||
</div>
|
</div>
|
||||||
</Transition>
|
</Transition>
|
||||||
</template>
|
</template>
|
||||||
<div class="text-black p-4.5 size-full flex justify-center items-center" v-else>
|
<div
|
||||||
|
class="text-black p-4.5 size-full flex justify-center items-center"
|
||||||
|
v-else
|
||||||
|
>
|
||||||
Please sign in first
|
Please sign in first
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,24 +1,21 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|
||||||
// state
|
// state
|
||||||
|
|
||||||
const isOpen = ref(false);
|
const isOpen = ref(false);
|
||||||
|
|
||||||
// method
|
// methods
|
||||||
|
|
||||||
const closeChat = () => isOpen.value = false;
|
const closeChat = () => (isOpen.value = false);
|
||||||
|
|
||||||
// provide-inject
|
// provide-inject
|
||||||
|
|
||||||
provide("isOpen", {
|
provide("isOpen", {
|
||||||
isOpen,
|
isOpen,
|
||||||
closeChat
|
closeChat,
|
||||||
});
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
v-if="!isOpen"
|
v-if="!isOpen"
|
||||||
@click="isOpen = !isOpen"
|
@click="isOpen = !isOpen"
|
||||||
@@ -32,5 +29,4 @@ provide("isOpen", {
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<ChatBoxContainer :isOpen="isOpen" />
|
<ChatBoxContainer :isOpen="isOpen" />
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|
||||||
// types
|
// types
|
||||||
|
|
||||||
import AiLoading from "~/components/product/ChatBox/AiLoading.vue";
|
import AiLoading from "~/components/product/ChatBox/AiLoading.vue";
|
||||||
@@ -12,12 +11,12 @@ const { $queryClient: queryClient } = useNuxtApp();
|
|||||||
|
|
||||||
const { addToast } = useToast();
|
const { addToast } = useToast();
|
||||||
|
|
||||||
const { mutateAsync: createMessage, isPending: isCreatingMessage } = useCreateChatMessage(queryClient);
|
const { mutateAsync: createMessage, isPending: isCreatingMessage } =
|
||||||
|
useCreateChatMessage(queryClient);
|
||||||
|
|
||||||
const chatInputEl = ref<HTMLInputElement | null>(null);
|
const chatInputEl = ref<HTMLInputElement | null>(null);
|
||||||
|
|
||||||
|
// methods
|
||||||
// method
|
|
||||||
|
|
||||||
const sendMessage = async () => {
|
const sendMessage = async () => {
|
||||||
const value = chatInputEl.value!.value;
|
const value = chatInputEl.value!.value;
|
||||||
@@ -28,20 +27,18 @@ const sendMessage = async () => {
|
|||||||
|
|
||||||
await createMessage({
|
await createMessage({
|
||||||
new_message: value,
|
new_message: value,
|
||||||
productId: 1
|
productId: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
addToast({
|
addToast({
|
||||||
message: "مشکلی پیش آمده",
|
message: "مشکلی پیش آمده",
|
||||||
options: {
|
options: {
|
||||||
status: "error"
|
status: "error",
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -71,12 +68,20 @@ const sendMessage = async () => {
|
|||||||
|
|
||||||
<form
|
<form
|
||||||
class="transition-all duration-200 relative flex items-center gap-4 w-full shadow-sm rounded-full h-[56px] border pe-3 ps-4"
|
class="transition-all duration-200 relative flex items-center gap-4 w-full shadow-sm rounded-full h-[56px] border pe-3 ps-4"
|
||||||
:class="isCreatingMessage ? 'border-transparent shadow-black/10 bg-white/85 backdrop-blur-xl' : 'border-slate-200 shadow-transparent bg-white'"
|
:class="
|
||||||
|
isCreatingMessage
|
||||||
|
? 'border-transparent shadow-black/10 bg-white/85 backdrop-blur-xl'
|
||||||
|
: 'border-slate-200 shadow-transparent bg-white'
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
ref="chatInputEl"
|
ref="chatInputEl"
|
||||||
:disabled="isCreatingMessage"
|
:disabled="isCreatingMessage"
|
||||||
:placeholder="isCreatingMessage ? 'دارم فکر میکنم...' : 'سوال خود را بپرسید'"
|
:placeholder="
|
||||||
|
isCreatingMessage
|
||||||
|
? 'دارم فکر میکنم...'
|
||||||
|
: 'سوال خود را بپرسید'
|
||||||
|
"
|
||||||
type="text"
|
type="text"
|
||||||
name="text"
|
name="text"
|
||||||
class="focus:outline-none h-full typo-p-sm w-full border-none"
|
class="focus:outline-none h-full typo-p-sm w-full border-none"
|
||||||
@@ -89,13 +94,21 @@ const sendMessage = async () => {
|
|||||||
:class="isCreatingMessage ? 'bg-transparent' : 'bg-black'"
|
:class="isCreatingMessage ? 'bg-transparent' : 'bg-black'"
|
||||||
>
|
>
|
||||||
<TransitionGroup name="fade-down">
|
<TransitionGroup name="fade-down">
|
||||||
<AiLoading v-if="isCreatingMessage" circle :size="75" class="mb-1" />
|
<AiLoading
|
||||||
<Icon v-else name="iconamoon:send-light" class="absolute rotate-180 **:stroke-white" />
|
v-if="isCreatingMessage"
|
||||||
|
circle
|
||||||
|
:size="75"
|
||||||
|
class="mb-1"
|
||||||
|
/>
|
||||||
|
<Icon
|
||||||
|
v-else
|
||||||
|
name="iconamoon:send-light"
|
||||||
|
class="absolute rotate-180 **:stroke-white"
|
||||||
|
/>
|
||||||
</TransitionGroup>
|
</TransitionGroup>
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|
||||||
// types
|
// types
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
id: number,
|
id: number;
|
||||||
reverse?: boolean,
|
reverse?: boolean;
|
||||||
content: string,
|
content: string;
|
||||||
isLast?: boolean,
|
isLast?: boolean;
|
||||||
loadingContent?: boolean
|
loadingContent?: boolean;
|
||||||
}
|
};
|
||||||
|
|
||||||
// props
|
// props
|
||||||
|
|
||||||
@@ -23,20 +22,24 @@ const emit = defineEmits(["textUpdate"]);
|
|||||||
|
|
||||||
const { $gsap: gsap } = useNuxtApp();
|
const { $gsap: gsap } = useNuxtApp();
|
||||||
|
|
||||||
// method
|
// methods
|
||||||
|
|
||||||
const showMessage = () => {
|
const showMessage = () => {
|
||||||
gsap.fromTo(`#message-container-${id.value}`, {
|
gsap.fromTo(
|
||||||
rotateX: -50,
|
`#message-container-${id.value}`,
|
||||||
translateY: -40,
|
{
|
||||||
opacity: 0
|
rotateX: -50,
|
||||||
}, {
|
translateY: -40,
|
||||||
rotateX: 0,
|
opacity: 0,
|
||||||
translateY: 0,
|
},
|
||||||
opacity: 1,
|
{
|
||||||
duration: 0.5,
|
rotateX: 0,
|
||||||
ease: "expo.out"
|
translateY: 0,
|
||||||
});
|
opacity: 1,
|
||||||
|
duration: 0.5,
|
||||||
|
ease: "expo.out",
|
||||||
|
}
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// lifecycle
|
// lifecycle
|
||||||
@@ -47,19 +50,22 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (reverse.value && isLast.value) {
|
if (reverse.value && isLast.value) {
|
||||||
gsap.fromTo(`#chat-message-content-${id.value}`, {
|
gsap.fromTo(
|
||||||
text: "",
|
`#chat-message-content-${id.value}`,
|
||||||
duration: 2.5,
|
{
|
||||||
ease: "none"
|
text: "",
|
||||||
}, {
|
duration: 2.5,
|
||||||
text: { value: content.value, rtl: false },
|
ease: "none",
|
||||||
duration: 2.5,
|
},
|
||||||
ease: "none",
|
{
|
||||||
onUpdate: () => emit("textUpdate")
|
text: { value: content.value, rtl: false },
|
||||||
});
|
duration: 2.5,
|
||||||
|
ease: "none",
|
||||||
|
onUpdate: () => emit("textUpdate"),
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -81,7 +87,11 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="rounded-150 px-4 py-3"
|
class="rounded-150 px-4 py-3"
|
||||||
:class="reverse ? 'bg-slate-100 text-slate-600' : 'bg-black text-white'"
|
:class="
|
||||||
|
reverse
|
||||||
|
? 'bg-slate-100 text-slate-600'
|
||||||
|
: 'bg-black text-white'
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<p
|
<p
|
||||||
v-if="!loadingContent"
|
v-if="!loadingContent"
|
||||||
@@ -91,11 +101,7 @@ onMounted(() => {
|
|||||||
{{ content }}
|
{{ content }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<Icon
|
<Icon v-else name="svg-spinners:3-dots-bounce" size="20" />
|
||||||
v-else
|
|
||||||
name="svg-spinners:3-dots-bounce"
|
|
||||||
size="20"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|
||||||
// import
|
// import
|
||||||
|
|
||||||
import useGetComments from "~/composables/api/product/useGetComments";
|
import useGetComments from "~/composables/api/product/useGetComments";
|
||||||
@@ -16,15 +15,16 @@ const page = ref(1);
|
|||||||
const { token } = useAuth();
|
const { token } = useAuth();
|
||||||
const userComment = ref("");
|
const userComment = ref("");
|
||||||
|
|
||||||
const { data: comments, refetch : refetchComments } = useGetComments(id, page);
|
const { data: comments, refetch: refetchComments } = useGetComments(id, page);
|
||||||
const { mutateAsync: createComment, isPending: isCreateCommentPending } = useCreateComment(id);
|
const { mutateAsync: createComment, isPending: isCreateCommentPending } =
|
||||||
|
useCreateComment(id);
|
||||||
|
|
||||||
// method
|
// methods
|
||||||
|
|
||||||
const submitComment = async () => {
|
const submitComment = async () => {
|
||||||
if (userComment.value.length > 3) {
|
if (userComment.value.length > 3) {
|
||||||
await createComment({
|
await createComment({
|
||||||
content: userComment.value
|
content: userComment.value,
|
||||||
});
|
});
|
||||||
|
|
||||||
userComment.value = "";
|
userComment.value = "";
|
||||||
@@ -32,23 +32,25 @@ const submitComment = async () => {
|
|||||||
await refetchComments();
|
await refetchComments();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="bg-slate-50">
|
<section class="bg-slate-50">
|
||||||
<div class="flex relative gap-8 my-42 container">
|
<div class="flex relative gap-8 my-42 container">
|
||||||
<div class="sticky top-0 flex flex-col gap-6 min-w-[400px] max-h-min bg-white p-8 rounded-xl border-[0.5px] border-slate-200">
|
<div
|
||||||
<h3 class="typo-h-3">
|
class="sticky top-0 flex flex-col gap-6 min-w-[400px] max-h-min bg-white p-8 rounded-xl border-[0.5px] border-slate-200"
|
||||||
نظرات کاربران
|
>
|
||||||
</h3>
|
<h3 class="typo-h-3">نظرات کاربران</h3>
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<Rating />
|
<Rating />
|
||||||
<span class="typo-p-sm">
|
<span class="typo-p-sm">
|
||||||
بر اساس {{ comments?.count }} نظر
|
بر اساس {{ comments?.count }} نظر
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<form @submit.prevent="submitComment" class="flex flex-col gap-6">
|
<form
|
||||||
|
@submit.prevent="submitComment"
|
||||||
|
class="flex flex-col gap-6"
|
||||||
|
>
|
||||||
<textarea
|
<textarea
|
||||||
:disabled="!token"
|
:disabled="!token"
|
||||||
class="w-full min-h-[200px] field-sizing-content rounded-xl bg-white p-4 border border-slate-200"
|
class="w-full min-h-[200px] field-sizing-content rounded-xl bg-white p-4 border border-slate-200"
|
||||||
@@ -65,10 +67,7 @@ const submitComment = async () => {
|
|||||||
نظر بنویسید
|
نظر بنویسید
|
||||||
</Button>
|
</Button>
|
||||||
<NuxtLink v-else to="/signin">
|
<NuxtLink v-else to="/signin">
|
||||||
<Button
|
<Button type="button" class="rounded-full w-full">
|
||||||
type="button"
|
|
||||||
class="rounded-full w-full"
|
|
||||||
>
|
|
||||||
وارد شوید
|
وارد شوید
|
||||||
</Button>
|
</Button>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
@@ -90,7 +89,6 @@ const submitComment = async () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
@@ -52,17 +52,28 @@ await suspense();
|
|||||||
<div class="flex items-center justify-between w-full">
|
<div class="flex items-center justify-between w-full">
|
||||||
<div class="flex flex-col items-start gap-1">
|
<div class="flex flex-col items-start gap-1">
|
||||||
<span class="flex font-semibold text-dynamic-primary">
|
<span class="flex font-semibold text-dynamic-primary">
|
||||||
<p>
|
<p v-if="account?.first_name">
|
||||||
{{ `${account?.first_name} ${account?.last_name}` }}
|
{{ `${account?.first_name} ${account?.last_name}` }}
|
||||||
</p>
|
</p>
|
||||||
|
<p v-else>بدون نام کاربری</p>
|
||||||
</span>
|
</span>
|
||||||
<button class="text-xs font-semibold text-cyan-500">
|
<NuxtLink
|
||||||
|
:to="{ name: 'profile' }"
|
||||||
|
class="text-xs font-semibold text-cyan-500"
|
||||||
|
>
|
||||||
ویرایش اطلاعات
|
ویرایش اطلاعات
|
||||||
</button>
|
</NuxtLink>
|
||||||
</div>
|
</div>
|
||||||
<img
|
<Avatar
|
||||||
src="https://shatelpart.com/storage/users/user-15-155Mn1.png"
|
class="!size-[3rem]"
|
||||||
class="rounded-full size-[3rem] hover:border-dynamic-rose transition"
|
:src="account!.profile_photo"
|
||||||
|
:alt="
|
||||||
|
account?.first_name && account?.last_name
|
||||||
|
? `${account?.first_name.charAt(
|
||||||
|
0
|
||||||
|
)} ${account?.last_name.charAt(0)}`
|
||||||
|
: 'بدون نام کاربری'
|
||||||
|
"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,14 @@
|
|||||||
<script setup lang="ts"></script>
|
<script setup lang="ts">
|
||||||
|
// types
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
data: Ticket;
|
||||||
|
};
|
||||||
|
|
||||||
|
// props
|
||||||
|
|
||||||
|
defineProps<Props>();
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<tr
|
<tr
|
||||||
@@ -8,13 +18,15 @@
|
|||||||
scope="row"
|
scope="row"
|
||||||
class="w-3/12 px-6 py-6 font-medium whitespace-nowrap text-black"
|
class="w-3/12 px-6 py-6 font-medium whitespace-nowrap text-black"
|
||||||
>
|
>
|
||||||
Apple MacBook Pro 17
|
{{ data.ticket_category }}
|
||||||
</td>
|
</td>
|
||||||
<td class="w-3/12 px-6 py-6">Silver</td>
|
<td class="w-3/12 px-6 py-6">{{ data.subject }}</td>
|
||||||
<td class="w-3/12 px-6 py-6">Laptop</td>
|
<td class="w-3/12 px-6 py-6">{{ data.created_at }}</td>
|
||||||
<td class="w-2/12 px-6 py-6">$2999</td>
|
<td class="w-2/12 px-6 py-6">{{ data.status }}</td>
|
||||||
<td class="w-1/12 px-6 py-6">
|
<td class="w-1/12 px-6 py-6">
|
||||||
<NuxtLink :to="{ name: 'profile-tickets-id', params: { id: 1 } }">
|
<NuxtLink
|
||||||
|
:to="{ name: 'profile-tickets-id', params: { id: data.id } }"
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
class="size-10 flex-center border border-slate-200 rounded-md"
|
class="size-10 flex-center border border-slate-200 rounded-md"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<script setup lang="ts"></script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<tr
|
||||||
|
class="odd:bg-white even:bg-gray-50 last:border-none border-b border-slate-200"
|
||||||
|
>
|
||||||
|
<td
|
||||||
|
scope="row"
|
||||||
|
class="w-3/12 px-6 py-6 font-medium whitespace-nowrap text-black"
|
||||||
|
>
|
||||||
|
<Skeleton class="w-full !h-10 !rounded-sm" />
|
||||||
|
</td>
|
||||||
|
<td class="w-3/12 px-6 py-6">
|
||||||
|
<Skeleton class="w-full !h-10 !rounded-sm" />
|
||||||
|
</td>
|
||||||
|
<td class="w-3/12 px-6 py-6">
|
||||||
|
<Skeleton class="w-full !h-10 !rounded-sm" />
|
||||||
|
</td>
|
||||||
|
<td class="w-2/12 px-6 py-6">
|
||||||
|
<Skeleton class="w-full !h-10 !rounded-sm" />
|
||||||
|
</td>
|
||||||
|
<td class="w-1/12 px-6 py-6">
|
||||||
|
<Skeleton class="!size-10 !rounded-sm" />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
// imports
|
||||||
|
|
||||||
|
import useDeleteAttachment from "~/composables/api/tickets/useDeleteAttachment";
|
||||||
|
|
||||||
|
// types
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
id: number;
|
||||||
|
size: number;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Emits = {
|
||||||
|
delete: [value: number];
|
||||||
|
};
|
||||||
|
|
||||||
|
// props
|
||||||
|
|
||||||
|
defineProps<Props>();
|
||||||
|
|
||||||
|
// Emits
|
||||||
|
|
||||||
|
const emit = defineEmits<Emits>();
|
||||||
|
|
||||||
|
// queries
|
||||||
|
|
||||||
|
const { mutateAsync: deleteAttachment, isPending: deleteAttachmentPending } =
|
||||||
|
useDeleteAttachment();
|
||||||
|
|
||||||
|
const handleDeleteAttachment = (id: number) => {
|
||||||
|
deleteAttachment(
|
||||||
|
{ id },
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
emit("delete", id);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<li class="w-full flex items-center">
|
||||||
|
<div class="w-2/12 flex justify-start">
|
||||||
|
<p class="text-sm text-black">{{ (size / 1024).toFixed(2) }}KB</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-end w-9/12">
|
||||||
|
<p class="text-sm text-black">
|
||||||
|
{{ name }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="w-1/12 ps-1">
|
||||||
|
<button class="cursor-pointer" @click="handleDeleteAttachment(id)">
|
||||||
|
<Icon
|
||||||
|
:name="
|
||||||
|
deleteAttachmentPending
|
||||||
|
? 'svg-spinners:ring-resize'
|
||||||
|
: 'ci:close'
|
||||||
|
"
|
||||||
|
:class="
|
||||||
|
deleteAttachmentPending
|
||||||
|
? 'text-black/50'
|
||||||
|
: '**:stroke-red-500'
|
||||||
|
"
|
||||||
|
class="pt-1"
|
||||||
|
size="28"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
@@ -5,17 +5,17 @@ import { API_ENDPOINTS } from "~/constants";
|
|||||||
|
|
||||||
// types
|
// types
|
||||||
|
|
||||||
export type CreateOrUpdateAddressRequest = Omit<Address, "is_main">;
|
export type CreateOrUpdateAddressResponse = Omit<Address, "is_main">;
|
||||||
|
|
||||||
const useCreateOrUpdateAddress = (update: ComputedRef<Boolean>) => {
|
const useCreateOrUpdateAddress = (update: ComputedRef<Boolean>) => {
|
||||||
// state
|
// state
|
||||||
|
|
||||||
const { $axios: axios } = useNuxtApp();
|
const { $axios: axios } = useNuxtApp();
|
||||||
|
|
||||||
// method
|
// methods
|
||||||
|
|
||||||
const handleCreateOrUpdateAddress = async (
|
const handleCreateOrUpdateAddress = async (
|
||||||
addressData: CreateOrUpdateAddressRequest
|
addressData: CreateOrUpdateAddressResponse
|
||||||
) => {
|
) => {
|
||||||
const { data } = await axios[update.value ? "put" : "post"](
|
const { data } = await axios[update.value ? "put" : "post"](
|
||||||
update.value
|
update.value
|
||||||
@@ -30,7 +30,7 @@ const useCreateOrUpdateAddress = (update: ComputedRef<Boolean>) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: (addressData: CreateOrUpdateAddressRequest) =>
|
mutationFn: (addressData: CreateOrUpdateAddressResponse) =>
|
||||||
handleCreateOrUpdateAddress(addressData),
|
handleCreateOrUpdateAddress(addressData),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ const useDeleteAddress = () => {
|
|||||||
|
|
||||||
const { $axios: axios } = useNuxtApp();
|
const { $axios: axios } = useNuxtApp();
|
||||||
|
|
||||||
// method
|
// methods
|
||||||
|
|
||||||
const handleDeleteAddress = async (id: number) => {
|
const handleDeleteAddress = async (id: number) => {
|
||||||
const { data } = await axios.delete(
|
const { data } = await axios.delete(
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ const useUpdateAccount = () => {
|
|||||||
|
|
||||||
const { $axios: axios } = useNuxtApp();
|
const { $axios: axios } = useNuxtApp();
|
||||||
|
|
||||||
// method
|
// methods
|
||||||
|
|
||||||
const handleUpdateAccount = async (params: UpdateAccountRequest) => {
|
const handleUpdateAccount = async (params: UpdateAccountRequest) => {
|
||||||
const { data } = await axios.patch(
|
const { data } = await axios.patch(
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
export const useAuth = () => {
|
export const useAuth = () => {
|
||||||
|
|
||||||
// state
|
// state
|
||||||
|
|
||||||
const token = useCookie("token");
|
const token = useCookie("token");
|
||||||
const refreshToken = useCookie("refresh-token");
|
const refreshToken = useCookie("refresh-token");
|
||||||
|
|
||||||
// method
|
// methods
|
||||||
|
|
||||||
const updateToken = (newToken: string) => {
|
const updateToken = (newToken: string) => {
|
||||||
token.value = newToken;
|
token.value = newToken;
|
||||||
@@ -15,7 +14,7 @@ export const useAuth = () => {
|
|||||||
refreshToken.value = newToken;
|
refreshToken.value = newToken;
|
||||||
};
|
};
|
||||||
|
|
||||||
const logout = (reload ?: boolean) => {
|
const logout = (reload?: boolean) => {
|
||||||
token.value = undefined;
|
token.value = undefined;
|
||||||
refreshToken.value = undefined;
|
refreshToken.value = undefined;
|
||||||
if (reload) window.location.reload();
|
if (reload) window.location.reload();
|
||||||
@@ -25,6 +24,12 @@ export const useAuth = () => {
|
|||||||
|
|
||||||
const isLoggedIn = computed(() => !!token.value);
|
const isLoggedIn = computed(() => !!token.value);
|
||||||
|
|
||||||
return { token, refreshToken, updateRefreshToken, updateToken, logout, isLoggedIn };
|
return {
|
||||||
|
token,
|
||||||
|
refreshToken,
|
||||||
|
updateRefreshToken,
|
||||||
|
updateToken,
|
||||||
|
logout,
|
||||||
|
isLoggedIn,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
@@ -6,17 +6,15 @@ import { API_ENDPOINTS } from "~/constants";
|
|||||||
// types
|
// types
|
||||||
|
|
||||||
export type RefreshAuthRequest = {
|
export type RefreshAuthRequest = {
|
||||||
refresh: string,
|
refresh: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type RefreshAuthResponse = {
|
export type RefreshAuthResponse = {
|
||||||
access: string,
|
access: string;
|
||||||
refresh: string,
|
refresh: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const useRefreshAuth = () => {
|
const useRefreshAuth = () => {
|
||||||
|
|
||||||
// state
|
// state
|
||||||
|
|
||||||
const { $axios: axios } = useNuxtApp();
|
const { $axios: axios } = useNuxtApp();
|
||||||
@@ -24,12 +22,16 @@ const useRefreshAuth = () => {
|
|||||||
// methods
|
// methods
|
||||||
|
|
||||||
const handleRefreshAuth = async (variables: RefreshAuthRequest) => {
|
const handleRefreshAuth = async (variables: RefreshAuthRequest) => {
|
||||||
const { data } = await axios.post<RefreshAuthResponse>(`${API_ENDPOINTS.auth.refresh}/`, variables);
|
const { data } = await axios.post<RefreshAuthResponse>(
|
||||||
|
`${API_ENDPOINTS.auth.refresh}`,
|
||||||
|
variables
|
||||||
|
);
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: (variables: RefreshAuthRequest) => handleRefreshAuth(variables)
|
mutationFn: (variables: RefreshAuthRequest) =>
|
||||||
|
handleRefreshAuth(variables),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -11,13 +11,11 @@ export type SignInRequest = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type SignInResponse = {
|
export type SignInResponse = {
|
||||||
access: string,
|
access: string;
|
||||||
refresh: string,
|
refresh: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const useSignIn = () => {
|
const useSignIn = () => {
|
||||||
|
|
||||||
// state
|
// state
|
||||||
|
|
||||||
const { $axios: axios } = useNuxtApp();
|
const { $axios: axios } = useNuxtApp();
|
||||||
@@ -25,12 +23,15 @@ const useSignIn = () => {
|
|||||||
// methods
|
// methods
|
||||||
|
|
||||||
const handleSignIn = async (variables: SignInRequest) => {
|
const handleSignIn = async (variables: SignInRequest) => {
|
||||||
const { data } = await axios.post<SignInResponse>(`${API_ENDPOINTS.auth.signin}/`, variables);
|
const { data } = await axios.post<SignInResponse>(
|
||||||
|
`${API_ENDPOINTS.auth.signin}`,
|
||||||
|
variables
|
||||||
|
);
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: (variables: SignInRequest) => handleSignIn(variables)
|
mutationFn: (variables: SignInRequest) => handleSignIn(variables),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -11,64 +11,37 @@ export type CreateChatMessageRequest = {
|
|||||||
new_message: string;
|
new_message: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CreateChatMessageResponse = Chat[]
|
export type CreateChatMessageResponse = Chat[];
|
||||||
|
|
||||||
const useCreateChatMessage = (queryClient: QueryClient) => {
|
const useCreateChatMessage = (queryClient: QueryClient) => {
|
||||||
|
|
||||||
// state
|
// state
|
||||||
|
|
||||||
const { $axios: axios } = useNuxtApp();
|
const { $axios: axios } = useNuxtApp();
|
||||||
|
|
||||||
// method
|
// methods
|
||||||
|
|
||||||
const handleCreateChatMessage = async (variables: CreateChatMessageRequest) => {
|
const handleCreateChatMessage = async (
|
||||||
|
variables: CreateChatMessageRequest
|
||||||
const { data } = await axios.post<CreateChatMessageResponse>(`${API_ENDPOINTS.chat.new_message}/${variables.productId}`, variables);
|
) => {
|
||||||
|
const { data } = await axios.post<CreateChatMessageResponse>(
|
||||||
|
`${API_ENDPOINTS.chat.new_message}/${variables.productId}`,
|
||||||
|
variables
|
||||||
|
);
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationKey: [MUTATION_KEYS.create_chat],
|
mutationKey: [MUTATION_KEYS.create_chat],
|
||||||
mutationFn: (variables: CreateChatMessageRequest) => handleCreateChatMessage(variables),
|
mutationFn: (variables: CreateChatMessageRequest) =>
|
||||||
|
handleCreateChatMessage(variables),
|
||||||
onMutate: (newMessage) => {
|
onMutate: (newMessage) => {
|
||||||
const prevData = queryClient.getQueriesData({ queryKey: [QUERY_KEYS.chat] });
|
const prevData = queryClient.getQueriesData({
|
||||||
|
queryKey: [QUERY_KEYS.chat],
|
||||||
queryClient.setQueryData<InfiniteData<ApiPaginated<Chat>>>([QUERY_KEYS.chat], (oldData) => {
|
|
||||||
const lastPage = oldData!.pages[oldData!.pages.length - 1];
|
|
||||||
|
|
||||||
return {
|
|
||||||
pages: [
|
|
||||||
{
|
|
||||||
count: lastPage.count,
|
|
||||||
next: lastPage.next,
|
|
||||||
previous: lastPage.previous,
|
|
||||||
results: [
|
|
||||||
{
|
|
||||||
id: Date.now(),
|
|
||||||
content: newMessage.new_message,
|
|
||||||
sender: "user"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
...oldData!.pages
|
|
||||||
],
|
|
||||||
pageParams: [
|
|
||||||
...oldData!.pageParams,
|
|
||||||
{
|
|
||||||
limit: 10,
|
|
||||||
offset: 0
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
queryClient.setQueryData<InfiniteData<ApiPaginated<Chat>>>(
|
||||||
return { prevData: prevData ? prevData[0][1] : undefined };
|
[QUERY_KEYS.chat],
|
||||||
},
|
(oldData) => {
|
||||||
onSuccess: (response) => {
|
|
||||||
|
|
||||||
queryClient.setQueryData<InfiniteData<ApiPaginated<Chat>>>([QUERY_KEYS.chat], (oldData) => {
|
|
||||||
if (oldData) {
|
|
||||||
const lastPage = oldData!.pages[oldData!.pages.length - 1];
|
const lastPage = oldData!.pages[oldData!.pages.length - 1];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -77,38 +50,72 @@ const useCreateChatMessage = (queryClient: QueryClient) => {
|
|||||||
count: lastPage.count,
|
count: lastPage.count,
|
||||||
next: lastPage.next,
|
next: lastPage.next,
|
||||||
previous: lastPage.previous,
|
previous: lastPage.previous,
|
||||||
results: {
|
results: [
|
||||||
...response[0],
|
{
|
||||||
id: Date.now()
|
id: Date.now(),
|
||||||
}
|
content: newMessage.new_message,
|
||||||
|
sender: "user",
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
...oldData!.pages
|
...oldData!.pages,
|
||||||
],
|
],
|
||||||
pageParams: [
|
pageParams: [
|
||||||
...oldData!.pageParams,
|
...oldData!.pageParams,
|
||||||
{
|
{
|
||||||
limit: 10,
|
limit: 10,
|
||||||
offset: 0
|
offset: 0,
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return oldData;
|
return { prevData: prevData ? prevData[0][1] : undefined };
|
||||||
});
|
},
|
||||||
|
onSuccess: (response) => {
|
||||||
|
queryClient.setQueryData<InfiniteData<ApiPaginated<Chat>>>(
|
||||||
|
[QUERY_KEYS.chat],
|
||||||
|
(oldData) => {
|
||||||
|
if (oldData) {
|
||||||
|
const lastPage =
|
||||||
|
oldData!.pages[oldData!.pages.length - 1];
|
||||||
|
|
||||||
|
return {
|
||||||
|
pages: [
|
||||||
|
{
|
||||||
|
count: lastPage.count,
|
||||||
|
next: lastPage.next,
|
||||||
|
previous: lastPage.previous,
|
||||||
|
results: {
|
||||||
|
...response[0],
|
||||||
|
id: Date.now(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...oldData!.pages,
|
||||||
|
],
|
||||||
|
pageParams: [
|
||||||
|
...oldData!.pageParams,
|
||||||
|
{
|
||||||
|
limit: 10,
|
||||||
|
offset: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return oldData;
|
||||||
|
}
|
||||||
|
);
|
||||||
},
|
},
|
||||||
onError: (err, newMessage, context) => {
|
onError: (err, newMessage, context) => {
|
||||||
if (context) {
|
if (context) {
|
||||||
queryClient.setQueryData(
|
queryClient.setQueryData([QUERY_KEYS.chat], context.prevData);
|
||||||
[QUERY_KEYS.chat],
|
|
||||||
context.prevData
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onSettled: (newMessage) => {
|
onSettled: (newMessage) => {
|
||||||
queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.chat] });
|
queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.chat] });
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -8,34 +8,36 @@ import { useAuth } from "~/composables/api/auth/useAuth";
|
|||||||
|
|
||||||
export type GetBranchResponse = ApiPaginated<Chat>;
|
export type GetBranchResponse = ApiPaginated<Chat>;
|
||||||
|
|
||||||
const useGetBranch = (
|
const useGetBranch = (productId: string | number, enabled: Ref<boolean>) => {
|
||||||
productId: string | number,
|
|
||||||
enabled: Ref<boolean>
|
|
||||||
) => {
|
|
||||||
|
|
||||||
// state
|
// state
|
||||||
|
|
||||||
const { $axios: axios } = useNuxtApp();
|
const { $axios: axios } = useNuxtApp();
|
||||||
|
|
||||||
const { isLoggedIn } = useAuth();
|
const { isLoggedIn } = useAuth();
|
||||||
|
|
||||||
// method
|
// methods
|
||||||
|
|
||||||
const handleGetChat = async ({ productId, limit, offset }: {
|
const handleGetChat = async ({
|
||||||
productId: number | string,
|
productId,
|
||||||
limit: number,
|
limit,
|
||||||
offset: number
|
offset,
|
||||||
|
}: {
|
||||||
|
productId: number | string;
|
||||||
|
limit: number;
|
||||||
|
offset: number;
|
||||||
}) => {
|
}) => {
|
||||||
|
const { data } = await axios.get<GetBranchResponse>(
|
||||||
const { data } = await axios.get<GetBranchResponse>(`${API_ENDPOINTS.chat.messages}/${productId}`, {
|
`${API_ENDPOINTS.chat.messages}/${productId}`,
|
||||||
params: {
|
{
|
||||||
offset,
|
params: {
|
||||||
limit
|
offset,
|
||||||
},
|
limit,
|
||||||
headers: {
|
},
|
||||||
Authorization: `Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoyMTY3ODE2OTAwLCJpYXQiOjE3MzU4MTY5MDAsImp0aSI6ImQwN2E2Y2Y2NzgwZjRlNTE5NWIzOGQxMTAzYzU4NDQ3IiwidXNlcl9pZCI6NX0.slwd7ZSV7nUXEuDTYwwHUOo9ekCefwEEL4kVv2vSTFo`
|
headers: {
|
||||||
|
Authorization: `Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoyMTY3ODE2OTAwLCJpYXQiOjE3MzU4MTY5MDAsImp0aSI6ImQwN2E2Y2Y2NzgwZjRlNTE5NWIzOGQxMTAzYzU4NDQ3IiwidXNlcl9pZCI6NX0.slwd7ZSV7nUXEuDTYwwHUOo9ekCefwEEL4kVv2vSTFo`,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
});
|
);
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -44,21 +46,22 @@ const useGetBranch = (
|
|||||||
queryKey: [QUERY_KEYS.chat],
|
queryKey: [QUERY_KEYS.chat],
|
||||||
initialPageParam: {
|
initialPageParam: {
|
||||||
limit: 10,
|
limit: 10,
|
||||||
offset: 0
|
offset: 0,
|
||||||
},
|
},
|
||||||
queryFn: ({ pageParam }) => handleGetChat({
|
queryFn: ({ pageParam }) =>
|
||||||
limit: pageParam.limit,
|
handleGetChat({
|
||||||
offset: pageParam.offset,
|
limit: pageParam.limit,
|
||||||
productId: productId
|
offset: pageParam.offset,
|
||||||
}),
|
productId: productId,
|
||||||
|
}),
|
||||||
getNextPageParam: (lastPage, pages) => {
|
getNextPageParam: (lastPage, pages) => {
|
||||||
if (!lastPage.next) return undefined;
|
if (!lastPage.next) return undefined;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
limit: 10,
|
limit: 10,
|
||||||
offset: pages.length * 10
|
offset: pages.length * 10,
|
||||||
};
|
};
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -6,24 +6,27 @@ import { API_ENDPOINTS } from "~/constants";
|
|||||||
// types
|
// types
|
||||||
|
|
||||||
export type CreateCommentRequest = {
|
export type CreateCommentRequest = {
|
||||||
content: string
|
content: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const useCreateComment = (id: number | string | undefined) => {
|
const useCreateComment = (id: number | string | undefined) => {
|
||||||
|
|
||||||
// state
|
// state
|
||||||
|
|
||||||
const { $axios: axios } = useNuxtApp();
|
const { $axios: axios } = useNuxtApp();
|
||||||
|
|
||||||
// method
|
// methods
|
||||||
|
|
||||||
const handleCreateComment = async (variables: CreateCommentRequest) => {
|
const handleCreateComment = async (variables: CreateCommentRequest) => {
|
||||||
const { data } = await axios.post(`${API_ENDPOINTS.product.create_comment}/${id}`, variables);
|
const { data } = await axios.post(
|
||||||
|
`${API_ENDPOINTS.product.create_comment}/${id}`,
|
||||||
|
variables
|
||||||
|
);
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: (variables: CreateCommentRequest) => handleCreateComment(variables)
|
mutationFn: (variables: CreateCommentRequest) =>
|
||||||
|
handleCreateComment(variables),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
// imports
|
||||||
|
|
||||||
|
import { useMutation } from "@tanstack/vue-query";
|
||||||
|
import { API_ENDPOINTS } from "~/constants";
|
||||||
|
|
||||||
|
// types
|
||||||
|
|
||||||
|
export type CreateTicketRequest = {
|
||||||
|
ticket_category: string | undefined;
|
||||||
|
order: number | undefined;
|
||||||
|
subject: string;
|
||||||
|
content: string;
|
||||||
|
attachments: {
|
||||||
|
id: number;
|
||||||
|
file_link: string;
|
||||||
|
date: string;
|
||||||
|
size: number;
|
||||||
|
name: string;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const useCreateTicket = () => {
|
||||||
|
// state
|
||||||
|
|
||||||
|
const { $axios: axios } = useNuxtApp();
|
||||||
|
|
||||||
|
// methods
|
||||||
|
|
||||||
|
const handleCreateTicket = async (params: CreateTicketRequest) => {
|
||||||
|
const { data } = await axios.post(
|
||||||
|
API_ENDPOINTS.account.address.update,
|
||||||
|
{
|
||||||
|
...params,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "multipart/form-data",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (ticketData: CreateTicketRequest) =>
|
||||||
|
handleCreateTicket(ticketData),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useCreateTicket;
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
// imports
|
||||||
|
|
||||||
|
import { useMutation } from "@tanstack/vue-query";
|
||||||
|
import { API_ENDPOINTS } from "~/constants";
|
||||||
|
|
||||||
|
// types
|
||||||
|
|
||||||
|
export type DeleteAttachmentRequest = {
|
||||||
|
id: number | string;
|
||||||
|
};
|
||||||
|
|
||||||
|
// methods
|
||||||
|
|
||||||
|
export const handleDeleteAttachment = async ({
|
||||||
|
id,
|
||||||
|
}: DeleteAttachmentRequest) => {
|
||||||
|
// state
|
||||||
|
|
||||||
|
const { $axios: axios } = useNuxtApp();
|
||||||
|
|
||||||
|
const { data } = await axios.delete(
|
||||||
|
`${API_ENDPOINTS.tickets.delete_attachment}/${id}`
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
// composable
|
||||||
|
|
||||||
|
const useDeleteAttachment = () => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (data: DeleteAttachmentRequest) =>
|
||||||
|
handleDeleteAttachment({ ...data }),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useDeleteAttachment;
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
// imports
|
||||||
|
|
||||||
|
import { useQuery } from "@tanstack/vue-query";
|
||||||
|
import { API_ENDPOINTS, QUERY_KEYS } from "~/constants";
|
||||||
|
|
||||||
|
// types
|
||||||
|
|
||||||
|
export type GetAllTicketsResponse = ApiPaginated<Ticket>;
|
||||||
|
|
||||||
|
export type GetAllTicketsFilters = {
|
||||||
|
sort: string | undefined;
|
||||||
|
filter: string | undefined;
|
||||||
|
page: string | string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const useGetAllTickets = (params: Ref<GetAllTicketsFilters>) => {
|
||||||
|
// state
|
||||||
|
|
||||||
|
const { $axios: axios } = useNuxtApp();
|
||||||
|
|
||||||
|
// methods
|
||||||
|
|
||||||
|
const handleGetAllTickets = async (params: GetAllTicketsFilters) => {
|
||||||
|
const { data } = await axios.get<GetAllTicketsResponse>(
|
||||||
|
API_ENDPOINTS.tickets.get_all,
|
||||||
|
{
|
||||||
|
params: {
|
||||||
|
sort: params.sort,
|
||||||
|
filter: params.filter,
|
||||||
|
offset: Number(params.page) * 10 - 10,
|
||||||
|
limit: 10,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
return useQuery({
|
||||||
|
queryKey: [QUERY_KEYS.tickets, params],
|
||||||
|
queryFn: () => handleGetAllTickets(params.value),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useGetAllTickets;
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
// imports
|
||||||
|
|
||||||
|
import { useMutation } from "@tanstack/vue-query";
|
||||||
|
import { API_ENDPOINTS } from "~/constants";
|
||||||
|
|
||||||
|
// types
|
||||||
|
|
||||||
|
export type UploadAttachmentRequest = {
|
||||||
|
file: File;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UploadAttachmentResponse = {
|
||||||
|
id: number;
|
||||||
|
file_link: string;
|
||||||
|
date: string;
|
||||||
|
size: number;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
// methods
|
||||||
|
|
||||||
|
export const handleUploadAttachment = async ({
|
||||||
|
file,
|
||||||
|
}: UploadAttachmentRequest) => {
|
||||||
|
// state
|
||||||
|
|
||||||
|
const { $axios: axios } = useNuxtApp();
|
||||||
|
|
||||||
|
const { data } = await axios.post<UploadAttachmentResponse>(
|
||||||
|
API_ENDPOINTS.tickets.upload_attachment,
|
||||||
|
{
|
||||||
|
file,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "multipart/form-data",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
// composable
|
||||||
|
|
||||||
|
const useUploadAttachment = () => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (data: UploadAttachmentRequest) =>
|
||||||
|
handleUploadAttachment({ file: data.file }),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useUploadAttachment;
|
||||||
@@ -34,6 +34,12 @@ export const API_ENDPOINTS = {
|
|||||||
get_all: "/products",
|
get_all: "/products",
|
||||||
categories: "/products/categories",
|
categories: "/products/categories",
|
||||||
},
|
},
|
||||||
|
tickets: {
|
||||||
|
get_all: "/tickets",
|
||||||
|
create: "/tickets/create",
|
||||||
|
upload_attachment: "/tickets/attachment/create",
|
||||||
|
delete_attachment: "/tickets/attachment/delete",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const QUERY_KEYS = {
|
export const QUERY_KEYS = {
|
||||||
@@ -47,6 +53,7 @@ export const QUERY_KEYS = {
|
|||||||
account: "account",
|
account: "account",
|
||||||
categories: "categories",
|
categories: "categories",
|
||||||
addresses: "addresses",
|
addresses: "addresses",
|
||||||
|
tickets: "tickets",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MUTATION_KEYS = {
|
export const MUTATION_KEYS = {
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
// imports
|
||||||
|
|
||||||
|
import useGetAllTickets, {
|
||||||
|
type GetAllTicketsFilters,
|
||||||
|
} from "~/composables/api/tickets/useGetAllTickets";
|
||||||
|
|
||||||
// meta
|
// meta
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
@@ -8,10 +14,12 @@ definePageMeta({
|
|||||||
|
|
||||||
// state
|
// state
|
||||||
|
|
||||||
const filters = ref({
|
const params = useUrlSearchParams();
|
||||||
|
|
||||||
|
const filters = ref<GetAllTicketsFilters>({
|
||||||
sort: undefined,
|
sort: undefined,
|
||||||
status: undefined,
|
filter: undefined,
|
||||||
search: "",
|
page: params.page ?? 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
const tableHeads = ref([
|
const tableHeads = ref([
|
||||||
@@ -21,6 +29,24 @@ const tableHeads = ref([
|
|||||||
"وضعیت",
|
"وضعیت",
|
||||||
"عملیات",
|
"عملیات",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// queries
|
||||||
|
|
||||||
|
const { data, isLoading: ticketsIsLoading } = useGetAllTickets(filters);
|
||||||
|
|
||||||
|
// computed
|
||||||
|
|
||||||
|
const tickets = computed(() => {
|
||||||
|
return data.value?.results.flat();
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasTickets = computed(() => tickets.value?.length > 0);
|
||||||
|
|
||||||
|
const paginationData = computed(() => {
|
||||||
|
return tickets!.value?.results.map((_, i: number) => {
|
||||||
|
return { type: "page", value: i };
|
||||||
|
});
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -30,7 +56,10 @@ const tableHeads = ref([
|
|||||||
<div class="w-full flex flex-col gap-5">
|
<div class="w-full flex flex-col gap-5">
|
||||||
<div class="w-full flex items-center justify-between px-5">
|
<div class="w-full flex items-center justify-between px-5">
|
||||||
<div class="flex items-center justify-start gap-8">
|
<div class="flex items-center justify-start gap-8">
|
||||||
<div class="flex items-center justify-start gap-3">
|
<div
|
||||||
|
v-if="hasTickets"
|
||||||
|
class="flex items-center justify-start gap-3"
|
||||||
|
>
|
||||||
<span class="text-sm">ترتیب بر اساس</span>
|
<span class="text-sm">ترتیب بر اساس</span>
|
||||||
<Select
|
<Select
|
||||||
:options="['جدید ترین', 'قدیمی ترین']"
|
:options="['جدید ترین', 'قدیمی ترین']"
|
||||||
@@ -39,7 +68,10 @@ const tableHeads = ref([
|
|||||||
class="w-[5rem]"
|
class="w-[5rem]"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-start gap-3">
|
<div
|
||||||
|
v-if="hasTickets"
|
||||||
|
class="flex items-center justify-start gap-3"
|
||||||
|
>
|
||||||
<span class="text-sm">وضعیت پرداخت</span>
|
<span class="text-sm">وضعیت پرداخت</span>
|
||||||
<Select
|
<Select
|
||||||
:options="[
|
:options="[
|
||||||
@@ -47,7 +79,7 @@ const tableHeads = ref([
|
|||||||
'در حال پردازش',
|
'در حال پردازش',
|
||||||
'لغو شده',
|
'لغو شده',
|
||||||
]"
|
]"
|
||||||
v-model="filters.status!"
|
v-model="filters.filter!"
|
||||||
triggerRootClass="!py-2.5"
|
triggerRootClass="!py-2.5"
|
||||||
class="w-[5rem]"
|
class="w-[5rem]"
|
||||||
/>
|
/>
|
||||||
@@ -63,7 +95,14 @@ const tableHeads = ref([
|
|||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Table>
|
<Placeholder
|
||||||
|
v-if="!hasTickets && !ticketsIsLoading"
|
||||||
|
class="!w-full !py-[5rem]"
|
||||||
|
icon="bi:ticket"
|
||||||
|
title="تیکتی یافت نشد"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Table v-else>
|
||||||
<template #thead>
|
<template #thead>
|
||||||
<th
|
<th
|
||||||
v-for="(tableHead, index) in tableHeads"
|
v-for="(tableHead, index) in tableHeads"
|
||||||
@@ -82,9 +121,22 @@ const tableHeads = ref([
|
|||||||
</th>
|
</th>
|
||||||
</template>
|
</template>
|
||||||
<template #tbody>
|
<template #tbody>
|
||||||
<TicketsTableRow v-for="i in 4" />
|
<template v-if="ticketsIsLoading">
|
||||||
|
<TicketsTableRowLoading v-for="i in 5" />
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<TicketsTableRow
|
||||||
|
v-for="(ticket, index) in tickets"
|
||||||
|
:key="index"
|
||||||
|
:data="ticket"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</Table>
|
</Table>
|
||||||
|
|
||||||
|
<div v-if="tickets?.length > 10" class="w-full flex-center py-10">
|
||||||
|
<Pagination :items="paginationData" :total="data?.count" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,4 +1,13 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
// imports
|
||||||
|
|
||||||
|
import useCreateTicket, {
|
||||||
|
type CreateTicketRequest,
|
||||||
|
} from "~/composables/api/tickets/useCreateTicket";
|
||||||
|
import useUploadAttachment from "~/composables/api/tickets/useUploadAttachment";
|
||||||
|
import { useToast } from "~/composables/global/useToast";
|
||||||
|
import { QUERY_KEYS } from "~/constants";
|
||||||
|
|
||||||
// meta
|
// meta
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
@@ -15,6 +24,12 @@ type TicketCategory = {
|
|||||||
|
|
||||||
// state
|
// state
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const { $queryClient: queryClient } = useNuxtApp();
|
||||||
|
|
||||||
|
const { addToast } = useToast();
|
||||||
|
|
||||||
const ticketCategories: TicketCategory[] = [
|
const ticketCategories: TicketCategory[] = [
|
||||||
{
|
{
|
||||||
title: "مالی و حسابداری",
|
title: "مالی و حسابداری",
|
||||||
@@ -46,12 +61,76 @@ const ticketCategories: TicketCategory[] = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const ticketData = ref({
|
const ticketData = ref<CreateTicketRequest>({
|
||||||
category: undefined,
|
ticket_category: undefined,
|
||||||
order: undefined,
|
order: undefined,
|
||||||
message: "",
|
subject: "",
|
||||||
files: [],
|
content: "",
|
||||||
|
attachments: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// queries
|
||||||
|
|
||||||
|
const { mutateAsync: createTicket, isPending: createTicketIsPending } =
|
||||||
|
useCreateTicket();
|
||||||
|
|
||||||
|
const { mutate: uploadAttachment, isPending: uploadAttachmentIsPending } =
|
||||||
|
useUploadAttachment();
|
||||||
|
|
||||||
|
// methods
|
||||||
|
|
||||||
|
const handleUploadAttachment = (file: File) => {
|
||||||
|
uploadAttachment(
|
||||||
|
{ file },
|
||||||
|
{
|
||||||
|
onSuccess: (data) => {
|
||||||
|
ticketData.value.attachments.push({ ...data });
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
addToast({
|
||||||
|
message: error.message
|
||||||
|
? error.message
|
||||||
|
: "خطایی در آپلود پیوست رخ داد",
|
||||||
|
options: {
|
||||||
|
status: "error",
|
||||||
|
description: "لطفا مجدد تلاش کنید",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
createTicket(
|
||||||
|
{ ...ticketData.value },
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
router.push({ name: "profile-tickets" });
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: [QUERY_KEYS.tickets],
|
||||||
|
});
|
||||||
|
addToast({
|
||||||
|
message: "تیکت شما با موفقیت ثبت شد",
|
||||||
|
options: {
|
||||||
|
status: "success",
|
||||||
|
description:
|
||||||
|
"پس از بررسی پشتیبانی به شما اطلاع رسانی می شود",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
addToast({
|
||||||
|
message: "خطایی در ثبت تیکت رخ داد",
|
||||||
|
options: {
|
||||||
|
status: "success",
|
||||||
|
description: "لطفا مجدد تلاش کنید",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -75,7 +154,7 @@ const ticketData = ref({
|
|||||||
<Select
|
<Select
|
||||||
placeholder="انتخاب کنید"
|
placeholder="انتخاب کنید"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
v-model="ticketData.category"
|
v-model="ticketData.ticket_category"
|
||||||
>
|
>
|
||||||
<template #content>
|
<template #content>
|
||||||
<SelectGroup>
|
<SelectGroup>
|
||||||
@@ -133,23 +212,39 @@ const ticketData = ref({
|
|||||||
</template>
|
</template>
|
||||||
</Select>
|
</Select>
|
||||||
</DataField>
|
</DataField>
|
||||||
|
<DataField
|
||||||
|
id="subject"
|
||||||
|
:required="true"
|
||||||
|
label="عنوان تیکت"
|
||||||
|
class="col-span-full"
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
v-model="ticketData.subject"
|
||||||
|
placeholder="عنوان تیکت را اینجا بنویسید ..."
|
||||||
|
variant="outlined"
|
||||||
|
/>
|
||||||
|
</DataField>
|
||||||
<DataField id="message" :required="true" label="متن تیکت">
|
<DataField id="message" :required="true" label="متن تیکت">
|
||||||
<textarea
|
<textarea
|
||||||
v-model="ticketData.message"
|
v-model="ticketData.content"
|
||||||
class="w-full bg-white border-[1.5px] border-slate-200 rounded-xl text-xs lg:text-sm p-5 text-black h-[10rem] lg:h-[20rem] transition resize-none outline-none focus:!border-black"
|
class="w-full bg-white border-[1.5px] border-slate-200 rounded-xl text-xs lg:text-sm p-5 text-black h-[10rem] lg:h-[20rem] transition resize-none outline-none focus:!border-black"
|
||||||
placeholder="متن تیکت را اینجا بنویسید ..."
|
placeholder="متن تیکت را اینجا بنویسید ..."
|
||||||
/>
|
/>
|
||||||
</DataField>
|
</DataField>
|
||||||
<FileInput v-model="ticketData.files" />
|
<FileInput
|
||||||
|
v-model="ticketData.attachments"
|
||||||
|
@change="handleUploadAttachment"
|
||||||
|
:loading="uploadAttachmentIsPending"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</ProfileSection>
|
</ProfileSection>
|
||||||
<div class="w-full flex-center py-5">
|
<div class="w-full flex-center py-5">
|
||||||
<Button class="rounded-full px-20" end-icon="bi:send" size="md">
|
<Button class="rounded-full px-20" end-icon="bi:send" size="md">
|
||||||
<!-- <Icon
|
<Icon
|
||||||
v-if="createAddressIsPending"
|
v-if="createTicketIsPending"
|
||||||
name="svg-spinners:3-dots-bounce"
|
name="svg-spinners:3-dots-bounce"
|
||||||
/> -->
|
/>
|
||||||
<span>ارسال تیکت</span>
|
<span v-else>ارسال تیکت</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+20
-15
@@ -8,7 +8,9 @@ export default defineNuxtPlugin(() => {
|
|||||||
const { token, logout } = useAuth();
|
const { token, logout } = useAuth();
|
||||||
|
|
||||||
const axios = axiosOriginal.create({
|
const axios = axiosOriginal.create({
|
||||||
baseURL: config.public.API_BASE_URL
|
timeout: 30000,
|
||||||
|
timeoutErrorMessage: "فرآیند بیش از حد انتظار طول کشید",
|
||||||
|
baseURL: config.public.API_BASE_URL,
|
||||||
});
|
});
|
||||||
|
|
||||||
axios.interceptors.request.use((config) => {
|
axios.interceptors.request.use((config) => {
|
||||||
@@ -16,29 +18,32 @@ export default defineNuxtPlugin(() => {
|
|||||||
!config.url?.includes(API_ENDPOINTS.auth.signin) &&
|
!config.url?.includes(API_ENDPOINTS.auth.signin) &&
|
||||||
!config.url?.includes(API_ENDPOINTS.account.send_otp)
|
!config.url?.includes(API_ENDPOINTS.account.send_otp)
|
||||||
) {
|
) {
|
||||||
config.headers.Authorization = token.value ? `Bearer ${token.value}` : undefined;
|
config.headers.Authorization = token.value
|
||||||
|
? `Bearer ${token.value}`
|
||||||
|
: undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
});
|
});
|
||||||
|
|
||||||
axios.interceptors.response.use((response) => {
|
axios.interceptors.response.use(
|
||||||
return response;
|
(response) => {
|
||||||
}, async function(error) {
|
return response;
|
||||||
|
},
|
||||||
|
async function (error) {
|
||||||
|
await Logger.axiosErrorLog(error);
|
||||||
|
|
||||||
await Logger.axiosErrorLog(error);
|
// if (error.status === 401) {
|
||||||
|
// logout();
|
||||||
|
// }
|
||||||
|
|
||||||
// if (error.status === 401) {
|
return Promise.reject(error);
|
||||||
// logout();
|
}
|
||||||
// }
|
);
|
||||||
|
|
||||||
return Promise.reject(error);
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
provide: {
|
provide: {
|
||||||
axios
|
axios,
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
});
|
});
|
||||||
Vendored
+9
@@ -165,4 +165,13 @@ declare global {
|
|||||||
iconClass?: string;
|
iconClass?: string;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type Ticket = {
|
||||||
|
id: number;
|
||||||
|
subject: string;
|
||||||
|
ticket_category: string;
|
||||||
|
status: string;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user