account/routes page

This commit is contained in:
2025-05-21 15:39:00 +03:00
parent d526f0730b
commit c4e1e16e79
17 changed files with 626 additions and 993 deletions

View File

@@ -13,6 +13,7 @@ transliterate = "*"
djangorestframework-simplejwt = "*"
django-cors-headers = "*"
requests = "*"
pytz = "*"
[dev-packages]

10
backend/Pipfile.lock generated
View File

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

View File

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

View File

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

View File

@@ -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 = 'Маршрут пользователя'

View File

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

View File

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

View File

@@ -1,7 +1,17 @@
import React from 'react'
const page = () => {
return <div>page</div>
const DelivelerPage = () => {
return (
<div className="space-y-3">
<div className="rounded-2xl shadow overflow-hidden">
<div className="p-6 bg-white sm:p-8">
<div className="space-y-4">
<h1 className="text-2xl">Перевезти посылку</h1>
</div>
</div>
</div>
</div>
)
}
export default page
export default DelivelerPage

View File

@@ -1,7 +1,17 @@
import React from 'react'
const page = () => {
return <div>page</div>
const SenderPage = () => {
return (
<div className="space-y-3">
<div className="rounded-2xl shadow overflow-hidden">
<div className="p-6 bg-white sm:p-8">
<div className="space-y-4">
<h1 className="text-2xl">Отправить посылку</h1>
</div>
</div>
</div>
</div>
)
}
export default page
export default SenderPage

View File

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

View File

@@ -1,7 +1,152 @@
import React from 'react'
import { cookies } from 'next/headers'
import { headers } from 'next/headers'
const page = () => {
return <div>page</div>
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 (
<div className="flex items-center justify-center py-12">
<div className="text-red-500">
{error instanceof Error
? error.message
: 'Не удалось загрузить маршруты'}
</div>
</div>
)
}
return (
<div className="space-y-3">
<div className="rounded-2xl shadow overflow-hidden">
<div className="p-6 bg-white sm:p-8">
<div className="space-y-4">
<h1 className="text-2xl">Мои маршруты</h1>
</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>
</div>
)}
{routes.length > 0 && (
<div className="mt-4 space-y-4">
{routes.map((route) => (
<div
key={route.id}
className="border rounded-2xl p-6 space-y-4 hover:shadow-md transition-shadow bg-white"
>
<div className="flex justify-between items-center">
<div className="text-sm text-gray-500">
ID маршрута: #{route.id}
</div>
<div
className={`px-3 py-1 rounded-full text-sm ${
route.owner_type === 'customer'
? 'bg-blue-100 text-blue-800'
: 'bg-green-100 text-green-800'
}`}
>
{route.owner_type === 'customer'
? 'Заказчик'
: 'Перевозчик'}
</div>
</div>
<div className="flex flex-col space-y-1">
<div className="flex items-center space-x-3">
<div className="w-4 h-4 rounded-full bg-blue-500 flex-shrink-0" />
<div>
<div className="font-medium">
{route.from_city_name}
</div>
<div className="text-sm text-gray-600">
{route.formatted_departure}
</div>
</div>
</div>
<div className="ml-[7px] w-[2px] h-6 bg-gray-300" />
<div className="flex items-center space-x-3">
<div className="w-4 h-4 rounded-full bg-green-500 flex-shrink-0" />
<div>
<div className="font-medium">{route.to_city_name}</div>
<div className="text-sm text-gray-600">
{route.formatted_arrival}
</div>
</div>
</div>
</div>
<div className="flex flex-wrap gap-4 pt-2">
<div className="flex items-center space-x-2">
<div className="text-gray-500">Тип груза:</div>
<div className="font-medium">
{route.formatted_cargo_type}
</div>
</div>
<div className="flex items-center space-x-2">
<div className="text-gray-500">Способ перевозки:</div>
<div className="font-medium">
{route.formatted_transport}
</div>
</div>
</div>
{route.comment && (
<div className="pt-2 border-t border-gray-300">
<div className="text-sm text-gray-500">
<span className="text-gray-500">Комментарий:</span>{' '}
{route.comment}
</div>
</div>
)}
</div>
))}
</div>
)}
</div>
</div>
</div>
)
}

View File

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

View File

View File

@@ -40,14 +40,6 @@ const Header = () => {
<UserLogin />
<div className="flex items-center space-x-4 md:hidden">
<Link href="/login">
<Image
src="/images/userlogo.png"
alt="user"
width={24}
height={24}
/>
</Link>
<Burger />
</div>
</div>

View File

@@ -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 (
<div className="hidden md:block text-base font-medium">
<Link href="/register" className="hover:text-orange transition-colors">
Регистрация
</Link>
<span> / </span>
<Link href="/login" className="hover:text-orange transition-colors">
Войти
</Link>
</div>
<>
<div className="hidden md:block text-base font-medium">
<Link
href="/register"
className="hover:text-orange transition-colors"
>
Регистрация
</Link>
<span> / </span>
<Link href="/login" className="hover:text-orange transition-colors">
Войти
</Link>
</div>
<div className="md:hidden">
<Link href="/login">
<Image
src="/images/userlogo.png"
alt="user"
width={24}
height={24}
/>
</Link>
</div>
</>
)
}
@@ -44,16 +60,14 @@ const UserLogin = () => {
<Link href={path} className="ml-2 sm:ml-5">
<Button
text={isUserAuth ? `Привет, ${name}!` : name}
className={`
text-sm sm:text-base
py-2 sm:py-3
px-3 sm:px-4
bg-orange text-white
flex items-center
rounded-2xl
whitespace-nowrap
hover:bg-orange/80
`}
className="hidden md:flex text-sm sm:text-base py-2 sm:py-3 px-3 sm:px-4 bg-orange text-white items-center rounded-2xl whitespace-nowrap hover:bg-orange/80"
/>
<Image
src="/images/userlogo.png"
alt="user"
width={24}
height={24}
className="md:hidden"
/>
</Link>
)

File diff suppressed because it is too large Load Diff