diff --git a/backend/api/account/client/serializers.py b/backend/api/account/client/serializers.py index 4579800..087bd27 100644 --- a/backend/api/account/client/serializers.py +++ b/backend/api/account/client/serializers.py @@ -1,5 +1,5 @@ from rest_framework import serializers -from routes.models import Route, City, Country +from routes.models import Route, City, Country, Leads from django.conf import settings from routes.constants.routeChoices import cargo_type_choices, type_transport_choices, owner_type_choices from routes.constants.account_types import account_types @@ -7,6 +7,8 @@ from api.models import UserProfile from sitemanagement.models import Pricing from django.shortcuts import get_object_or_404 import pytz +from django.utils import timezone +from django.contrib.auth.models import User class CountrySerializer(serializers.ModelSerializer): value = serializers.CharField(source='international_name') # для совместимости с селектом на фронте @@ -266,5 +268,20 @@ class PlanChangeSerializer(serializers.Serializer): instance.save() return instance -class LeadSerializer(serializers.Serializer): - pass \ No newline at end of file +class LeadSerializer(serializers.ModelSerializer): + route = serializers.PrimaryKeyRelatedField(queryset=Route.objects.all()) + moving_user = serializers.PrimaryKeyRelatedField(queryset=User.objects.all()) + + class Meta: + model = Leads + fields = ['route', 'moving_user', 'moving_price', 'moving_date', 'comment'] + + def validate_moving_date(self, value): + if value < timezone.now().date(): + raise serializers.ValidationError("Дата перевозки не может быть в прошлом") + return value + + def validate_moving_price(self, value): + if value <= 0: + raise serializers.ValidationError("Цена должна быть больше нуля") + return value \ No newline at end of file diff --git a/backend/api/account/client/views.py b/backend/api/account/client/views.py index 1a370bf..192aa74 100644 --- a/backend/api/account/client/views.py +++ b/backend/api/account/client/views.py @@ -13,9 +13,9 @@ from django.contrib.auth.models import User from api.auth.serializers import UserResponseSerializer from api.models import UserProfile from api.utils.decorators import handle_exceptions -from routes.models import Route, City, Country +from routes.models import Route, City, Country, Leads from sitemanagement.models import Pricing -from .serializers import RouteSerializer, CreateRouteSerializer, CitySerializer, CountrySerializer, PlanChangeSerializer, PricingSerializer +from .serializers import RouteSerializer, CreateRouteSerializer, CitySerializer, CountrySerializer, PlanChangeSerializer, PricingSerializer, LeadSerializer class UserDataView(ViewSet): """Эндпоинт для наполнения стора фронта данными""" @@ -193,9 +193,56 @@ class GetMembershipData(ViewSet): return Response(serializer.data, status=status.HTTP_200_OK) class LeadViewSet(ViewSet): - """Собираем лиды""" + """ViewSet для работы с заявками на перевозку""" + permission_classes = [IsAuthenticated] + @action(detail=False, methods=['post']) @handle_exceptions def send_lead(self, request, id): - pass + """ + Создание новой заявки на перевозку + """ + # добавляем текущего пользователя в данные + data = request.data.copy() + data['moving_user'] = request.user.id + + # проверяем существование и доступность маршрута + try: + route = Route.objects.get(id=data.get('route')) + if route.owner == request.user: + return Response( + {"error": "Вы не можете откликнуться на собственную заявку"}, + status=status.HTTP_400_BAD_REQUEST + ) + except Route.DoesNotExist: + return Response( + {"error": "Указанный маршрут не найден"}, + status=status.HTTP_404_NOT_FOUND + ) + + serializer = LeadSerializer(data=data) + if serializer.is_valid(): + lead = serializer.save() + + # собираем ответ с данными о заявке для фронта + response_data = { + "status": "success", + "message": "Заявка успешно создана", + "data": { + "id": lead.id, + "route_id": lead.route.id, + "moving_price": lead.moving_price, + "moving_date": lead.moving_date, + } + } + return Response(response_data, status=status.HTTP_201_CREATED) + + return Response( + { + "status": "error", + "message": "Ошибка валидации данных", + "errors": serializer.errors + }, + status=status.HTTP_400_BAD_REQUEST + ) \ No newline at end of file diff --git a/backend/api/urls.py b/backend/api/urls.py index d92a070..ef11fcc 100644 --- a/backend/api/urls.py +++ b/backend/api/urls.py @@ -36,7 +36,7 @@ urlpatterns = [ path("v1/account/routes/", AccountActionsView.as_view({'get':'user_routes'}), name='user_routes'), path("v1/account/create_route/", AccountActionsView.as_view({'post':'create_route'}), name='create_route'), - path("v1/account/sendlead/", LeadViewSet.as_view({'post':'send_lead'}), name='send_lead'), + path("v1/account/send_lead/", LeadViewSet.as_view({'post':'send_lead'}), name='send_lead'), path("v1/cities/", CityView.as_view({'get':'get_cities'}), name='get_cities'), path("v1/countries/", CountryView.as_view({'get':'get_countries'}), name='get_countries'), diff --git a/frontend/app/types/index.ts b/frontend/app/types/index.ts index 19e318f..303da08 100644 --- a/frontend/app/types/index.ts +++ b/frontend/app/types/index.ts @@ -236,3 +236,12 @@ export interface SearchPageProps { params: Promise<{ category?: string }> searchParams?: Promise<{ [key: string]: string | string[] | undefined }> } + +export interface Lead { + name: string + phone_number: string + email: string + moving_price: string + moving_date: string + comment: string +} diff --git a/frontend/components/forms/RouteForm.tsx b/frontend/components/forms/RouteForm.tsx index 1502c4c..18b212c 100644 --- a/frontend/components/forms/RouteForm.tsx +++ b/frontend/components/forms/RouteForm.tsx @@ -25,7 +25,7 @@ interface RouteFormProps { description: string } -const formatDateToHTML = (date: Date) => { +export const formatDateToHTML = (date: Date) => { const year = date.getFullYear() const month = String(date.getMonth() + 1).padStart(2, '0') const day = String(date.getDate()).padStart(2, '0') diff --git a/frontend/components/popups/LeadPopup.tsx b/frontend/components/popups/LeadPopup.tsx index e73f1b9..d245aed 100644 --- a/frontend/components/popups/LeadPopup.tsx +++ b/frontend/components/popups/LeadPopup.tsx @@ -7,13 +7,14 @@ import TextInput from '../ui/TextInput' import TextAreaInput from '../ui/TextAreaInput' import PhoneInput from '../ui/PhoneInput' import useUserStore from '@/app/store/userStore' +import { sendLead } from '@/lib/main/sendLead' const validationRules = { name: { required: true }, phone_number: { required: true }, email: { required: true }, - price: { required: false }, - deliveryTime: { required: false }, + moving_price: { required: true }, + moving_date: { required: true }, comment: { required: false }, } @@ -26,21 +27,24 @@ interface LeadPopupProps { const LeadPopup = ({ id, isOpen, onClose, onSuccess }: LeadPopupProps) => { const { user } = useUserStore() + const today = new Date().toISOString().split('T')[0] + + const initialValues = { + name: user?.name || '', + phone_number: user?.phone_number || '', + email: user?.email || '', + moving_price: '', + moving_date: '', + comment: '', + id: id, + } const { values, handleChange, handleSubmit, setValues } = useForm( - { - name: user?.name || '', - phone_number: user?.phone_number || '', - email: user?.email || '', - price: '', - deliveryTime: '', - comment: '', - id: id, - }, + initialValues, validationRules, async values => { try { - // await sendLead(values) + await sendLead(values) showToast({ type: 'success', message: 'Сообщение отправлено!', @@ -98,22 +102,28 @@ const LeadPopup = ({ id, isOpen, onClose, onSuccess }: LeadPopupProps) => { /> - +
+ + +
{ + const API_URL = process.env.NEXT_PUBLIC_API_URL + + const headers: Record = { + 'Content-Type': 'application/json', + Accept: 'application/json', + } + + try { + const body = JSON.stringify({ + name: data.name, + phone_number: data.phone_number, + email: data.email, + moving_price: data.moving_price, + moving_date: data.moving_date, + comment: data.comment, + }) + + const response = await fetch(`${API_URL}/account/send_lead/`, { + method: 'POST', + headers, + body, + }) + + if (!response.ok) { + let errorMessage = `Failed to send lead data: ${response.status} ${response.statusText}` + try { + const errorData = await response.text() + + if (errorData) { + errorMessage += ` - ${errorData}` + } + } catch (e) { + console.error('Error parsing error response:', e) + } + throw new Error(errorMessage) + } + const text = await response.text() + return text ? JSON.parse(text) : null + } catch (error) { + console.error('Error sending lead data:', error) + throw error + } +}