create RouteForm using sender page

This commit is contained in:
2025-05-22 14:22:13 +03:00
parent b0aef18c38
commit f3233ae175
4 changed files with 387 additions and 364 deletions

View File

@@ -106,7 +106,7 @@ class CreateRouteSerializer(serializers.ModelSerializer):
transport = serializers.ChoiceField(choices=type_transport_choices, source='type_transport')
email_notification = serializers.BooleanField(source='receive_msg_by_email')
phone_number = serializers.CharField(write_only=True)
owner_type = serializers.ChoiceField(choices=[('sender', 'Отправитель'), ('deliverer', 'Перевозчик')])
owner_type = serializers.ChoiceField(choices=owner_type_choices)
class Meta:
model = Route

View File

@@ -1,17 +1,14 @@
import React from 'react'
import MultiSelect from '@/components/ui/Selector'
import RouteForm from '@/components/forms/RouteForm'
const DelivelerPage = () => {
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">
<h1 className="text-2xl">Перевезти посылку</h1>
</div>
</div>
</div>
</div>
<RouteForm
routeHandlerUrl="/api/account/sender"
ownerType="customer"
title="Перевезти посылку"
description="Заполните информацию о вашей посылке и маршруте"
/>
)
}

View File

@@ -1,362 +1,16 @@
'use client'
import React from 'react'
import PhoneInput from '@/components/ui/PhoneInput'
import Button from '@/components/ui/Button'
import TextAreaInput from '@/components/ui/TextAreaInput'
import CheckboxInput from '@/components/ui/CheckboxInput'
import LocationSelect from '@/components/ui/LocationSelect'
import SingleSelect from '@/components/ui/SingleSelect'
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 {
cargo_types,
cargo_type_translations,
transport_types,
transport_translations,
} from '@/app/constants'
const formatDateToHTML = (date: Date) => {
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
return `${year}-${month}-${day}T${hours}:${minutes}`
}
const validationRules = {
transport: { required: true },
country_from_id: { required: true },
city_from: { required: true },
country_to_id: { required: true },
city_to: { required: true },
cargo_type: { required: true },
departure: {
required: true,
pattern: /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/,
},
arrival: {
required: true,
pattern: /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/,
},
phone_number: {
required: true,
minLength: 11,
pattern: /^\+?[0-9]{11,}$/,
},
comment: { required: false },
email_notification: { required: false },
}
import RouteForm from '@/components/forms/RouteForm'
const SenderPage = () => {
const { user, setUser } = useUserStore()
const today = formatDateToHTML(new Date())
const initialValues: SenderPageProps = {
transport: '',
country_from: '',
city_from: '',
country_to: '',
city_to: '',
country_from_id: '',
country_to_id: '',
cargo_type: '',
departure: '',
arrival: '',
phone_number: user?.phone_number || '',
comment: '',
email_notification: false,
}
const cargoOptions: SelectOption[] = cargo_types.map((type, index) => ({
id: index + 1,
value: type,
label: cargo_type_translations[type],
}))
const transportOptions: SelectOption[] = transport_types.map((type, index) => ({
id: index + 1,
value: type,
label: transport_translations[type],
}))
const { values, handleChange, handleSubmit, resetField } = useForm<SenderPageProps>(
initialValues,
validationRules,
async values => {
try {
// находим выбранные опции
const selectedTransport = transportOptions.find(
opt => opt.id.toString() === values.transport
)
const selectedCargoType = cargoOptions.find(opt => opt.id.toString() === values.cargo_type)
if (!selectedTransport || !selectedCargoType) {
throw new Error('Некорректный тип транспорта или груза')
}
// подготавливаем данные для отправки
const requestData = {
...values,
owner_type: 'sender',
transport: selectedTransport.value,
cargo_type: selectedCargoType.value,
phone_number: values.phone_number,
}
const response = await fetch('/api/account/sender', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(requestData),
})
if (!response.ok) {
const error = await response.json()
console.error('Ошибка от сервера:', error)
throw new Error(error.error || 'Ошибка при создании маршрута')
}
// Обновляем номер телефона в сторе, если он изменился
if (user && user.phone_number !== values.phone_number) {
setUser({
...user,
phone_number: values.phone_number,
})
}
showToast({
type: 'success',
message: 'Маршрут успешно создан!',
})
// сбрасываем все поля формы кроме телефона
Object.keys(initialValues).forEach(field => {
if (field !== 'phone_number') {
resetField(field)
}
})
} catch (error) {
console.error('Ошибка:', error)
showToast({
type: 'error',
message: error instanceof Error ? error.message : 'Ой, что то пошло не так..',
})
}
}
)
return (
<form onSubmit={handleSubmit} className="space-y-6">
<div className="overflow-hidden rounded-2xl bg-white shadow">
<div className="p-6 sm:p-8">
<div className="space-y-8">
<div>
<h1 className="text-2xl font-semibold text-gray-900">Отправить посылку</h1>
<p className="mt-1 text-sm text-gray-600">
Заполните информацию о вашей посылке и маршруте
</p>
</div>
{/* тип груза и транспорта */}
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2">
<div>
<SingleSelect
name="cargo_type"
value={values.cargo_type}
handleChange={handleChange}
label="Тип груза"
placeholder="Выберите тип груза"
options={cargoOptions}
className="mt-1"
noOptionsMessage="Нет доступных типов груза"
/>
</div>
<div>
<SingleSelect
name="transport"
value={values.transport}
handleChange={handleChange}
label="Способ перевозки"
placeholder="Выберите способ перевозки"
options={transportOptions}
className="mt-1"
noOptionsMessage="Нет доступных способов перевозки"
/>
</div>
</div>
{/* маршрут */}
<div>
<h2 className="mb-2 text-xl font-medium text-gray-900">Маршрут</h2>
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2">
<div className="space-y-6">
<LocationSelect
name="country_from"
value={values.country_from}
handleChange={e => {
const selectedOption = e.target.selectedOption
handleChange({
target: {
id: 'country_from',
value: e.target.value,
},
})
handleChange({
target: {
id: 'country_from_id',
value: selectedOption?.id?.toString() || '',
},
})
handleChange({
target: {
id: 'city_from',
value: '',
},
})
}}
label="Страна отправления"
placeholder="Выберите страну отправления"
/>
<LocationSelect
name="city_from"
value={values.city_from}
handleChange={handleChange}
label="Город отправления"
placeholder="Выберите город отправления"
countryId={values.country_from_id}
isCity
/>
</div>
<div className="space-y-6">
<LocationSelect
name="country_to"
value={values.country_to}
handleChange={e => {
const selectedOption = e.target.selectedOption
handleChange({
target: {
id: 'country_to',
value: e.target.value,
},
})
handleChange({
target: {
id: 'country_to_id',
value: selectedOption?.id?.toString() || '',
},
})
handleChange({
target: {
id: 'city_to',
value: '',
},
})
}}
label="Страна назначения"
placeholder="Выберите страну назначения"
/>
<LocationSelect
name="city_to"
value={values.city_to}
handleChange={handleChange}
label="Город назначения"
placeholder="Выберите город назначения"
countryId={values.country_to_id}
isCity
/>
</div>
</div>
</div>
{/* даты */}
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2">
<div>
<label htmlFor="departure" className="block text-sm font-medium text-gray-700">
Дата отправления
</label>
<input
type="datetime-local"
name="departure"
id="departure"
value={values.departure}
onChange={handleChange}
min={today}
className="mt-1 block w-full rounded-xl border border-gray-300 px-3 py-2 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none"
/>
</div>
<div>
<label htmlFor="arrival" className="block text-sm font-medium text-gray-700">
Дата прибытия
</label>
<input
type="datetime-local"
name="arrival"
id="arrival"
value={values.arrival}
onChange={handleChange}
min={values.departure || today}
className="mt-1 block w-full rounded-xl border border-gray-300 px-3 py-2 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none"
/>
</div>
</div>
{/* контактная информация */}
<h2 className="mb-2 text-xl font-medium text-gray-900">Контактная информация</h2>
<div className="space-y-6">
<PhoneInput
value={values.phone_number}
handleChange={handleChange}
label="Контактный телефон"
operatorsInfo={false}
/>
<TextAreaInput
value={values.comment}
handleChange={handleChange}
label="Комментарий"
placeholder="Дополнительная информация о грузе или пожелания"
name="comment"
/>
<CheckboxInput
name="email_notification"
label="Уведомления"
checked={values.email_notification}
handleChange={handleChange}
enabledText="Хочу получать уведомления по email"
disabledText="Не получать уведомления"
info="Вы будете получать уведомления о важных событиях на указанный email адрес"
/>
</div>
</div>
</div>
</div>
{/* кнопки действий */}
<div className="flex justify-end space-x-4">
<Button
type="button"
text="Отмена"
className="border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:outline-none"
/>
<Button
type="submit"
text="Создать маршрут"
className="bg-orange hover:bg-orange/80 focus:ring-red w-1/3 border border-gray-300 py-2 text-sm font-medium text-white shadow-sm focus:ring-2 focus:ring-offset-2 focus:outline-none"
/>
</div>
</form>
<RouteForm
routeHandlerUrl="/api/account/sender"
ownerType="mover"
title="Отправить посылку"
description="Заполните информацию о вашей посылке и маршруте"
/>
)
}

View File

@@ -0,0 +1,372 @@
'use client'
import React from 'react'
import PhoneInput from '@/components/ui/PhoneInput'
import Button from '@/components/ui/Button'
import TextAreaInput from '@/components/ui/TextAreaInput'
import CheckboxInput from '@/components/ui/CheckboxInput'
import LocationSelect from '@/components/ui/LocationSelect'
import SingleSelect from '@/components/ui/SingleSelect'
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 {
cargo_types,
cargo_type_translations,
transport_types,
transport_translations,
} from '@/app/constants'
interface RouteFormProps {
routeHandlerUrl: string
ownerType: 'customer' | 'mover'
title: string
description: string
}
const formatDateToHTML = (date: Date) => {
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
return `${year}-${month}-${day}T${hours}:${minutes}`
}
const validationRules = {
transport: { required: true },
country_from_id: { required: true },
city_from: { required: true },
country_to_id: { required: true },
city_to: { required: true },
cargo_type: { required: true },
departure: {
required: true,
pattern: /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/,
},
arrival: {
required: true,
pattern: /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/,
},
phone_number: {
required: true,
minLength: 11,
pattern: /^\+?[0-9]{11,}$/,
},
comment: { required: false },
email_notification: { required: false },
}
const RouteForm: React.FC<RouteFormProps> = ({
routeHandlerUrl,
ownerType,
title,
description,
}) => {
const { user, setUser } = useUserStore()
const today = formatDateToHTML(new Date())
const initialValues: SenderPageProps = {
transport: '',
country_from: '',
city_from: '',
country_to: '',
city_to: '',
country_from_id: '',
country_to_id: '',
cargo_type: '',
departure: '',
arrival: '',
phone_number: user?.phone_number || '',
comment: '',
email_notification: false,
}
const cargoOptions: SelectOption[] = cargo_types.map((type, index) => ({
id: index + 1,
value: type,
label: cargo_type_translations[type],
}))
const transportOptions: SelectOption[] = transport_types.map((type, index) => ({
id: index + 1,
value: type,
label: transport_translations[type],
}))
const { values, handleChange, handleSubmit, resetField } = useForm<SenderPageProps>(
initialValues,
validationRules,
async values => {
try {
// находим выбранные опции
const selectedTransport = transportOptions.find(
opt => opt.id.toString() === values.transport
)
const selectedCargoType = cargoOptions.find(opt => opt.id.toString() === values.cargo_type)
if (!selectedTransport || !selectedCargoType) {
throw new Error('Некорректный тип транспорта или груза')
}
// подготавливаем данные для отправки
const requestData = {
...values,
owner_type: ownerType,
transport: selectedTransport.value,
cargo_type: selectedCargoType.value,
phone_number: values.phone_number,
}
const response = await fetch(routeHandlerUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(requestData),
})
if (!response.ok) {
const error = await response.json()
console.error('Ошибка от сервера:', error)
throw new Error(error.error || 'Ошибка при создании маршрута')
}
// Обновляем номер телефона в сторе, если он изменился
if (user && user.phone_number !== values.phone_number) {
setUser({
...user,
phone_number: values.phone_number,
})
}
showToast({
type: 'success',
message: 'Маршрут успешно создан!',
})
// сбрасываем все поля формы кроме телефона
Object.keys(initialValues).forEach(field => {
if (field !== 'phone_number') {
resetField(field)
}
})
} catch (error) {
console.error('Ошибка:', error)
showToast({
type: 'error',
message: error instanceof Error ? error.message : 'Ой, что то пошло не так..',
})
}
}
)
return (
<form onSubmit={handleSubmit} className="space-y-6">
<div className="overflow-hidden rounded-2xl bg-white shadow">
<div className="p-6 sm:p-8">
<div className="space-y-8">
<div>
<h1 className="text-2xl font-semibold text-gray-900">{title}</h1>
<p className="mt-1 text-sm text-gray-600">{description}</p>
</div>
{/* тип груза и транспорта */}
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2">
<div>
<SingleSelect
name="cargo_type"
value={values.cargo_type}
handleChange={handleChange}
label="Тип груза"
placeholder="Выберите тип груза"
options={cargoOptions}
className="mt-1"
noOptionsMessage="Нет доступных типов груза"
/>
</div>
<div>
<SingleSelect
name="transport"
value={values.transport}
handleChange={handleChange}
label="Способ перевозки"
placeholder="Выберите способ перевозки"
options={transportOptions}
className="mt-1"
noOptionsMessage="Нет доступных способов перевозки"
/>
</div>
</div>
{/* маршрут */}
<div>
<h2 className="mb-2 text-xl font-medium text-gray-900">Маршрут</h2>
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2">
<div className="space-y-6">
<LocationSelect
name="country_from"
value={values.country_from}
handleChange={e => {
const selectedOption = e.target.selectedOption
handleChange({
target: {
id: 'country_from',
value: e.target.value,
},
})
handleChange({
target: {
id: 'country_from_id',
value: selectedOption?.id?.toString() || '',
},
})
handleChange({
target: {
id: 'city_from',
value: '',
},
})
}}
label="Страна отправления"
placeholder="Выберите страну отправления"
/>
<LocationSelect
name="city_from"
value={values.city_from}
handleChange={handleChange}
label="Город отправления"
placeholder="Выберите город отправления"
countryId={values.country_from_id}
isCity
/>
</div>
<div className="space-y-6">
<LocationSelect
name="country_to"
value={values.country_to}
handleChange={e => {
const selectedOption = e.target.selectedOption
handleChange({
target: {
id: 'country_to',
value: e.target.value,
},
})
handleChange({
target: {
id: 'country_to_id',
value: selectedOption?.id?.toString() || '',
},
})
handleChange({
target: {
id: 'city_to',
value: '',
},
})
}}
label="Страна назначения"
placeholder="Выберите страну назначения"
/>
<LocationSelect
name="city_to"
value={values.city_to}
handleChange={handleChange}
label="Город назначения"
placeholder="Выберите город назначения"
countryId={values.country_to_id}
isCity
/>
</div>
</div>
</div>
{/* даты */}
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2">
<div>
<label htmlFor="departure" className="block text-sm font-medium text-gray-700">
Дата отправления
</label>
<input
type="datetime-local"
name="departure"
id="departure"
value={values.departure}
onChange={handleChange}
min={today}
className="mt-1 block w-full rounded-xl border border-gray-300 px-3 py-2 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none"
/>
</div>
<div>
<label htmlFor="arrival" className="block text-sm font-medium text-gray-700">
Дата прибытия
</label>
<input
type="datetime-local"
name="arrival"
id="arrival"
value={values.arrival}
onChange={handleChange}
min={values.departure || today}
className="mt-1 block w-full rounded-xl border border-gray-300 px-3 py-2 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none"
/>
</div>
</div>
{/* контактная информация */}
<h2 className="mb-2 text-xl font-medium text-gray-900">Контактная информация</h2>
<div className="space-y-6">
<PhoneInput
value={values.phone_number}
handleChange={handleChange}
label="Контактный телефон"
operatorsInfo={false}
/>
<TextAreaInput
value={values.comment}
handleChange={handleChange}
label="Комментарий"
placeholder="Дополнительная информация о грузе или пожелания"
name="comment"
/>
<CheckboxInput
name="email_notification"
label="Уведомления"
checked={values.email_notification}
handleChange={handleChange}
enabledText="Хочу получать уведомления по email"
disabledText="Не получать уведомления"
info="Вы будете получать уведомления о важных событиях на указанный email адрес"
/>
</div>
</div>
</div>
</div>
{/* кнопки действий */}
<div className="flex justify-end space-x-4">
<Button
type="button"
text="Отмена"
className="border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:outline-none"
/>
<Button
type="submit"
text="Создать маршрут"
className="bg-orange hover:bg-orange/80 focus:ring-red w-1/3 border border-gray-300 py-2 text-sm font-medium text-white shadow-sm focus:ring-2 focus:ring-offset-2 focus:outline-none"
/>
</div>
</form>
)
}
export default RouteForm