account/responses page

This commit is contained in:
2025-05-26 12:15:32 +03:00
parent 50e6da10f1
commit c761c60818
8 changed files with 252 additions and 11 deletions

View File

@@ -315,4 +315,38 @@ class LeadSerializer(serializers.ModelSerializer):
try:
return super().to_internal_value(data)
except Exception as e:
raise
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

View File

@@ -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
)
@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
)

View File

@@ -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'),

View File

@@ -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',

View File

@@ -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<LeadPageProps[]>([])
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState<string | null>(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 <Loader />
}
if (error) {
return <div className="p-8 text-center text-red-500">{error}</div>
}
return (
<div className="space-y-3">
<div className="overflow-hidden rounded-2xl shadow">
<div className="bg-white p-6 sm:p-8">
<div className="space-y-4">
<div className="flex items-center justify-between">
<h1 className="text-2xl">Мои отклики</h1>
</div>
</div>
{leads.length > 0 && (
<div className="mt-4 space-y-4">
{leads.map(lead => (
<div
key={lead.id}
className="space-y-4 rounded-2xl border bg-white p-6 transition-shadow hover:shadow-md"
>
<div className="flex flex-col space-y-4">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between">
<div>
<div className="text-sm text-gray-500">Маршрут</div>
<div className="font-medium">#{lead.route.id}</div>
<div className="mt-1 text-sm text-gray-600">
{lead.route.from_city_name}, {lead.route.from_country_name} {' '}
{lead.route.to_city_name}, {lead.route.to_country_name}
</div>
<div className="mt-1 text-xs text-gray-500">
{lead.route.formatted_departure} - {lead.route.formatted_arrival}
</div>
<div className="mt-1 text-xs text-gray-500">
<span className="font-medium">Тип груза:</span>{' '}
{lead.route.formatted_cargo_type} {' '}
<span className="font-medium">Транспорт:</span>{' '}
{lead.route.formatted_transport}
</div>
{lead.route.comment && (
<div className="mt-1 text-xs text-gray-500">
<span className="font-medium">Комментарий к маршруту:</span>{' '}
{lead.route.comment}
</div>
)}
<div className="mt-2 flex items-center text-xs text-gray-500">
<svg
className="mr-1 h-4 w-4 text-gray-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
/>
</svg>
{lead.owner_name}
<span className="mx-1"></span>
<a
href={`mailto:${lead.owner_email}`}
className="text-blue-600 hover:text-blue-800"
>
{lead.owner_email}
</a>
</div>
</div>
<div className="mt-4 sm:mt-0 sm:text-right">
<div className="text-sm text-gray-500">Предложенная цена</div>
<div className="text-lg font-medium">{lead.moving_price} тенге</div>
</div>
</div>
{lead.comment && (
<div className="rounded-lg bg-gray-50 p-3">
<div className="text-sm text-gray-500">Комментарий к отклику:</div>
<div className="mt-1">{lead.comment}</div>
</div>
)}
<div className="text-sm text-gray-500">
Отправлено:{' '}
{new Date(lead.created_at).toLocaleString('ru-RU', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
})}
</div>
</div>
</div>
))}
</div>
)}
</div>
</div>
</div>
)
}
export default ResponsesPage

View File

@@ -50,10 +50,8 @@ export default async function UserRoutes() {
</div>
{(!routes || routes.length === 0) && (
<div className="flex flex-col items-center justify-center py-12 text-gray-500">
<p className="text-lg">У вас пока нет завершенных маршрутов</p>
<p className="text-sm">
Создавайте или принимайте заявки на перевозку, чтобы они отобразились тут
</p>
<p className="text-lg">У вас пока нет созданных маршрутов</p>
<p className="text-sm">Создавайте заявки на перевозку, чтобы они отобразились тут</p>
</div>
)}
{routes.length > 0 && (

View File

@@ -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,
})
}
}

View File

@@ -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
}