diff --git a/backend/.env.local b/backend/.env.local index 4cb8221..2678574 100644 --- a/backend/.env.local +++ b/backend/.env.local @@ -28,4 +28,16 @@ SMS_API_KEY = '' VAPID_PRIVATE_KEY = 'NajogmGTsGsZ_dfURrjUpgsm5fui-s5AzruBQgMh_I4' -OPENAI_API_KEY = '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' \ No newline at end of file +OPENAI_API_KEY = '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' + + +# DATABASES = { +# 'default': { +# 'ENGINE': 'django.db.backends.postgresql', +# 'NAME': 'hshop', +# 'USER': 'byeto', +# 'PASSWORD': 'vuhbyq-cypMu0-sirbon', +# 'HOST': '185.110.189.208', +# 'PORT': '5434', +# } +# } \ No newline at end of file diff --git a/backend/core/settings/unfold_conf.py b/backend/core/settings/unfold_conf.py index 23bf662..a143954 100644 --- a/backend/core/settings/unfold_conf.py +++ b/backend/core/settings/unfold_conf.py @@ -239,6 +239,12 @@ UNFOLD = { "badge": "utils.admin.new_ticket_count", }, + { + "title": _("ارتباط با ما"), + "icon": "perm_phone_msg", + "link": reverse_lazy("admin:ticket_contactusmodel_changelist"), + "badge": "utils.admin.new_contact_us_count", + }, ], }, { @@ -262,6 +268,11 @@ UNFOLD = { "icon": "photo_prints", "link": reverse_lazy("admin:product_productvariant_changelist"), }, + { + "title": _("بخش جزيیات محصول"), + "icon": "subject", + "link": reverse_lazy("admin:product_productdetailmodel_changelist"), + }, ], }, diff --git a/backend/core/views.py b/backend/core/views.py index be623df..9894f4e 100644 --- a/backend/core/views.py +++ b/backend/core/views.py @@ -8,7 +8,7 @@ from django.utils.translation import gettext_lazy as _ from django.views.generic import RedirectView, TemplateView from unfold.views import UnfoldModelAdminViewMixin from order.models import OrderModel -from ticket.models import Ticket +from ticket.models import Ticket, ContactUsModel from account.models import SecurityBreachAttemptModel import json @@ -19,9 +19,11 @@ def dashboard_callback(request, context): pending_count = OrderModel.objects.filter(status='ADMIN_PENDING').count() open_tickets_count = Ticket.objects.filter(status__in=['open', 'in_progress']).count() + open_contact_us_count = ContactUsModel.objects.filter(is_reviewed=False).count() context.update(random_data()) context.update({'pending_count': pending_count}) context.update({'open_tickets_count': open_tickets_count}) + context.update({'open_contact_us_count': open_contact_us_count}) return context diff --git a/backend/product/admin.py b/backend/product/admin.py index a7d6e19..80ac965 100644 --- a/backend/product/admin.py +++ b/backend/product/admin.py @@ -1,4 +1,5 @@ from django.contrib import admin, messages +# from product.tasks import update_prices from .models import * from unfold.admin import TabularInline, StackedInline from home.models import LearnVideoModel @@ -122,7 +123,7 @@ class DetailModelAdmin(ModelAdmin, ImportExportModelAdmin): class ProductDetailModel1Admin(ModelAdmin, ImportExportModelAdmin): import_form_class = ImportForm export_form_class = ExportForm - search_fields = ['detail_category__title'] + search_fields = ['detail_category__title', 'name'] compressed_fields = True warn_unsaved_form = True @@ -216,8 +217,7 @@ class ProductModelAdmin(ModelAdmin, ImportExportModelAdmin): @action(description=f"اپدیت قیمت ها") def update_products_price(self, request): - print('from the button') - ProductVariant.update_all_prices() + # update_prices() messages.success(request, f"قیمت {ProductVariant.objects.all().count()} تنوع محصول اپدیت شد") return redirect("admin:product_productmodel_changelist") diff --git a/backend/product/migrations/0036_alter_productdetailmodel_detail_category.py b/backend/product/migrations/0036_alter_productdetailmodel_detail_category.py new file mode 100644 index 0000000..1ae6eb5 --- /dev/null +++ b/backend/product/migrations/0036_alter_productdetailmodel_detail_category.py @@ -0,0 +1,20 @@ +# Generated by Django 5.1.2 on 2025-04-21 23:30 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('product', '0035_alter_attributetype_options_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='productdetailmodel', + name='detail_category', + field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='product.productdetailcategory', verbose_name='دسته بندی جزيات'), + preserve_default=False, + ), + ] diff --git a/backend/product/migrations/0037_productdetailmodel_name.py b/backend/product/migrations/0037_productdetailmodel_name.py new file mode 100644 index 0000000..c93a035 --- /dev/null +++ b/backend/product/migrations/0037_productdetailmodel_name.py @@ -0,0 +1,19 @@ +# Generated by Django 5.1.2 on 2025-04-21 23:37 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('product', '0036_alter_productdetailmodel_detail_category'), + ] + + operations = [ + migrations.AddField( + model_name='productdetailmodel', + name='name', + field=models.CharField(default=1, help_text='این متن فقط برای راحتی در استفاده از پنل ادمین میباشد', max_length=50, verbose_name='نام جزيیات'), + preserve_default=False, + ), + ] diff --git a/backend/product/models.py b/backend/product/models.py index 1a1ca6b..1a87333 100644 --- a/backend/product/models.py +++ b/backend/product/models.py @@ -4,6 +4,7 @@ from account.models import User from django.urls import reverse import requests from django.utils.translation import gettext_lazy as _ +from django.core.exceptions import ValidationError class MainCategoryModel(models.Model): name = models.CharField(max_length=50, verbose_name='نام دسته بندی') @@ -212,14 +213,15 @@ class ProductImageModel(models.Model): class ProductDetailModel(models.Model): - detail_category = models.ForeignKey(ProductDetailCategory, on_delete=models.CASCADE, verbose_name='دسته بندی جزيات', blank=True, null=True) + name = models.CharField(max_length=50, verbose_name='نام جزيیات', help_text='این متن فقط برای راحتی در استفاده از پنل ادمین میباشد') + detail_category = models.ForeignKey(ProductDetailCategory, on_delete=models.CASCADE, verbose_name='دسته بندی جزيات') detail = models.ManyToManyField(DetailModel, verbose_name='جزيات ها') class Meta: verbose_name = 'جزیات محصول' verbose_name_plural = 'جزیات محصول ها' - # def __str__(self): - # return f'جزيیات محصول {self.product}' + def __str__(self): + return f'جزيیات محصول {self.detail_category.title} - {self.name}' class ProductVariant(models.Model): product = models.ForeignKey(ProductModel, on_delete=models.CASCADE, related_name='variants', verbose_name='محصول') @@ -272,21 +274,3 @@ class ProductVariant(models.Model): def save(self, *args, **kwargs): self.set_or_update_price() super().save(*args, **kwargs) - - def get_toman_price_after_discount(self): - return self.price * ((100 - self.discount) / 100) - - @classmethod - def update_all_prices(cls): - print('calling the update all prices ') - dollor_object, _ = DollorModel.objects.get_or_create(unique_filed='unique') - print(dollor_object.price) - dollor_object.update_price() - dollor_object.save() - dollor_price = dollor_object.price - print(dollor_object.price) - print('classmethod dollor price update ') - products = cls.objects.all() - for product in products: - product.set_or_update_price(dollor_price=dollor_price) - product.save() \ No newline at end of file diff --git a/backend/templates/formula/service.html b/backend/templates/formula/service.html index 57856a4..ca6cc92 100644 --- a/backend/templates/formula/service.html +++ b/backend/templates/formula/service.html @@ -39,4 +39,25 @@ {% endcomponent %} -{% endif %} \ No newline at end of file +{% endif %} +{% if open_contact_us_count%} + + +
+
+

+ confirmation_number + ارتباط با ما جدید داری + {{ open_contact_us_count }} +

+
+ +
+ {% component "unfold/components/flex.html" with class="flex-col gap-4 lg:flex-row" %} + {% component "unfold/components/button.html" with href="/secret-admin/ticket/contactusmodel/" %} + نمایش ارتباط با ما ها + {% endcomponent %} + {% endcomponent %} +
+
+{% endif %} diff --git a/backend/ticket/admin.py b/backend/ticket/admin.py index 1426233..bc50d27 100644 --- a/backend/ticket/admin.py +++ b/backend/ticket/admin.py @@ -18,6 +18,15 @@ class MessageInline(TabularInline): model = Message extra = 1 +@admin.register(ContactUsModel) +class ContactUsAdmin(ModelAdmin): + list_filter = ['type', 'is_reviewed'] + list_display = ['full_name', 'phone', 'email', 'message', 'is_reviewed'] + compressed_fields = True + warn_unsaved_form = True + readonly_fields = ['full_name', 'email', 'phone', 'type', 'message', ] + + @admin.register(Ticket) class TicketAdmin(ModelAdmin, ImportExportModelAdmin): import_form_class = ImportForm diff --git a/backend/ticket/migrations/0020_contactusmodel.py b/backend/ticket/migrations/0020_contactusmodel.py new file mode 100644 index 0000000..d2bd2be --- /dev/null +++ b/backend/ticket/migrations/0020_contactusmodel.py @@ -0,0 +1,24 @@ +# Generated by Django 5.1.2 on 2025-04-21 22:52 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ticket', '0019_alter_attachment_options'), + ] + + operations = [ + migrations.CreateModel( + name='ContactUsModel', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('full_name', models.CharField(max_length=50, verbose_name='نام و نام خانوادگی')), + ('email', models.EmailField(max_length=254, verbose_name='ایمیل')), + ('phone', models.CharField(max_length=30)), + ('type', models.CharField(choices=[('ORDER', 'پیگیری سفارش'), ('SUGGESTION', 'پیشنهادات'), ('COMPLAINT', 'انتقادات')], max_length=20, verbose_name='نوع')), + ('message', models.TextField(verbose_name='پیام')), + ], + ), + ] diff --git a/backend/ticket/migrations/0021_alter_contactusmodel_type.py b/backend/ticket/migrations/0021_alter_contactusmodel_type.py new file mode 100644 index 0000000..59c5790 --- /dev/null +++ b/backend/ticket/migrations/0021_alter_contactusmodel_type.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.2 on 2025-04-21 22:53 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ticket', '0020_contactusmodel'), + ] + + operations = [ + migrations.AlterField( + model_name='contactusmodel', + name='type', + field=models.CharField(choices=[('ORDER_FOLLOW_UP', 'پیگیری سفارش'), ('SUGGESTION', 'پیشنهادات'), ('COMPLAINT', 'انتقادات')], max_length=20, verbose_name='نوع'), + ), + ] diff --git a/backend/ticket/migrations/0022_alter_contactusmodel_options_and_more.py b/backend/ticket/migrations/0022_alter_contactusmodel_options_and_more.py new file mode 100644 index 0000000..537844b --- /dev/null +++ b/backend/ticket/migrations/0022_alter_contactusmodel_options_and_more.py @@ -0,0 +1,22 @@ +# Generated by Django 5.1.2 on 2025-04-21 22:57 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ticket', '0021_alter_contactusmodel_type'), + ] + + operations = [ + migrations.AlterModelOptions( + name='contactusmodel', + options={'verbose_name': 'ارتباط با ما', 'verbose_name_plural': 'ارتباط با ما ها '}, + ), + migrations.AddField( + model_name='contactusmodel', + name='is_reviewed', + field=models.BooleanField(default=False), + ), + ] diff --git a/backend/ticket/migrations/0023_alter_contactusmodel_is_reviewed_and_more.py b/backend/ticket/migrations/0023_alter_contactusmodel_is_reviewed_and_more.py new file mode 100644 index 0000000..e798399 --- /dev/null +++ b/backend/ticket/migrations/0023_alter_contactusmodel_is_reviewed_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 5.1.2 on 2025-04-21 23:30 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ticket', '0022_alter_contactusmodel_options_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='contactusmodel', + name='is_reviewed', + field=models.BooleanField(default=False, verbose_name='بررسی شده'), + ), + migrations.AlterField( + model_name='contactusmodel', + name='phone', + field=models.CharField(max_length=30, verbose_name='شماره تماس'), + ), + ] diff --git a/backend/ticket/models.py b/backend/ticket/models.py index 0941fe3..a6b89a4 100644 --- a/backend/ticket/models.py +++ b/backend/ticket/models.py @@ -73,4 +73,26 @@ class Message(models.Model): class Meta: verbose_name = 'پیام تیکت' - verbose_name_plural = 'پیام های تیکت' \ No newline at end of file + verbose_name_plural = 'پیام های تیکت' + + +class ContactUsModel(models.Model): + full_name = models.CharField(max_length=50, verbose_name='نام و نام خانوادگی') + email = models.EmailField(max_length=254, verbose_name='ایمیل') + phone = models.CharField(max_length=30, verbose_name='شماره تماس') + FEEDBACK_TYPES = [ + ('ORDER_FOLLOW_UP', 'پیگیری سفارش'), + ('SUGGESTION', 'پیشنهادات'), + ('COMPLAINT', 'انتقادات'), + ] + + type = models.CharField(max_length=20, choices=FEEDBACK_TYPES, verbose_name='نوع') + message = models.TextField(verbose_name='پیام') + is_reviewed = models.BooleanField(default=False, verbose_name='بررسی شده') + + def __str__(self): + return f'{self.full_name} - {self.message[:15]}...' + + class Meta: + verbose_name = 'ارتباط با ما' + verbose_name_plural = 'ارتباط با ما ها ' \ No newline at end of file diff --git a/backend/ticket/serializers.py b/backend/ticket/serializers.py index d99a8d2..f4af17d 100644 --- a/backend/ticket/serializers.py +++ b/backend/ticket/serializers.py @@ -1,5 +1,5 @@ from rest_framework import serializers -from .models import Ticket, Message, Attachment +from .models import Ticket, Message, Attachment, ContactUsModel from django.utils.timezone import localtime from order.serializers import OrderListSerializer from order.serializers import OrderModel @@ -88,4 +88,10 @@ class TicketListSerializer(serializers.ModelSerializer): return obj.get_status_display() def get_ticket_category(self, obj): - return obj.get_ticket_category_display() \ No newline at end of file + return obj.get_ticket_category_display() + + +class ContactUsSerializer(serializers.ModelSerializer): + class Meta: + model = ContactUsModel + fields = ['full_name', 'email', 'phone', 'type', 'message'] \ No newline at end of file diff --git a/backend/ticket/urls.py b/backend/ticket/urls.py index c3a47f5..50eed91 100644 --- a/backend/ticket/urls.py +++ b/backend/ticket/urls.py @@ -6,6 +6,7 @@ urlpatterns = [ path('', views.TicketListView.as_view(), name='ticket-list'), path('', views.TicketDetailView.as_view(), name='ticket-detail'), path('message/create', views.MessageCreateView.as_view(), name='message-create'), + path('contact-us/create', views.CreateContactUsView.as_view(), name='contact-us-create'), path('attachment/create', views.AttachmentUploadView.as_view(), name='attachment-upload'), path('attachment/delete/', views.AttachmentDeleteView.as_view(), name='attachment-upload'), ] \ No newline at end of file diff --git a/backend/ticket/views.py b/backend/ticket/views.py index ac6fdae..e669864 100644 --- a/backend/ticket/views.py +++ b/backend/ticket/views.py @@ -2,7 +2,7 @@ from rest_framework import generics, permissions from rest_framework.response import Response from rest_framework.views import APIView from .models import Ticket, Message, Attachment -from .serializers import TicketListSerializer, MessageSerializer, TicketSerializer, AttachmentSerializer +from .serializers import TicketListSerializer, MessageSerializer, TicketSerializer, AttachmentSerializer, ContactUsSerializer from utils.pagination import StructurePagination from drf_spectacular.utils import extend_schema, OpenApiParameter, OpenApiTypes from rest_framework.permissions import IsAuthenticated @@ -56,7 +56,6 @@ class TicketCreateView(APIView): permission_classes = [permissions.IsAuthenticated] def post(self, request): new_ticket_ser = self.serializer_class(data=request.data, context={'request': request}) - message = request.data.get('message', None) if new_ticket_ser.is_valid(): new_ticket_ser.save(customer=request.user) return Response(new_ticket_ser.data, status=status.HTTP_201_CREATED) @@ -159,4 +158,16 @@ class UpdateTicketStatusView(APIView): return Response({"error": "Invalid status"}, status=400) ticket.status = new_status ticket.save() - return Response({"message": "Ticket status updated successfully"}) \ No newline at end of file + return Response({"message": "Ticket status updated successfully"}) + + +class CreateContactUsView(APIView): + serializer_class = ContactUsSerializer + permission_classes = [permissions.AllowAny] + def post(self, request): + contact_us_ser = self.serializer_class(data=request.data, context={'request': request}) + if contact_us_ser.is_valid(): + contact_us_ser.save() + return Response(contact_us_ser.data, status=status.HTTP_201_CREATED) + else: + return Response(contact_us_ser.errors, status=status.HTTP_400_BAD_REQUEST) \ No newline at end of file diff --git a/backend/utils/admin.py b/backend/utils/admin.py index 1107fab..82c4d05 100644 --- a/backend/utils/admin.py +++ b/backend/utils/admin.py @@ -1,6 +1,6 @@ from order.models import OrderModel from product.models import DollorModel, CommentModel -from ticket.models import Ticket +from ticket.models import Ticket, ContactUsModel from home.models import LearnVideoModel from account.models import SecurityBreachAttemptModel @@ -24,6 +24,9 @@ def new_learn_video_count(request): def new_attck_count(request): return SecurityBreachAttemptModel.objects.filter(viewd=False).count() +def new_contact_us_count(request): + return ContactUsModel.objects.filter(is_reviewed=False).count() + from django.contrib import admin, messages from unfold.admin import ModelAdmin from home.models import LearnVideoModel diff --git a/frontend/components/cart/index/CartItem.vue b/frontend/components/cart/index/CartItem.vue index 17ba187..9fee44b 100644 --- a/frontend/components/cart/index/CartItem.vue +++ b/frontend/components/cart/index/CartItem.vue @@ -150,9 +150,12 @@ watch( - + {{ data.product.title }} - +
@@ -46,32 +50,104 @@ const {} = toRefs(props); class="bg-slate-100/70 flex items-center pr-20 gap-12 sm:gap-20 w-max animate-marquee h-[90px] sm:h-[140px]" > diff --git a/frontend/components/global/DataField.vue b/frontend/components/global/DataField.vue index 75e8fae..fb13443 100644 --- a/frontend/components/global/DataField.vue +++ b/frontend/components/global/DataField.vue @@ -18,10 +18,16 @@ withDefaults(defineProps(), {