diff --git a/backend/api/account/client/serializers.py b/backend/api/account/client/serializers.py index d8ae3b0..e48641e 100644 --- a/backend/api/account/client/serializers.py +++ b/backend/api/account/client/serializers.py @@ -315,4 +315,38 @@ class LeadSerializer(serializers.ModelSerializer): try: return super().to_internal_value(data) except Exception as e: - raise \ No newline at end of file + raise + +class LeadResponseSerializer(serializers.ModelSerializer): + route = RouteSerializer(read_only=True) + moving_price = serializers.DecimalField(max_digits=10, decimal_places=2) + created_at = serializers.DateTimeField() + owner_name = serializers.SerializerMethodField() + owner_email = serializers.SerializerMethodField() + + class Meta: + model = Leads + fields = [ + 'id', + 'route', + 'moving_user', + 'moving_price', + 'moving_date', + 'comment', + 'created_at', + 'owner_name', + 'owner_email' + ] + + def get_owner_name(self, obj): + owner = obj.route.owner + return f"{owner.first_name} {owner.last_name}".strip() if owner else None + + def get_owner_email(self, obj): + return obj.route.owner.email if obj.route.owner else None + + def to_representation(self, instance): + data = super().to_representation(instance) + if instance.created_at: + data['created_at'] = instance.created_at.strftime('%Y-%m-%dT%H:%M:%S.%f%z') + return data \ No newline at end of file diff --git a/backend/api/account/client/views.py b/backend/api/account/client/views.py index 01de539..66d434f 100644 --- a/backend/api/account/client/views.py +++ b/backend/api/account/client/views.py @@ -12,9 +12,9 @@ from django.db import models 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, LeadSerializer +from .serializers import RouteSerializer, CreateRouteSerializer, CitySerializer, CountrySerializer, PlanChangeSerializer, PricingSerializer, LeadSerializer, LeadResponseSerializer class UserDataView(ViewSet): """Эндпоинт для наполнения стора фронта данными""" @@ -259,4 +259,21 @@ class LeadViewSet(ViewSet): }, status=status.HTTP_400_BAD_REQUEST ) - \ No newline at end of file + + @action(detail=False, methods=['get']) + @handle_exceptions + def get_leads(self, request): + """Получаем список заявок на перевозку""" + leads = Leads.objects.select_related( + 'route', + 'route__from_city', + 'route__to_city', + 'route__from_city__country', + 'route__to_city__country', + 'route__owner' + ).filter(moving_user=request.user).order_by('-created_at') + + return Response( + LeadResponseSerializer(leads, many=True).data, + status=status.HTTP_200_OK + ) \ No newline at end of file diff --git a/backend/api/urls.py b/backend/api/urls.py index ef11fcc..ed1708b 100644 --- a/backend/api/urls.py +++ b/backend/api/urls.py @@ -37,6 +37,7 @@ urlpatterns = [ path("v1/account/create_route/", AccountActionsView.as_view({'post':'create_route'}), name='create_route'), path("v1/account/send_lead/", LeadViewSet.as_view({'post':'send_lead'}), name='send_lead'), + path("v1/account/leads/", LeadViewSet.as_view({'get':'get_leads'}), name='get_leads'), 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/(urls)/account/layout.tsx b/frontend/app/(urls)/account/layout.tsx index 9c2ae6d..b7d7820 100644 --- a/frontend/app/(urls)/account/layout.tsx +++ b/frontend/app/(urls)/account/layout.tsx @@ -8,6 +8,7 @@ import { RiUser3Line } from 'react-icons/ri' import { FaRoute } from 'react-icons/fa' import { GoPackageDependents, GoPackageDependencies } from 'react-icons/go' import { MdOutlinePayments, MdOutlineContactSupport } from 'react-icons/md' +import { IoGitPullRequest } from 'react-icons/io5' import useUserStore from '@/app/store/userStore' export default function AccountLayout({ children }: { children: React.ReactNode }) { @@ -35,6 +36,7 @@ export default function AccountLayout({ children }: { children: React.ReactNode const userNavigation = [ { name: 'Профиль', href: '/account', icon: RiUser3Line }, { name: 'Мои маршруты', href: '/account/routes', icon: FaRoute }, + { name: 'Мои отклики', href: '/account/responses', icon: IoGitPullRequest }, { name: 'Отправить посылку', href: '/account/create-as-sender', diff --git a/frontend/app/(urls)/account/responses/page.tsx b/frontend/app/(urls)/account/responses/page.tsx new file mode 100644 index 0000000..9495aae --- /dev/null +++ b/frontend/app/(urls)/account/responses/page.tsx @@ -0,0 +1,139 @@ +'use client' +import React, { useEffect, useState } from 'react' +import Loader from '@/components/ui/Loader' +import { LeadPageProps } from '@/app/types' + +const ResponsesPage = () => { + const [leads, setLeads] = useState([]) + const [isLoading, setIsLoading] = useState(true) + const [error, setError] = useState(null) + + useEffect(() => { + const fetchLeads = async () => { + try { + const response = await fetch('/api/account/requests') + if (!response.ok) { + throw new Error('Failed to fetch leads') + } + const data = await response.json() + + setLeads(data) + } catch (err) { + console.error('Component error:', err) + setError(err instanceof Error ? err.message : 'Failed to load leads') + } finally { + setIsLoading(false) + } + } + + fetchLeads() + }, []) + + if (isLoading) { + return + } + + if (error) { + return
{error}
+ } + + return ( +
+
+
+
+
+

Мои отклики

+
+
+ + {leads.length > 0 && ( +
+ {leads.map(lead => ( +
+
+
+
+
Маршрут
+
#{lead.route.id}
+
+ {lead.route.from_city_name}, {lead.route.from_country_name} →{' '} + {lead.route.to_city_name}, {lead.route.to_country_name} +
+
+ {lead.route.formatted_departure} - {lead.route.formatted_arrival} +
+
+ Тип груза:{' '} + {lead.route.formatted_cargo_type} •{' '} + Транспорт:{' '} + {lead.route.formatted_transport} +
+ {lead.route.comment && ( +
+ Комментарий к маршруту:{' '} + {lead.route.comment} +
+ )} +
+ + + + {lead.owner_name} + + + {lead.owner_email} + +
+
+
+
Предложенная цена
+
{lead.moving_price} тенге
+
+
+ + {lead.comment && ( +
+
Комментарий к отклику:
+
{lead.comment}
+
+ )} + +
+ Отправлено:{' '} + {new Date(lead.created_at).toLocaleString('ru-RU', { + year: 'numeric', + month: 'long', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + })} +
+
+
+ ))} +
+ )} +
+
+
+ ) +} + +export default ResponsesPage diff --git a/frontend/app/(urls)/account/routes/page.tsx b/frontend/app/(urls)/account/routes/page.tsx index 2e14a12..176f0de 100644 --- a/frontend/app/(urls)/account/routes/page.tsx +++ b/frontend/app/(urls)/account/routes/page.tsx @@ -50,10 +50,8 @@ export default async function UserRoutes() { {(!routes || routes.length === 0) && (
-

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

-

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

+

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

+

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

)} {routes.length > 0 && ( diff --git a/frontend/app/api/account/requests/route.ts b/frontend/app/api/account/requests/route.ts new file mode 100644 index 0000000..e8fbbf4 --- /dev/null +++ b/frontend/app/api/account/requests/route.ts @@ -0,0 +1,38 @@ +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, + }) + } + + const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/account/leads/`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${session.accessToken}`, + }, + }) + + if (!response.ok) { + const error = await response.json() + console.error('API error:', error) + return new Response(JSON.stringify(error), { status: response.status }) + } + + 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/types/index.ts b/frontend/app/types/index.ts index 07b399d..81db2b2 100644 --- a/frontend/app/types/index.ts +++ b/frontend/app/types/index.ts @@ -157,7 +157,7 @@ export interface TextAreaProps { } export interface Route { - id: string + id: string | number from_city_name: string to_city_name: string from_country_name: string @@ -168,6 +168,9 @@ export interface Route { formatted_transport: string comment?: string owner_type: string + status: string + owner_email: string + owner_name: string } export interface SelectOption { @@ -238,11 +241,20 @@ export interface SearchPageProps { } export interface Lead { + id: number name: string phone_number: string email: string + route: Route + moving_user: number moving_price: string moving_date: string - comment: string - route?: number + comment?: string + created_at: string + status: 'actual' | 'canceled' | 'completed' +} + +export interface LeadPageProps extends Lead { + owner_name: string + owner_email: string }