From c4e1e16e79e6d7046f3a1f4294e139f5ff3c1c9e Mon Sep 17 00:00:00 2001 From: Timofey Date: Wed, 21 May 2025 15:39:00 +0300 Subject: [PATCH] account/routes page --- backend/Pipfile | 1 + backend/Pipfile.lock | 10 +- backend/api/account/client/serializers.py | 58 + backend/api/account/client/views.py | 11 +- backend/api/admin.py | 4 +- backend/api/urls.py | 3 +- backend/requirements.txt | 1 + .../account/create-as-deliveler/page.tsx | 16 +- .../(urls)/account/create-as-sender/page.tsx | 16 +- frontend/app/(urls)/account/layout.tsx | 4 +- frontend/app/(urls)/account/routes/page.tsx | 151 +- frontend/app/api/account/deliveler/route.ts | 0 frontend/app/api/account/routes/route.ts | 43 +- frontend/app/api/account/sender/route.ts | 0 frontend/components/Header.tsx | 8 - frontend/components/UserLogin.tsx | 52 +- frontend/package-lock.json | 1241 ++++------------- 17 files changed, 626 insertions(+), 993 deletions(-) create mode 100644 frontend/app/api/account/deliveler/route.ts create mode 100644 frontend/app/api/account/sender/route.ts diff --git a/backend/Pipfile b/backend/Pipfile index 0dc5a4f..c43df81 100644 --- a/backend/Pipfile +++ b/backend/Pipfile @@ -13,6 +13,7 @@ transliterate = "*" djangorestframework-simplejwt = "*" django-cors-headers = "*" requests = "*" +pytz = "*" [dev-packages] diff --git a/backend/Pipfile.lock b/backend/Pipfile.lock index 6b6f9cd..304742e 100644 --- a/backend/Pipfile.lock +++ b/backend/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "cb9b63f4897299c7cedf6a49a97a73a97587c60df970d9c7044fec2f6f3861c8" + "sha256": "94474e1990b5d4d58689cca46373a808771ba11de6b9dc4bda6cdbdd93a4bb70" }, "pipfile-spec": 6, "requires": { @@ -296,6 +296,14 @@ "markers": "python_version >= '3.9'", "version": "==1.1.0" }, + "pytz": { + "hashes": [ + "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", + "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00" + ], + "index": "pypi", + "version": "==2025.2" + }, "requests": { "hashes": [ "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", diff --git a/backend/api/account/client/serializers.py b/backend/api/account/client/serializers.py index e69de29..2531744 100644 --- a/backend/api/account/client/serializers.py +++ b/backend/api/account/client/serializers.py @@ -0,0 +1,58 @@ +from rest_framework import serializers +from routes.models import Route, City +from django.conf import settings +from routes.constants.routeChoices import cargo_type_choices, type_transport_choices +import pytz + +class RouteSerializer(serializers.ModelSerializer): + from_city_name = serializers.SerializerMethodField() + to_city_name = serializers.SerializerMethodField() + formatted_departure = serializers.SerializerMethodField() + formatted_arrival = serializers.SerializerMethodField() + formatted_cargo_type = serializers.SerializerMethodField() + formatted_transport = serializers.SerializerMethodField() + + class Meta: + model = Route + fields = '__all__' + + def get_from_city_name(self, obj): + try: + city = City.objects.get(id=obj.from_city_id) + return city.name + except City.DoesNotExist: + return None + + def get_to_city_name(self, obj): + try: + city = City.objects.get(id=obj.to_city_id) + return city.name + except City.DoesNotExist: + return None + + def _convert_to_local_time(self, dt): + if dt is None: + return None + # проверяем что у datetime есть временная зона (если нет, считаем UTC) + if dt.tzinfo is None: + dt = pytz.UTC.localize(dt) + # конвертируем в локальную временную зону + local_tz = pytz.timezone(settings.TIME_ZONE) + local_dt = dt.astimezone(local_tz) + return local_dt + + def get_formatted_departure(self, obj): + local_dt = self._convert_to_local_time(obj.departure_DT) + return local_dt.strftime("%d.%m.%Y, %H:%M") if local_dt else None + + def get_formatted_arrival(self, obj): + local_dt = self._convert_to_local_time(obj.arrival_DT) + return local_dt.strftime("%d.%m.%Y, %H:%M") if local_dt else None + + def get_formatted_cargo_type(self, obj): + cargo_types = dict(cargo_type_choices) + return cargo_types.get(obj.cargo_type, obj.cargo_type) + + def get_formatted_transport(self, obj): + transport_types = dict(type_transport_choices) + return transport_types.get(obj.type_transport, obj.type_transport) diff --git a/backend/api/account/client/views.py b/backend/api/account/client/views.py index 340a3ef..1aa59af 100644 --- a/backend/api/account/client/views.py +++ b/backend/api/account/client/views.py @@ -11,6 +11,8 @@ from django.core.exceptions import ValidationError 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 class UserDataView(ViewSet): permission_classes = [IsAuthenticated] @@ -82,4 +84,11 @@ class AccountActionsView(ViewSet): return Response({ "message": "Данные успешно обновлены", "user": UserResponseSerializer(user).data - }, status=status.HTTP_200_OK) \ No newline at end of file + }, status=status.HTTP_200_OK) + + @action(detail=False, methods=['get']) + @handle_exceptions + def user_routes(self, request): + user = request.user + routes = Route.objects.filter(owner=user) + return Response(RouteSerializer(routes, many=True).data, status=status.HTTP_200_OK) diff --git a/backend/api/admin.py b/backend/api/admin.py index 07c000a..e5f28ef 100644 --- a/backend/api/admin.py +++ b/backend/api/admin.py @@ -6,8 +6,8 @@ from routes.models import Route class RouteInline(admin.TabularInline): model = Route - fields = ('owner_type', 'type_transport', 'from_city', 'to_city', 'cargo_type', 'departure_DT', 'arrival_DT') - readonly_fields = ('owner_type', 'type_transport', 'from_city', 'to_city', 'cargo_type', 'departure_DT', 'arrival_DT') + fields = ('owner_type', 'type_transport', 'from_city', 'to_city', 'cargo_type', 'departure_DT', 'arrival_DT', 'comment') + # readonly_fields = ('owner_type', 'type_transport', 'from_city', 'to_city', 'cargo_type', 'departure_DT', 'arrival_DT') extra = 0 can_delete = False verbose_name = 'Маршрут пользователя' diff --git a/backend/api/urls.py b/backend/api/urls.py index 21c926f..298dd6e 100644 --- a/backend/api/urls.py +++ b/backend/api/urls.py @@ -22,5 +22,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/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') ] \ No newline at end of file diff --git a/backend/requirements.txt b/backend/requirements.txt index 48e8ce1..d8f96d3 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -10,6 +10,7 @@ pillow==11.2.1 psycopg2==2.9.10 PyJWT==2.9.0 python-dotenv==1.1.0 +pytz==2025.2 requests==2.32.3 six==1.17.0 sqlparse==0.5.3 diff --git a/frontend/app/(urls)/account/create-as-deliveler/page.tsx b/frontend/app/(urls)/account/create-as-deliveler/page.tsx index 2868bf2..86be183 100644 --- a/frontend/app/(urls)/account/create-as-deliveler/page.tsx +++ b/frontend/app/(urls)/account/create-as-deliveler/page.tsx @@ -1,7 +1,17 @@ import React from 'react' -const page = () => { - return
page
+const DelivelerPage = () => { + return ( +
+
+
+
+

Перевезти посылку

+
+
+
+
+ ) } -export default page +export default DelivelerPage diff --git a/frontend/app/(urls)/account/create-as-sender/page.tsx b/frontend/app/(urls)/account/create-as-sender/page.tsx index 2868bf2..0ca1448 100644 --- a/frontend/app/(urls)/account/create-as-sender/page.tsx +++ b/frontend/app/(urls)/account/create-as-sender/page.tsx @@ -1,7 +1,17 @@ import React from 'react' -const page = () => { - return
page
+const SenderPage = () => { + return ( +
+
+
+
+

Отправить посылку

+
+
+
+
+ ) } -export default page +export default SenderPage diff --git a/frontend/app/(urls)/account/layout.tsx b/frontend/app/(urls)/account/layout.tsx index db07a4f..b49e3f8 100644 --- a/frontend/app/(urls)/account/layout.tsx +++ b/frontend/app/(urls)/account/layout.tsx @@ -42,12 +42,12 @@ export default function AccountLayout({ { name: 'Мои маршруты', href: '/account/routes', icon: FaRoute }, { name: 'Отправить посылку', - href: '/account/create_as_sender', + href: '/account/create-as-sender', icon: GoPackageDependents, }, { name: 'Перевезти посылку', - href: '/account/create_as_deliveler', + href: '/account/create-as-deliveler', icon: GoPackageDependencies, }, { name: 'Тарифы', href: '/account/payments', icon: MdOutlinePayments }, diff --git a/frontend/app/(urls)/account/routes/page.tsx b/frontend/app/(urls)/account/routes/page.tsx index 2868bf2..99a5a63 100644 --- a/frontend/app/(urls)/account/routes/page.tsx +++ b/frontend/app/(urls)/account/routes/page.tsx @@ -1,7 +1,152 @@ import React from 'react' +import { cookies } from 'next/headers' +import { headers } from 'next/headers' -const page = () => { - return
page
+interface Route { + id: string + from_city_name: string + to_city_name: string + formatted_departure: string + formatted_arrival: string + formatted_cargo_type: string + formatted_transport: string + comment?: string + owner_type: string } -export default page +async function getRoutes() { + const cookieStore = await cookies() + const headersList = await headers() + const protocol = headersList.get('x-forwarded-proto') || 'http' + const host = headersList.get('host') || 'localhost:3000' + + const response = await fetch(`${protocol}://${host}/api/account/routes`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Cookie: cookieStore.toString(), + }, + }) + + if (!response.ok) { + const error = await response.json() + console.error('Error fetching routes:', error) + throw new Error(error.message || 'Failed to fetch routes') + } + + return response.json() +} + +export default async function UserRoutes() { + let routes: Route[] = [] + try { + routes = (await getRoutes()) || [] + } catch (error) { + console.error('Component error:', error) + return ( +
+
+ {error instanceof Error + ? error.message + : 'Не удалось загрузить маршруты'} +
+
+ ) + } + + return ( +
+
+
+
+

Мои маршруты

+
+ {(!routes || routes.length === 0) && ( +
+

У вас пока нет завершенных маршрутов

+

+ Создавайте или принимайте заявки на перевозку, чтобы они + отобразились тут +

+
+ )} + {routes.length > 0 && ( +
+ {routes.map((route) => ( +
+
+
+ ID маршрута: #{route.id} +
+
+ {route.owner_type === 'customer' + ? 'Заказчик' + : 'Перевозчик'} +
+
+ +
+
+
+
+
+ {route.from_city_name} +
+
+ {route.formatted_departure} +
+
+
+
+
+
+
+
{route.to_city_name}
+
+ {route.formatted_arrival} +
+
+
+
+ +
+
+
Тип груза:
+
+ {route.formatted_cargo_type} +
+
+
+
Способ перевозки:
+
+ {route.formatted_transport} +
+
+
+ + {route.comment && ( +
+
+ Комментарий:{' '} + {route.comment} +
+
+ )} +
+ ))} +
+ )} +
+
+
+ ) +} diff --git a/frontend/app/api/account/deliveler/route.ts b/frontend/app/api/account/deliveler/route.ts new file mode 100644 index 0000000..e69de29 diff --git a/frontend/app/api/account/routes/route.ts b/frontend/app/api/account/routes/route.ts index af1e585..7db3dc7 100644 --- a/frontend/app/api/account/routes/route.ts +++ b/frontend/app/api/account/routes/route.ts @@ -1,17 +1,40 @@ import { NextRequest } from 'next/server' +import { getServerSession } from 'next-auth' +import { authOptions } from '@/app/api/auth/[...nextauth]/route' export async function GET(req: NextRequest) { - // получить список маршрутов/локаций пользователя -} + try { + const session = await getServerSession(authOptions) + if (!session) { + console.error('No session found') + return new Response(JSON.stringify({ error: 'Unauthorized' }), { + status: 401, + }) + } -export async function POST(req: NextRequest) { - // добавить новый маршрут -} + const response = await fetch( + `${process.env.NEXT_PUBLIC_API_URL}/account/routes/`, + { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${session.accessToken}`, + }, + } + ) -export async function PUT(req: NextRequest) { - // обновить маршрут -} + if (!response.ok) { + const error = await response.json() + console.error('API error:', error) + return new Response(JSON.stringify(error), { status: response.status }) + } -export async function DELETE(req: NextRequest) { - // удалить маршрут + const result = await response.json() + return new Response(JSON.stringify(result), { status: 200 }) + } catch (error) { + console.error('Route handler error:', error) + return new Response(JSON.stringify({ error: 'Internal Server Error' }), { + status: 500, + }) + } } diff --git a/frontend/app/api/account/sender/route.ts b/frontend/app/api/account/sender/route.ts new file mode 100644 index 0000000..e69de29 diff --git a/frontend/components/Header.tsx b/frontend/components/Header.tsx index 8adf259..a806dc8 100644 --- a/frontend/components/Header.tsx +++ b/frontend/components/Header.tsx @@ -40,14 +40,6 @@ const Header = () => {
- - user -
diff --git a/frontend/components/UserLogin.tsx b/frontend/components/UserLogin.tsx index 74a8f52..f61f3f4 100644 --- a/frontend/components/UserLogin.tsx +++ b/frontend/components/UserLogin.tsx @@ -2,6 +2,7 @@ import React from 'react' import Link from 'next/link' +import Image from 'next/image' import Button from './ui/Button' import useUserStore from '@/app/store/userStore' @@ -28,15 +29,30 @@ const UserLogin = () => { if (!isUserAuth) { return ( -
- - Регистрация - - / - - Войти - -
+ <> +
+ + Регистрация + + / + + Войти + +
+
+ + user + +
+ ) } @@ -44,16 +60,14 @@ const UserLogin = () => {