Profile: редактируемый профиль при verification="0", сохранение через N8N_PROFILE_UPDATE_WEBHOOK
- Backend: config.py — добавлена настройка n8n_profile_update_webhook (читает N8N_PROFILE_UPDATE_WEBHOOK из .env). - Backend: profile.py — общий хелпер _resolve_profile_identity(), обновлён _fetch_contact(), новый эндпоинт POST /api/v1/profile/contact/update, который отправляет данные профиля в N8N_PROFILE_UPDATE_WEBHOOK. - Frontend: Profile.tsx — если verification === "0", показывается форма редактирования (все поля, кроме телефона, обязательны к заполнению, телефон только для чтения) и сохранение вызывает /api/v1/profile/contact/update; иначе профиль остаётся только для просмотра.
This commit is contained in:
5
COMMIT_MSG_PROFILE_EDIT.txt
Normal file
5
COMMIT_MSG_PROFILE_EDIT.txt
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
Profile: редактируемый профиль при verification="0", сохранение через N8N_PROFILE_UPDATE_WEBHOOK
|
||||||
|
|
||||||
|
- Backend: config.py — добавлена настройка n8n_profile_update_webhook (читает N8N_PROFILE_UPDATE_WEBHOOK из .env).
|
||||||
|
- Backend: profile.py — общий хелпер _resolve_profile_identity(), обновлён _fetch_contact(), новый эндпоинт POST /api/v1/profile/contact/update, который отправляет данные профиля в N8N_PROFILE_UPDATE_WEBHOOK.
|
||||||
|
- Frontend: Profile.tsx — если verification === "0", показывается форма редактирования (все поля, кроме телефона, обязательны к заполнению, телефон только для чтения) и сохранение вызывает /api/v1/profile/contact/update; иначе профиль остаётся только для просмотра.
|
||||||
@@ -35,7 +35,7 @@ unified_id берётся из сессии по session_token или перед
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from typing import Optional
|
from typing import Optional, Tuple
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from fastapi import APIRouter, HTTPException, Query
|
from fastapi import APIRouter, HTTPException, Query
|
||||||
@@ -48,6 +48,59 @@ logger = logging.getLogger(__name__)
|
|||||||
router = APIRouter(prefix="/api/v1/profile", tags=["profile"])
|
router = APIRouter(prefix="/api/v1/profile", tags=["profile"])
|
||||||
|
|
||||||
|
|
||||||
|
async def _resolve_profile_identity(
|
||||||
|
session_token: Optional[str] = None,
|
||||||
|
unified_id: Optional[str] = None,
|
||||||
|
channel: Optional[str] = None,
|
||||||
|
channel_user_id: Optional[str] = None,
|
||||||
|
entry_channel: Optional[str] = None,
|
||||||
|
chat_id: Optional[str] = None,
|
||||||
|
) -> Tuple[str, Optional[str], Optional[str], Optional[str]]:
|
||||||
|
"""Возвращает (unified_id, contact_id, phone, chat_id). При ошибке — HTTPException(401/400)."""
|
||||||
|
contact_id: Optional[str] = None
|
||||||
|
phone: Optional[str] = None
|
||||||
|
|
||||||
|
if not unified_id and channel and channel_user_id:
|
||||||
|
try:
|
||||||
|
from app.api.session import get_session_by_channel_user
|
||||||
|
session_data = await get_session_by_channel_user(channel.strip(), str(channel_user_id).strip())
|
||||||
|
if session_data:
|
||||||
|
unified_id = session_data.get("unified_id")
|
||||||
|
contact_id = session_data.get("contact_id")
|
||||||
|
phone = session_data.get("phone")
|
||||||
|
if chat_id is None:
|
||||||
|
chat_id = session_data.get("chat_id")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("Ошибка чтения сессии по channel: %s", e)
|
||||||
|
if not unified_id:
|
||||||
|
raise HTTPException(status_code=401, detail="Сессия недействительна или истекла")
|
||||||
|
|
||||||
|
if not unified_id and session_token:
|
||||||
|
try:
|
||||||
|
from app.api.session import SessionVerifyRequest, verify_session
|
||||||
|
verify_res = await verify_session(SessionVerifyRequest(session_token=session_token))
|
||||||
|
if getattr(verify_res, "valid", False):
|
||||||
|
unified_id = getattr(verify_res, "unified_id", None)
|
||||||
|
contact_id = getattr(verify_res, "contact_id", None)
|
||||||
|
phone = getattr(verify_res, "phone", None)
|
||||||
|
if chat_id is None:
|
||||||
|
chat_id = getattr(verify_res, "chat_id", None)
|
||||||
|
if not unified_id:
|
||||||
|
raise HTTPException(status_code=401, detail="Сессия недействительна или истекла")
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("Ошибка верификации сессии для профиля: %s", e)
|
||||||
|
raise HTTPException(status_code=401, detail="Сессия недействительна")
|
||||||
|
|
||||||
|
if not unified_id:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail="Укажите session_token, (channel + channel_user_id) или unified_id",
|
||||||
|
)
|
||||||
|
return unified_id, contact_id, phone, chat_id
|
||||||
|
|
||||||
|
|
||||||
class ProfileContactRequest(BaseModel):
|
class ProfileContactRequest(BaseModel):
|
||||||
"""Запрос контактных данных: session_token, (channel + channel_user_id) или unified_id."""
|
"""Запрос контактных данных: session_token, (channel + channel_user_id) или unified_id."""
|
||||||
session_token: Optional[str] = Field(None, description="Токен сессии (unified_id подставится из Redis)")
|
session_token: Optional[str] = Field(None, description="Токен сессии (unified_id подставится из Redis)")
|
||||||
@@ -58,6 +111,23 @@ class ProfileContactRequest(BaseModel):
|
|||||||
chat_id: Optional[str] = Field(None, description="Telegram user id или Max user id (для передачи в n8n)")
|
chat_id: Optional[str] = Field(None, description="Telegram user id или Max user id (для передачи в n8n)")
|
||||||
|
|
||||||
|
|
||||||
|
class ProfileContactUpdateRequest(BaseModel):
|
||||||
|
"""Обновление контакта: session_token обязателен; остальные поля — редактируемые (все обязательны на фронте, кроме phone)."""
|
||||||
|
session_token: str = Field(..., description="Токен сессии")
|
||||||
|
entry_channel: Optional[str] = Field("web", description="Канал входа: telegram | max | web")
|
||||||
|
last_name: str = Field("", description="Фамилия")
|
||||||
|
first_name: str = Field("", description="Имя")
|
||||||
|
middle_name: str = Field("", description="Отчество")
|
||||||
|
birth_date: str = Field("", description="Дата рождения")
|
||||||
|
birth_place: str = Field("", description="Место рождения")
|
||||||
|
inn: str = Field("", description="ИНН")
|
||||||
|
email: str = Field("", description="Email")
|
||||||
|
registration_address: str = Field("", description="Адрес регистрации")
|
||||||
|
mailing_address: str = Field("", description="Почтовый адрес")
|
||||||
|
bank_for_compensation: str = Field("", description="Банк для возмещения")
|
||||||
|
phone: Optional[str] = Field(None, description="Телефон (read-only на фронте, передаётся в n8n)")
|
||||||
|
|
||||||
|
|
||||||
@router.get("/contact")
|
@router.get("/contact")
|
||||||
async def get_profile_contact(
|
async def get_profile_contact(
|
||||||
session_token: Optional[str] = Query(None, description="Токен сессии"),
|
session_token: Optional[str] = Query(None, description="Токен сессии"),
|
||||||
@@ -109,51 +179,15 @@ async def _fetch_contact(
|
|||||||
detail="N8N_CONTACT_WEBHOOK не настроен",
|
detail="N8N_CONTACT_WEBHOOK не настроен",
|
||||||
)
|
)
|
||||||
|
|
||||||
contact_id: Optional[str] = None
|
unified_id, contact_id, phone, chat_id = await _resolve_profile_identity(
|
||||||
phone: Optional[str] = None
|
session_token=session_token,
|
||||||
|
unified_id=unified_id,
|
||||||
|
channel=channel,
|
||||||
|
channel_user_id=channel_user_id,
|
||||||
|
entry_channel=entry_channel,
|
||||||
|
chat_id=chat_id,
|
||||||
|
)
|
||||||
|
|
||||||
# Сессия по channel + channel_user_id (универсальный auth пишет в Redis по этому ключу)
|
|
||||||
if not unified_id and channel and channel_user_id:
|
|
||||||
try:
|
|
||||||
from app.api.session import get_session_by_channel_user
|
|
||||||
session_data = await get_session_by_channel_user(channel.strip(), str(channel_user_id).strip())
|
|
||||||
if session_data:
|
|
||||||
unified_id = session_data.get("unified_id")
|
|
||||||
contact_id = session_data.get("contact_id")
|
|
||||||
phone = session_data.get("phone")
|
|
||||||
if chat_id is None:
|
|
||||||
chat_id = session_data.get("chat_id")
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning("Ошибка чтения сессии по channel: %s", e)
|
|
||||||
if not unified_id:
|
|
||||||
raise HTTPException(status_code=401, detail="Сессия недействительна или истекла")
|
|
||||||
|
|
||||||
# Сессия по session_token
|
|
||||||
if not unified_id and session_token:
|
|
||||||
try:
|
|
||||||
from app.api.session import SessionVerifyRequest, verify_session
|
|
||||||
verify_res = await verify_session(SessionVerifyRequest(session_token=session_token))
|
|
||||||
if getattr(verify_res, "valid", False):
|
|
||||||
unified_id = getattr(verify_res, "unified_id", None)
|
|
||||||
contact_id = getattr(verify_res, "contact_id", None)
|
|
||||||
phone = getattr(verify_res, "phone", None)
|
|
||||||
if chat_id is None:
|
|
||||||
chat_id = getattr(verify_res, "chat_id", None)
|
|
||||||
if not unified_id:
|
|
||||||
raise HTTPException(status_code=401, detail="Сессия недействительна или истекла")
|
|
||||||
except HTTPException:
|
|
||||||
raise
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning("Ошибка верификации сессии для профиля: %s", e)
|
|
||||||
raise HTTPException(status_code=401, detail="Сессия недействительна")
|
|
||||||
|
|
||||||
if not unified_id:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=400,
|
|
||||||
detail="Укажите session_token, (channel + channel_user_id) или unified_id",
|
|
||||||
)
|
|
||||||
|
|
||||||
# В хук уходит всё, что нужно для обработки в n8n: канал, unified_id, chat_id, contact_id, phone, session_token
|
|
||||||
payload: dict = {
|
payload: dict = {
|
||||||
"unified_id": unified_id,
|
"unified_id": unified_id,
|
||||||
"entry_channel": (entry_channel or "web").strip() or "web",
|
"entry_channel": (entry_channel or "web").strip() or "web",
|
||||||
@@ -190,7 +224,6 @@ async def _fetch_contact(
|
|||||||
except Exception:
|
except Exception:
|
||||||
data = response.text or ""
|
data = response.text or ""
|
||||||
|
|
||||||
# Нормализация ответа n8n в единый формат { "items": [...] }
|
|
||||||
if isinstance(data, list):
|
if isinstance(data, list):
|
||||||
return {"items": data if data else []}
|
return {"items": data if data else []}
|
||||||
if isinstance(data, dict):
|
if isinstance(data, dict):
|
||||||
@@ -201,8 +234,78 @@ async def _fetch_contact(
|
|||||||
return {"items": c if isinstance(c, list) else [c] if c else []}
|
return {"items": c if isinstance(c, list) else [c] if c else []}
|
||||||
if "data" in data and isinstance(data["data"], list):
|
if "data" in data and isinstance(data["data"], list):
|
||||||
return {"items": data["data"]}
|
return {"items": data["data"]}
|
||||||
# Один объект-контакт без обёртки (если есть хоть одно поле контакта — считаем контактом)
|
|
||||||
if data and isinstance(data, dict):
|
if data and isinstance(data, dict):
|
||||||
return {"items": [data]}
|
return {"items": [data]}
|
||||||
return {"items": []}
|
return {"items": []}
|
||||||
return {"items": []}
|
return {"items": []}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/contact/update")
|
||||||
|
async def post_profile_contact_update(body: ProfileContactUpdateRequest):
|
||||||
|
"""
|
||||||
|
Обновить контакт в CRM через N8N_PROFILE_UPDATE_WEBHOOK.
|
||||||
|
Вызывается с фронта при verification="0". Сессия проверяется по session_token.
|
||||||
|
"""
|
||||||
|
webhook_url = (getattr(settings, "n8n_profile_update_webhook", None) or "").strip()
|
||||||
|
if not webhook_url:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=503,
|
||||||
|
detail="N8N_PROFILE_UPDATE_WEBHOOK не настроен",
|
||||||
|
)
|
||||||
|
|
||||||
|
unified_id, contact_id, phone, chat_id = await _resolve_profile_identity(
|
||||||
|
session_token=body.session_token,
|
||||||
|
entry_channel=body.entry_channel,
|
||||||
|
chat_id=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
payload: dict = {
|
||||||
|
"unified_id": unified_id,
|
||||||
|
"entry_channel": (body.entry_channel or "web").strip() or "web",
|
||||||
|
"session_token": body.session_token,
|
||||||
|
"last_name": (body.last_name or "").strip(),
|
||||||
|
"first_name": (body.first_name or "").strip(),
|
||||||
|
"middle_name": (body.middle_name or "").strip(),
|
||||||
|
"birth_date": (body.birth_date or "").strip(),
|
||||||
|
"birth_place": (body.birth_place or "").strip(),
|
||||||
|
"inn": (body.inn or "").strip(),
|
||||||
|
"email": (body.email or "").strip(),
|
||||||
|
"registration_address": (body.registration_address or "").strip(),
|
||||||
|
"mailing_address": (body.mailing_address or "").strip(),
|
||||||
|
"bank_for_compensation": (body.bank_for_compensation or "").strip(),
|
||||||
|
}
|
||||||
|
if contact_id is not None:
|
||||||
|
payload["contact_id"] = contact_id
|
||||||
|
if body.phone is not None and str(body.phone).strip():
|
||||||
|
payload["phone"] = str(body.phone).strip()
|
||||||
|
elif phone is not None:
|
||||||
|
payload["phone"] = phone
|
||||||
|
if chat_id is not None and str(chat_id).strip():
|
||||||
|
payload["chat_id"] = str(chat_id).strip()
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with httpx.AsyncClient(timeout=15.0) as client:
|
||||||
|
response = await client.post(
|
||||||
|
webhook_url,
|
||||||
|
json=payload,
|
||||||
|
headers={"Content-Type": "application/json"},
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception("Ошибка вызова N8N_PROFILE_UPDATE_WEBHOOK: %s", e)
|
||||||
|
raise HTTPException(status_code=502, detail="Не удалось сохранить профиль, попробуйте позже")
|
||||||
|
|
||||||
|
if response.status_code < 200 or response.status_code >= 300:
|
||||||
|
logger.warning("N8N profile update webhook вернул %s: %s", response.status_code, response.text[:500])
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=502,
|
||||||
|
detail="Не удалось сохранить профиль, попробуйте позже",
|
||||||
|
)
|
||||||
|
|
||||||
|
result: dict = {"success": True}
|
||||||
|
try:
|
||||||
|
data = response.json()
|
||||||
|
if isinstance(data, dict) and data:
|
||||||
|
result.update(data)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return result
|
||||||
|
|||||||
@@ -204,6 +204,7 @@ class Settings(BaseSettings):
|
|||||||
|
|
||||||
# Контактные данные из CRM для раздела «Профиль» (массив или пусто)
|
# Контактные данные из CRM для раздела «Профиль» (массив или пусто)
|
||||||
n8n_contact_webhook: str = "" # N8N_CONTACT_WEBHOOK в .env
|
n8n_contact_webhook: str = "" # N8N_CONTACT_WEBHOOK в .env
|
||||||
|
n8n_profile_update_webhook: str = "" # N8N_PROFILE_UPDATE_WEBHOOK в .env — обновление профиля (verification=0)
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
# TELEGRAM BOT
|
# TELEGRAM BOT
|
||||||
|
|||||||
@@ -1,23 +1,23 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { Button, Card, Descriptions, Spin, Typography } from 'antd';
|
import { Button, Card, Descriptions, Form, Input, Spin, Typography, message } from 'antd';
|
||||||
import { User } from 'lucide-react';
|
import { User } from 'lucide-react';
|
||||||
import './Profile.css';
|
import './Profile.css';
|
||||||
|
|
||||||
const { Title, Text } = Typography;
|
const { Title, Text } = Typography;
|
||||||
|
|
||||||
/** Поля профиля из CRM (поддержка snake_case и camelCase) */
|
/** Поля профиля из CRM (поддержка snake_case и camelCase). Все кроме phone редактируемые при verification="0". */
|
||||||
const PROFILE_FIELDS: Array<{ key: string; keys: string[]; label: string }> = [
|
const PROFILE_FIELDS: Array<{ key: string; keys: string[]; label: string; editable: boolean }> = [
|
||||||
{ key: 'last_name', keys: ['last_name', 'lastName'], label: 'Фамилия' },
|
{ key: 'last_name', keys: ['last_name', 'lastName'], label: 'Фамилия', editable: true },
|
||||||
{ key: 'first_name', keys: ['first_name', 'firstName'], label: 'Имя' },
|
{ key: 'first_name', keys: ['first_name', 'firstName'], label: 'Имя', editable: true },
|
||||||
{ key: 'middle_name', keys: ['middle_name', 'middleName', 'otchestvo'], label: 'Отчество' },
|
{ key: 'middle_name', keys: ['middle_name', 'middleName', 'otchestvo'], label: 'Отчество', editable: true },
|
||||||
{ key: 'birth_date', keys: ['birth_date', 'birthDate', 'birthday'], label: 'Дата рождения' },
|
{ key: 'birth_date', keys: ['birth_date', 'birthDate', 'birthday'], label: 'Дата рождения', editable: true },
|
||||||
{ key: 'birth_place', keys: ['birth_place', 'birthPlace'], label: 'Место рождения' },
|
{ key: 'birth_place', keys: ['birth_place', 'birthPlace'], label: 'Место рождения', editable: true },
|
||||||
{ key: 'inn', keys: ['inn'], label: 'ИНН' },
|
{ key: 'inn', keys: ['inn'], label: 'ИНН', editable: true },
|
||||||
{ key: 'email', keys: ['email'], label: 'Электронная почта' },
|
{ key: 'email', keys: ['email'], label: 'Электронная почта', editable: true },
|
||||||
{ key: 'registration_address', keys: ['registration_address', 'address', 'mailingstreet'], label: 'Адрес регистрации' },
|
{ key: 'registration_address', keys: ['registration_address', 'address', 'mailingstreet'], label: 'Адрес регистрации', editable: true },
|
||||||
{ key: 'mailing_address', keys: ['mailing_address', 'postal_address'], label: 'Почтовый адрес' },
|
{ key: 'mailing_address', keys: ['mailing_address', 'postal_address'], label: 'Почтовый адрес', editable: true },
|
||||||
{ key: 'bank_for_compensation', keys: ['bank_for_compensation', 'bank'], label: 'Банк для получения возмещения' },
|
{ key: 'bank_for_compensation', keys: ['bank_for_compensation', 'bank'], label: 'Банк для получения возмещения', editable: true },
|
||||||
{ key: 'phone', keys: ['phone', 'mobile', 'mobile_phone'], label: 'Мобильный телефон' },
|
{ key: 'phone', keys: ['phone', 'mobile', 'mobile_phone'], label: 'Мобильный телефон', editable: false },
|
||||||
];
|
];
|
||||||
|
|
||||||
function getValue(obj: Record<string, unknown>, keys: string[]): string {
|
function getValue(obj: Record<string, unknown>, keys: string[]): string {
|
||||||
@@ -28,14 +28,22 @@ function getValue(obj: Record<string, unknown>, keys: string[]): string {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** verification === "0" — профиль можно редактировать (ответ n8n). Иначе — только просмотр. */
|
||||||
|
function canEditProfile(contact: Record<string, unknown>): boolean {
|
||||||
|
const v = contact?.verification ?? contact?.Verification;
|
||||||
|
return v === '0' || v === 0;
|
||||||
|
}
|
||||||
|
|
||||||
interface ProfileProps {
|
interface ProfileProps {
|
||||||
onNavigate?: (path: string) => void;
|
onNavigate?: (path: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Profile({ onNavigate }: ProfileProps) {
|
export default function Profile({ onNavigate }: ProfileProps) {
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [saving, setSaving] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [contact, setContact] = useState<Record<string, unknown> | null>(null);
|
const [contact, setContact] = useState<Record<string, unknown> | null>(null);
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
@@ -81,6 +89,13 @@ export default function Profile({ onNavigate }: ProfileProps) {
|
|||||||
? (items[0] as Record<string, unknown>)
|
? (items[0] as Record<string, unknown>)
|
||||||
: null;
|
: null;
|
||||||
setContact(first);
|
setContact(first);
|
||||||
|
if (first && canEditProfile(first)) {
|
||||||
|
const initial: Record<string, string> = {};
|
||||||
|
PROFILE_FIELDS.forEach(({ key, keys }) => {
|
||||||
|
initial[key] = getValue(first, keys) || '';
|
||||||
|
});
|
||||||
|
form.setFieldsValue(initial);
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
if (!cancelled) setError(e?.message || 'Не удалось загрузить данные');
|
if (!cancelled) setError(e?.message || 'Не удалось загрузить данные');
|
||||||
@@ -89,7 +104,61 @@ export default function Profile({ onNavigate }: ProfileProps) {
|
|||||||
if (!cancelled) setLoading(false);
|
if (!cancelled) setLoading(false);
|
||||||
});
|
});
|
||||||
return () => { cancelled = true; };
|
return () => { cancelled = true; };
|
||||||
}, [onNavigate]);
|
}, [onNavigate, form]);
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
if (!contact || !canEditProfile(contact)) return;
|
||||||
|
const token = sessionStorage.getItem('session_token') || localStorage.getItem('session_token');
|
||||||
|
if (!token) {
|
||||||
|
message.error('Сессия истекла');
|
||||||
|
onNavigate?.('/hello');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const entryChannel =
|
||||||
|
(typeof window !== 'undefined' && (window as any).Telegram?.WebApp?.initData) ? 'telegram'
|
||||||
|
: (typeof window !== 'undefined' && (window as any).WebApp?.initData) ? 'max'
|
||||||
|
: 'web';
|
||||||
|
let values: Record<string, string>;
|
||||||
|
try {
|
||||||
|
values = await form.validateFields();
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setSaving(true);
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/v1/profile/contact/update', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
session_token: token,
|
||||||
|
entry_channel: entryChannel,
|
||||||
|
last_name: values.last_name ?? '',
|
||||||
|
first_name: values.first_name ?? '',
|
||||||
|
middle_name: values.middle_name ?? '',
|
||||||
|
birth_date: values.birth_date ?? '',
|
||||||
|
birth_place: values.birth_place ?? '',
|
||||||
|
inn: values.inn ?? '',
|
||||||
|
email: values.email ?? '',
|
||||||
|
registration_address: values.registration_address ?? '',
|
||||||
|
mailing_address: values.mailing_address ?? '',
|
||||||
|
bank_for_compensation: values.bank_for_compensation ?? '',
|
||||||
|
phone: getValue(contact, ['phone', 'mobile', 'mobile_phone']) || undefined,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const data = await res.json().catch(() => ({}));
|
||||||
|
if (!res.ok) {
|
||||||
|
const detail = typeof data?.detail === 'string' ? data.detail : data?.detail?.[0]?.msg || 'Не удалось сохранить профиль';
|
||||||
|
message.error(detail);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
message.success('Профиль сохранён');
|
||||||
|
setContact({ ...contact, ...values });
|
||||||
|
} catch (e) {
|
||||||
|
message.error('Не удалось сохранить профиль, попробуйте позже');
|
||||||
|
} finally {
|
||||||
|
setSaving(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
@@ -130,6 +199,38 @@ export default function Profile({ onNavigate }: ProfileProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const canEdit = canEditProfile(contact);
|
||||||
|
|
||||||
|
if (canEdit) {
|
||||||
|
return (
|
||||||
|
<div className="profile-page">
|
||||||
|
<Card
|
||||||
|
className="profile-card"
|
||||||
|
title={<><User size={20} style={{ marginRight: 8, verticalAlign: 'middle' }} /> Профиль</>}
|
||||||
|
extra={onNavigate ? <Button type="link" onClick={() => onNavigate('/')}>Домой</Button> : null}
|
||||||
|
>
|
||||||
|
<Form form={form} layout="vertical" onFinish={handleSave}>
|
||||||
|
{PROFILE_FIELDS.map(({ key, keys, label, editable }) => (
|
||||||
|
<Form.Item
|
||||||
|
key={key}
|
||||||
|
name={key}
|
||||||
|
label={label}
|
||||||
|
rules={editable ? [{ required: true, message: 'Обязательное поле' }] : undefined}
|
||||||
|
>
|
||||||
|
<Input disabled={!editable} placeholder={editable ? undefined : '—'} />
|
||||||
|
</Form.Item>
|
||||||
|
))}
|
||||||
|
<Form.Item>
|
||||||
|
<Button type="primary" htmlType="submit" loading={saving}>
|
||||||
|
Сохранить изменения
|
||||||
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const items = PROFILE_FIELDS.map(({ keys, label }) => ({
|
const items = PROFILE_FIELDS.map(({ keys, label }) => ({
|
||||||
key: keys[0],
|
key: keys[0],
|
||||||
label,
|
label,
|
||||||
|
|||||||
Reference in New Issue
Block a user