From 5ba9121f33f34ac92ad6191bbcb68601bfba2855 Mon Sep 17 00:00:00 2001 From: Timofey Date: Thu, 22 May 2025 11:45:25 +0300 Subject: [PATCH] route handler + backend api --- backend/api/account/client/serializers.py | 114 +++++++++++++++++- backend/api/account/client/views.py | 10 +- backend/api/urls.py | 3 +- .../(urls)/account/create-as-sender/page.tsx | 8 +- frontend/app/api/account/sender/route.ts | 63 ++++++++++ frontend/app/types/index.ts | 2 +- frontend/package-lock.json | 56 ++++----- 7 files changed, 220 insertions(+), 36 deletions(-) diff --git a/backend/api/account/client/serializers.py b/backend/api/account/client/serializers.py index f01679d..3c50ccc 100644 --- a/backend/api/account/client/serializers.py +++ b/backend/api/account/client/serializers.py @@ -1,7 +1,9 @@ from rest_framework import serializers -from routes.models import Route, City +from routes.models import Route, City, Country from django.conf import settings -from routes.constants.routeChoices import cargo_type_choices, type_transport_choices +from routes.constants.routeChoices import cargo_type_choices, type_transport_choices, owner_type_choices +from api.models import UserProfile +from django.shortcuts import get_object_or_404 import pytz class RouteSerializer(serializers.ModelSerializer): @@ -72,3 +74,111 @@ class RouteSerializer(serializers.ModelSerializer): def get_formatted_transport(self, obj): transport_types = dict(type_transport_choices) return transport_types.get(obj.type_transport, obj.type_transport) + + +class CreateRouteSerializer(serializers.ModelSerializer): + country_from = serializers.CharField(write_only=True) + city_from = serializers.CharField(write_only=True) + country_to = serializers.CharField(write_only=True) + city_to = serializers.CharField(write_only=True) + departure = serializers.DateTimeField(source='departure_DT') + arrival = serializers.DateTimeField(source='arrival_DT') + transport = serializers.ChoiceField(choices=type_transport_choices, source='type_transport') + email_notification = serializers.BooleanField(source='receive_msg_by_email') + contact_number = serializers.CharField(write_only=True) + owner_type = serializers.ChoiceField(choices=[('sender', 'Отправитель'), ('deliverer', 'Перевозчик')]) + + class Meta: + model = Route + fields = [ + 'transport', + 'country_from', + 'city_from', + 'country_to', + 'city_to', + 'cargo_type', + 'departure', + 'arrival', + 'phone_number', + 'comment', + 'email_notification', + 'owner_type', + ] + + def validate_phone_number(self, value): + if len(value) < 13: # +375XXXXXXXXX + raise serializers.ValidationError("Номер телефона слишком короткий") + if len(value) > 18: + raise serializers.ValidationError("Номер телефона слишком длинный") + return value + + def validate(self, data): + # проверяем что дата прибытия позже даты отправления + if data['departure_DT'] >= data['arrival_DT']: + raise serializers.ValidationError( + "Дата прибытия должна быть позже даты отправления" + ) + + # проверяем существование стран + try: + country_from = Country.objects.get(international_name=data['country_from']) + except Country.DoesNotExist: + raise serializers.ValidationError({ + "country_from": f"Страна '{data['country_from']}' не найдена в базе данных" + }) + + try: + country_to = Country.objects.get(international_name=data['country_to']) + except Country.DoesNotExist: + raise serializers.ValidationError({ + "country_to": f"Страна '{data['country_to']}' не найдена в базе данных" + }) + + # проверяем существование городов в указанных странах + try: + City.objects.get(name=data['city_from'], country=country_from) + except City.DoesNotExist: + raise serializers.ValidationError({ + "city_from": f"Город '{data['city_from']}' не найден в стране {country_from}" + }) + + try: + City.objects.get(name=data['city_to'], country=country_to) + except City.DoesNotExist: + raise serializers.ValidationError({ + "city_to": f"Город '{data['city_to']}' не найден в стране {country_to}" + }) + + return data + + def create(self, validated_data): + # получаем города и страны + country_from = Country.objects.get(international_name=validated_data.pop('country_from')) + country_to = Country.objects.get(international_name=validated_data.pop('country_to')) + + from_city = City.objects.get(name=validated_data.pop('city_from'), country=country_from) + to_city = City.objects.get(name=validated_data.pop('city_to'), country=country_to) + + # обновляем номер телефона в профиле пользователя + contact_number = validated_data.pop('contact_number') + user_profile = get_object_or_404(UserProfile, user=self.context['request'].user) + + # проверяем, не используется ли этот номер другим пользователем + if UserProfile.objects.filter(phone_number=contact_number).exclude(user=self.context['request'].user).exists(): + raise serializers.ValidationError({ + "contact_number": "Этот номер телефона уже используется другим пользователем" + }) + + user_profile.phone_number = contact_number + user_profile.save() + + # создаем маршрут + route = Route.objects.create( + from_city=from_city, + to_city=to_city, + owner=self.context['request'].user, + **validated_data # owner_type приходит с фронта + ) + + return route + \ No newline at end of file diff --git a/backend/api/account/client/views.py b/backend/api/account/client/views.py index 1aa59af..7daedf1 100644 --- a/backend/api/account/client/views.py +++ b/backend/api/account/client/views.py @@ -12,7 +12,7 @@ from api.auth.serializers import UserResponseSerializer from api.models import UserProfile from api.utils.decorators import handle_exceptions from routes.models import Route -from .serializers import RouteSerializer +from .serializers import RouteSerializer, CreateRouteSerializer class UserDataView(ViewSet): permission_classes = [IsAuthenticated] @@ -92,3 +92,11 @@ class AccountActionsView(ViewSet): user = request.user routes = Route.objects.filter(owner=user) return Response(RouteSerializer(routes, many=True).data, status=status.HTTP_200_OK) + + @action(detail=False, methods=['post']) + @handle_exceptions + def create_sender_route(self, request): + serializer = CreateRouteSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + serializer.save() + return Response(serializer.data, status=status.HTTP_201_CREATED) diff --git a/backend/api/urls.py b/backend/api/urls.py index 298dd6e..c8877b3 100644 --- a/backend/api/urls.py +++ b/backend/api/urls.py @@ -23,5 +23,6 @@ urlpatterns = [ path ("v1/user/", UserDataView.as_view({'get': 'user_data'}), name="user"), path("v1/account/change_main_data/", AccountActionsView.as_view({'patch':'change_data_main_tab'}), name='change_data_main_tab'), - path("v1/account/routes/", AccountActionsView.as_view({'get':'user_routes'}), name='user_routes') + path("v1/account/routes/", AccountActionsView.as_view({'get':'user_routes'}), name='user_routes'), + path("v1/account/create_sender/", AccountActionsView.as_view({'post':'create_sender_route'}), name='create_sender_route') ] \ No newline at end of file diff --git a/frontend/app/(urls)/account/create-as-sender/page.tsx b/frontend/app/(urls)/account/create-as-sender/page.tsx index 28abb29..13102e5 100644 --- a/frontend/app/(urls)/account/create-as-sender/page.tsx +++ b/frontend/app/(urls)/account/create-as-sender/page.tsx @@ -8,6 +8,7 @@ import Button from '@/components/ui/Button' import TextAreaInput from '@/components/ui/TextAreaInput' import CheckboxInput from '@/components/ui/CheckboxInput' import { useForm } from '@/app/hooks/useForm' +import useUserStore from '@/app/store/userStore' import showToast from '@/components/ui/Toast' import { SenderPageProps, SelectOption } from '@/app/types' import { @@ -41,7 +42,7 @@ const validationRules = { required: true, pattern: /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/, }, - contact_number: { + phone_number: { required: true, minLength: 11, pattern: /^\+?[0-9]{11,}$/, @@ -51,6 +52,7 @@ const validationRules = { } const SenderPage = () => { + const { user, setUser } = useUserStore() const today = formatDateToHTML(new Date()) const initialValues: SenderPageProps = { @@ -62,7 +64,7 @@ const SenderPage = () => { cargo_type: '', departure: '', arrival: '', - contact_number: '', + phone_number: user?.phone_number || '', comment: '', email_notification: false, } @@ -240,7 +242,7 @@ const SenderPage = () => {

Контактная информация

{ cargo_type: string departure: string arrival: string - contact_number: string + phone_number: string comment: string email_notification: boolean } diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 6770b7f..227ed89 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1603,9 +1603,9 @@ "license": "MIT" }, "node_modules/@types/react": { - "version": "19.1.4", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.4.tgz", - "integrity": "sha512-EB1yiiYdvySuIITtD5lhW4yPyJ31RkJkkDw794LaQYrxCSaQV/47y5o1FMC4zF9ZyjUjzJMZwbovEnT5yHTW6g==", + "version": "19.1.5", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.5.tgz", + "integrity": "sha512-piErsCVVbpMMT2r7wbawdZsq4xMvIAhQuac2gedQHysu1TZYEigE6pnFfgZT+/jQnrRuF5r+SHzuehFjfRjr4g==", "license": "MIT", "dependencies": { "csstype": "^3.0.2" @@ -2909,16 +2909,10 @@ "is-arrayish": "^0.2.1" } }, - "node_modules/error-ex/node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "license": "MIT" - }, "node_modules/es-abstract": { - "version": "1.23.9", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.9.tgz", - "integrity": "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==", + "version": "1.23.10", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.10.tgz", + "integrity": "sha512-MtUbM072wlJNyeYAe0mhzrD+M6DIJa96CZAOBBrhDbgKnB4MApIKefcyAB1eOdYn8cUNZgvwBvEzdoAYsxgEIw==", "dev": true, "license": "MIT", "dependencies": { @@ -2926,18 +2920,18 @@ "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", - "call-bound": "^1.0.3", + "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", + "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", - "get-intrinsic": "^1.2.7", - "get-proto": "^1.0.0", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", @@ -2953,13 +2947,13 @@ "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", - "is-weakref": "^1.1.0", + "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", - "object-inspect": "^1.13.3", + "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", - "regexp.prototype.flags": "^1.5.3", + "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", @@ -2972,7 +2966,7 @@ "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", - "which-typed-array": "^1.1.18" + "which-typed-array": "^1.1.19" }, "engines": { "node": ">= 0.4" @@ -4061,11 +4055,10 @@ } }, "node_modules/is-arrayish": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", - "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", - "license": "MIT", - "optional": true + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" }, "node_modules/is-async-function": { "version": "2.1.1", @@ -6227,6 +6220,13 @@ "is-arrayish": "^0.3.1" } }, + "node_modules/simple-swizzle/node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "license": "MIT", + "optional": true + }, "node_modules/source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", @@ -6926,9 +6926,9 @@ } }, "node_modules/zustand": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.4.tgz", - "integrity": "sha512-39VFTN5InDtMd28ZhjLyuTnlytDr9HfwO512Ai4I8ZABCoyAj4F1+sr7sD1jP/+p7k77Iko0Pb5NhgBFDCX0kQ==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.5.tgz", + "integrity": "sha512-mILtRfKW9xM47hqxGIxCv12gXusoY/xTSHBYApXozR0HmQv299whhBeeAcRy+KrPPybzosvJBCOmVjq6x12fCg==", "license": "MIT", "engines": { "node": ">=12.20.0"