Add logistics UI actions and demo login buttons
- Fix bug in transfer-to-logistics: body.note was incorrectly used for delivery_agreement_status - Add quick actions section in OrderDetailPanel for status changes (visible to role) - Add driver assignment buttons for logistician/admin - Add demo login buttons for all roles (works with real Supabase config) - Add loginAsDemoUser function to AuthContext for quick demo login
This commit is contained in:
parent
219670583b
commit
f2230f3277
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"mcpServers": {
|
||||
"n8n-mcp": {
|
||||
"type": "http",
|
||||
"url": "https://n8n.supersamsev.ru/mcp-server/http",
|
||||
"headers": {
|
||||
"Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMjMyZTZiZC00MzlhLTQwYjMtODgzMi05MzAxZTM3ZjRlMzgiLCJpc3MiOiJuOG4iLCJhdWQiOiJtY3Atc2VydmVyLWFwaSIsImp0aSI6ImE1OTU3YjgzLWExYTUtNDg4OC1iMzUyLWQxM2JhZDEyZDg0NCIsImlhdCI6MTc3Nzg5ODEyN30.wT6z8T9V8gRsS1uUg3HYEFFoS223krEC0KjQPBxlfis"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"servers": {
|
||||
"n8n-mcp": {
|
||||
"type": "http",
|
||||
"url": "https://n8n.supersamsev.ru/mcp-server/http",
|
||||
"headers": {
|
||||
"Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMjMyZTZiZC00MzlhLTQwYjMtODgzMi05MzAxZTM3ZjRlMzgiLCJpc3MiOiJuOG4iLCJhdWQiOiJtY3Atc2VydmVyLWFwaSIsImp0aSI6ImE1OTU3YjgzLWExYTUtNDg4OC1iMzUyLWQxM2JhZDEyZDg0NCIsImlhdCI6MTc3Nzg5ODEyN30.wT6z8T9V8gRsS1uUg3HYEFFoS223krEC0KjQPBxlfis"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,427 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Скрипт для анонимизации персональных данных в XML-файле.
|
||||
Заменяет ФИО и телефоны на сгенерированные, сохраняя соответствие дублей.
|
||||
|
||||
Использование:
|
||||
python3 anonymize_xml.py <входной_xml> [выходной_xml]
|
||||
"""
|
||||
|
||||
import sys
|
||||
import random
|
||||
import xml.etree.ElementTree as ET
|
||||
from pathlib import Path
|
||||
|
||||
# --- Генераторы фейковых данных ---
|
||||
|
||||
MALE_SURNAMES = [
|
||||
"Иванов",
|
||||
"Смирнов",
|
||||
"Кузнецов",
|
||||
"Попов",
|
||||
"Васильев",
|
||||
"Петров",
|
||||
"Соколов",
|
||||
"Михайлов",
|
||||
"Новиков",
|
||||
"Федоров",
|
||||
"Морозов",
|
||||
"Волков",
|
||||
"Алексеев",
|
||||
"Лебедев",
|
||||
"Семенов",
|
||||
"Егоров",
|
||||
"Павлов",
|
||||
"Козлов",
|
||||
"Степанов",
|
||||
"Николаев",
|
||||
"Орлов",
|
||||
"Андреев",
|
||||
"Макаров",
|
||||
"Зайцев",
|
||||
"Соловьев",
|
||||
"Борисов",
|
||||
"Яковлев",
|
||||
"Григорьев",
|
||||
"Романов",
|
||||
"Воробьев",
|
||||
"Антонов",
|
||||
"Захаров",
|
||||
"Максимов",
|
||||
"Фролов",
|
||||
"Дмитриев",
|
||||
"Калинин",
|
||||
"Беляев",
|
||||
"Гусев",
|
||||
"Назаров",
|
||||
"Карпов",
|
||||
"Афанасьев",
|
||||
"Тихонов",
|
||||
"Марков",
|
||||
"Комаров",
|
||||
"Шестаков",
|
||||
"Артемьев",
|
||||
"Кукушкин",
|
||||
"Плотников",
|
||||
"Дорофеев",
|
||||
]
|
||||
|
||||
FEMALE_SURNAMES = [
|
||||
"Иванова",
|
||||
"Смирнова",
|
||||
"Кузнецова",
|
||||
"Попова",
|
||||
"Васильева",
|
||||
"Петрова",
|
||||
"Соколова",
|
||||
"Михайлова",
|
||||
"Новикова",
|
||||
"Федорова",
|
||||
"Морозова",
|
||||
"Волкова",
|
||||
"Алексеева",
|
||||
"Лебедева",
|
||||
"Семенова",
|
||||
"Егорова",
|
||||
"Павлова",
|
||||
"Козлова",
|
||||
"Степанова",
|
||||
"Николаева",
|
||||
"Орлова",
|
||||
"Андреева",
|
||||
"Макарова",
|
||||
"Зайцева",
|
||||
"Соловьева",
|
||||
"Борисова",
|
||||
"Яковлева",
|
||||
"Григорьева",
|
||||
"Романова",
|
||||
"Воробьева",
|
||||
"Атонова",
|
||||
"Захарова",
|
||||
"Максимова",
|
||||
"Фролова",
|
||||
"Дмитриева",
|
||||
"Калинина",
|
||||
"Беляева",
|
||||
"Гусева",
|
||||
"Назарова",
|
||||
"Карпова",
|
||||
"Афанасьева",
|
||||
"Тихонова",
|
||||
"Маркова",
|
||||
"Комарова",
|
||||
"Шестакова",
|
||||
"Артемьева",
|
||||
"Кукушкина",
|
||||
"Плотникова",
|
||||
"Дорофеева",
|
||||
]
|
||||
|
||||
MALE_NAMES = [
|
||||
"Александр",
|
||||
"Дмитрий",
|
||||
"Максим",
|
||||
"Сергей",
|
||||
"Андрей",
|
||||
"Алексей",
|
||||
"Артем",
|
||||
"Илья",
|
||||
"Кирилл",
|
||||
"Михаил",
|
||||
"Никита",
|
||||
"Матвей",
|
||||
"Роман",
|
||||
"Егор",
|
||||
"Арсений",
|
||||
"Иван",
|
||||
"Денис",
|
||||
"Евгений",
|
||||
"Даниил",
|
||||
"Тимофей",
|
||||
"Владимир",
|
||||
"Павел",
|
||||
"Руслан",
|
||||
"Марк",
|
||||
"Константин",
|
||||
"Тимур",
|
||||
"Олег",
|
||||
"Ярослав",
|
||||
"Степан",
|
||||
"Глеб",
|
||||
"Николай",
|
||||
"Петр",
|
||||
"Виктор",
|
||||
"Семен",
|
||||
"Леонид",
|
||||
"Григорий",
|
||||
"Богдан",
|
||||
"Захар",
|
||||
"Тарас",
|
||||
"Федор",
|
||||
]
|
||||
|
||||
FEMALE_NAMES = [
|
||||
"Анна",
|
||||
"Мария",
|
||||
"Елена",
|
||||
"Алиса",
|
||||
"Виктория",
|
||||
"Полина",
|
||||
"Александра",
|
||||
"Елизавета",
|
||||
"Дарья",
|
||||
"Анастасия",
|
||||
"Ксения",
|
||||
"Валерия",
|
||||
"Варвара",
|
||||
"Вероника",
|
||||
"София",
|
||||
"Арина",
|
||||
"Марина",
|
||||
"Юлия",
|
||||
"Татьяна",
|
||||
"Наталья",
|
||||
"Ольга",
|
||||
"Светлана",
|
||||
"Ирина",
|
||||
"Екатерина",
|
||||
"Оксана",
|
||||
"Алина",
|
||||
"Кристина",
|
||||
"Людмила",
|
||||
"Галина",
|
||||
"Надежда",
|
||||
"Любовь",
|
||||
"Маргарита",
|
||||
"Софья",
|
||||
"Яна",
|
||||
"Диана",
|
||||
"Алла",
|
||||
"Инна",
|
||||
"Эмилия",
|
||||
"Лариса",
|
||||
"Злата",
|
||||
]
|
||||
|
||||
MALE_PATRONYMICS = [
|
||||
"Александрович",
|
||||
"Дмитриевич",
|
||||
"Максимович",
|
||||
"Сергеевич",
|
||||
"Андреевич",
|
||||
"Алексеевич",
|
||||
"Артемович",
|
||||
"Ильич",
|
||||
"Кириллович",
|
||||
"Михайлович",
|
||||
"Никитич",
|
||||
"Матвеевич",
|
||||
"Романович",
|
||||
"Егорович",
|
||||
"Арсеньевич",
|
||||
"Иванович",
|
||||
"Денисович",
|
||||
"Евгеньевич",
|
||||
"Даниилович",
|
||||
"Тимофеевич",
|
||||
"Владимирович",
|
||||
"Павлович",
|
||||
"Константинович",
|
||||
"Тимурович",
|
||||
"Олегович",
|
||||
"Ярославович",
|
||||
"Степанович",
|
||||
"Глебович",
|
||||
"Николаевич",
|
||||
"Петрович",
|
||||
"Викторович",
|
||||
"Семенович",
|
||||
"Леонидович",
|
||||
"Григорьевич",
|
||||
"Богданович",
|
||||
"Захарович",
|
||||
"Тарасович",
|
||||
"Федорович",
|
||||
]
|
||||
|
||||
FEMALE_PATRONYMICS = [
|
||||
"Александровна",
|
||||
"Дмитриевна",
|
||||
"Максимовна",
|
||||
"Сергеевна",
|
||||
"Андреевна",
|
||||
"Алексеевна",
|
||||
"Артемовна",
|
||||
"Ильинична",
|
||||
"Кирилловна",
|
||||
"Михайловна",
|
||||
"Никитична",
|
||||
"Матвеевна",
|
||||
"Романовна",
|
||||
"Егоровна",
|
||||
"Арсеньевна",
|
||||
"Ивановна",
|
||||
"Денисовна",
|
||||
"Евгеньевна",
|
||||
"Данииловна",
|
||||
"Тимофеевна",
|
||||
"Владимировна",
|
||||
"Павловна",
|
||||
"Константиновна",
|
||||
"Тимуровна",
|
||||
"Олеговна",
|
||||
"Ярославовна",
|
||||
"Степановна",
|
||||
"Глебовна",
|
||||
"Николаевна",
|
||||
"Петровна",
|
||||
"Викторовна",
|
||||
"Семеновна",
|
||||
"Леонидовна",
|
||||
"Григорьевна",
|
||||
"Богдановна",
|
||||
"Захаровна",
|
||||
"Тарасовна",
|
||||
"Федоровна",
|
||||
]
|
||||
|
||||
|
||||
def generate_fake_fio():
|
||||
"""Генерирует случайное русское ФИО с инициалами или полным отчеством."""
|
||||
if random.choice([True, False]):
|
||||
# Полное ФИО
|
||||
surname = random.choice(MALE_SURNAMES + FEMALE_SURNAMES)
|
||||
# Попытка согласовать род фамилии и имя/отчество
|
||||
if surname.endswith("а") and not surname.endswith(
|
||||
("ина", "ова", "ева", "ына", "ая")
|
||||
):
|
||||
# Может быть мужская фамилия типа Буря, но проще — выбираем случайно
|
||||
is_female = random.choice([True, False])
|
||||
else:
|
||||
is_female = surname.endswith(
|
||||
("ина", "ова", "ева", "ына", "ая", "ска", "цка", "ёва")
|
||||
)
|
||||
|
||||
if is_female:
|
||||
name = random.choice(FEMALE_NAMES)
|
||||
patronymic = random.choice(FEMALE_PATRONYMICS)
|
||||
else:
|
||||
name = random.choice(MALE_NAMES)
|
||||
patronymic = random.choice(MALE_PATRONYMICS)
|
||||
return f"{surname} {name} {patronymic}"
|
||||
else:
|
||||
# Фамилия с инициалами
|
||||
surname = random.choice(MALE_SURNAMES + FEMALE_SURNAMES)
|
||||
is_female = surname.endswith(
|
||||
("ина", "ова", "ева", "ына", "ая", "ска", "цка", "ёва")
|
||||
)
|
||||
if is_female:
|
||||
name_initial = random.choice(FEMALE_NAMES)[0]
|
||||
patronymic_initial = random.choice(FEMALE_PATRONYMICS)[0]
|
||||
else:
|
||||
name_initial = random.choice(MALE_NAMES)[0]
|
||||
patronymic_initial = random.choice(MALE_PATRONYMICS)[0]
|
||||
return f"{surname} {name_initial}.{patronymic_initial}."
|
||||
|
||||
|
||||
def generate_fake_phone():
|
||||
"""Генерирует случайный российский мобильный номер (10 цифр, начинается с 9)."""
|
||||
return f"9{random.randint(100000000, 999999999)}"
|
||||
|
||||
|
||||
def anonymize_xml(input_path: str, output_path: str = None):
|
||||
input_file = Path(input_path)
|
||||
if not input_file.exists():
|
||||
print(f"Ошибка: файл не найден: {input_path}")
|
||||
sys.exit(1)
|
||||
|
||||
# Определяем выходной путь
|
||||
if output_path is None:
|
||||
output_file = input_file.with_suffix(".anonymized.xml")
|
||||
else:
|
||||
output_file = Path(output_path)
|
||||
|
||||
# Читаем исходный файл
|
||||
# Пробуем cp1251 (WINDOWS-1251), если ошибка — utf-8
|
||||
raw_bytes = input_file.read_bytes()
|
||||
try:
|
||||
text = raw_bytes.decode("cp1251")
|
||||
except UnicodeDecodeError:
|
||||
text = raw_bytes.decode("utf-8", errors="replace")
|
||||
|
||||
# Парсим XML
|
||||
try:
|
||||
root = ET.fromstring(text)
|
||||
except ET.ParseError as e:
|
||||
print(f"Ошибка парсинга XML: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
# Собираем уникальные значения
|
||||
fio_map = {}
|
||||
tel_map = {}
|
||||
fio_values = set()
|
||||
tel_values = set()
|
||||
|
||||
for account in root.findall("Account"):
|
||||
fio = account.get("fio")
|
||||
tel = account.get("tel")
|
||||
if fio:
|
||||
fio_values.add(fio)
|
||||
if tel:
|
||||
tel_values.add(tel)
|
||||
|
||||
# Генерируем маппинги
|
||||
random.seed(42) # Для воспроизводимости (можно убрать или изменить)
|
||||
for fio in sorted(fio_values):
|
||||
fio_map[fio] = generate_fake_fio()
|
||||
for tel in sorted(tel_values):
|
||||
tel_map[tel] = generate_fake_phone()
|
||||
|
||||
# Заменяем в дереве
|
||||
for account in root.findall("Account"):
|
||||
original_fio = account.get("fio")
|
||||
original_tel = account.get("tel")
|
||||
if original_fio and original_fio in fio_map:
|
||||
account.set("fio", fio_map[original_fio])
|
||||
if original_tel and original_tel in tel_map:
|
||||
account.set("tel", tel_map[original_tel])
|
||||
|
||||
# Сохраняем результат
|
||||
# Используем собственную сериализацию для сохранения формата
|
||||
# ElementTree.write может слегка изменять формат, поэтому используем tostring
|
||||
tree = ET.ElementTree(root)
|
||||
# Добавляем XML declaration
|
||||
xml_bytes = ET.tostring(root, encoding="unicode")
|
||||
result = f'<?xml version="1.0" encoding="UTF-8"?>\n{xml_bytes}'
|
||||
|
||||
output_file.write_text(result, encoding="utf-8")
|
||||
print(f"Готово! Результат сохранён в: {output_file.resolve()}")
|
||||
print(f" Уникальных ФИО заменено: {len(fio_map)}")
|
||||
print(f" Уникальных телефонов заменено: {len(tel_map)}")
|
||||
|
||||
# Печатаем часть маппинга для информации
|
||||
print("\nПримеры замен (первые 10):")
|
||||
count = 0
|
||||
for k, v in fio_map.items():
|
||||
if count >= 10:
|
||||
break
|
||||
tel_k = list(tel_map.keys())[count] if count < len(tel_map) else None
|
||||
tel_v = tel_map[tel_k] if tel_k else ""
|
||||
print(f" ФИО: '{k}' -> '{v}'")
|
||||
if tel_k:
|
||||
print(f" Тел: '{tel_k}' -> '{tel_v}'")
|
||||
count += 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 2:
|
||||
script_name = Path(sys.argv[0]).name
|
||||
print(f"Использование: python3 {script_name} <входной_xml> [выходной_xml]")
|
||||
print(f"Пример: python3 {script_name} data.xml data_anon.xml")
|
||||
sys.exit(1)
|
||||
|
||||
input_path = sys.argv[1]
|
||||
output_path = sys.argv[2] if len(sys.argv) > 2 else None
|
||||
anonymize_xml(input_path, output_path)
|
||||
|
|
@ -1,8 +1,9 @@
|
|||
import React from "react";
|
||||
import { getDeliveryAgreementComment, getOrderStatusComment, getStatusTone } from "../../constants/deliveryWorkflow";
|
||||
import { getAvailableTransitionsByRole, getDeliveryAgreementComment, getOrderStatusComment, getStatusTone } from "../../constants/deliveryWorkflow";
|
||||
import { demoUsers } from "../../data/mockAppData";
|
||||
import { formatDateTime } from "../../utils/formatters";
|
||||
import { Badge } from "../UI/Badge";
|
||||
import { Button } from "../UI/Button";
|
||||
import { Panel } from "../UI/Panel";
|
||||
|
||||
const getUsers = (users) => (Array.isArray(users) && users.length ? users : demoUsers);
|
||||
|
|
@ -31,7 +32,7 @@ const splitItem = (item) => {
|
|||
return { name: "Позиция", quantity: "" };
|
||||
};
|
||||
|
||||
export const OrderDetailPanel = ({ order, users }) => {
|
||||
export const OrderDetailPanel = ({ order, users, currentUser, onStatusChange, onAssignDriver }) => {
|
||||
if (!order) {
|
||||
return (
|
||||
<Panel className="flex min-h-[460px] items-center justify-center">
|
||||
|
|
@ -42,6 +43,10 @@ export const OrderDetailPanel = ({ order, users }) => {
|
|||
|
||||
const orderItems = Array.isArray(order.items) ? order.items.map(splitItem) : [];
|
||||
const orderHistory = Array.isArray(order.history) ? order.history : [];
|
||||
const role = currentUser?.role;
|
||||
const availableTransitions = role ? getAvailableTransitionsByRole({ status: order.status, role }) : [];
|
||||
const drivers = (Array.isArray(users) && users.length ? users : demoUsers).filter((u) => u.role === "driver");
|
||||
const canAssignDriver = role === "logistician" || role === "admin";
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
|
|
@ -169,6 +174,45 @@ export const OrderDetailPanel = ({ order, users }) => {
|
|||
</Panel>
|
||||
) : null}
|
||||
|
||||
{availableTransitions.length ? (
|
||||
<Panel className="space-y-4 p-5">
|
||||
<strong>Действия</strong>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{availableTransitions.map((status) => (
|
||||
<Button
|
||||
key={status}
|
||||
variant={status === "Проблема доставки" || status === "Платное хранение" || status === "Отменён" ? "ghost" : "secondary"}
|
||||
onClick={() => onStatusChange?.(status)}
|
||||
>
|
||||
{status}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</Panel>
|
||||
) : null}
|
||||
|
||||
{canAssignDriver ? (
|
||||
<Panel className="space-y-4 p-5">
|
||||
<strong>Назначить водителя</strong>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{drivers.map((driver) => (
|
||||
<Button
|
||||
key={driver.id}
|
||||
variant={order.assignedDriverId === driver.id ? "primary" : "secondary"}
|
||||
onClick={() => onAssignDriver?.({ orderId: order.id, driverId: driver.id, actorName: currentUser.name })}
|
||||
>
|
||||
{driver.name}
|
||||
</Button>
|
||||
))}
|
||||
{order.assignedDriverId ? (
|
||||
<Button variant="ghost" onClick={() => onAssignDriver?.({ orderId: order.id, driverId: null, actorName: currentUser.name })}>
|
||||
Снять водителя
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
</Panel>
|
||||
) : null}
|
||||
|
||||
{orderHistory.length ? (
|
||||
<Panel className="space-y-3 p-5">
|
||||
<strong>История</strong>
|
||||
|
|
|
|||
|
|
@ -209,6 +209,10 @@ export const AuthProvider = ({ children }) => {
|
|||
requestOtp,
|
||||
verifyOtp,
|
||||
signOut,
|
||||
loginAsDemoUser: (demoUser) => {
|
||||
setUser(demoUser);
|
||||
setAuthError("");
|
||||
},
|
||||
};
|
||||
|
||||
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ export const DashboardPage = () => {
|
|||
filters,
|
||||
setFilters,
|
||||
updateStatus,
|
||||
assignDriver,
|
||||
users,
|
||||
isLoading,
|
||||
loadError,
|
||||
|
|
@ -204,7 +205,15 @@ export const DashboardPage = () => {
|
|||
Закрыть
|
||||
</Button>
|
||||
</div>
|
||||
<OrderDetailPanel order={selectedOrder} users={users} />
|
||||
<OrderDetailPanel
|
||||
order={selectedOrder}
|
||||
users={users}
|
||||
currentUser={user}
|
||||
onStatusChange={(nextStatus) =>
|
||||
selectedOrder && updateStatus(selectedOrder.id, nextStatus, user.name)
|
||||
}
|
||||
onAssignDriver={assignDriver}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,15 @@
|
|||
import React from "react";
|
||||
import { Navigate } from "react-router-dom";
|
||||
import { OtpLoginForm } from "../components/auth/OtpLoginForm";
|
||||
import { ROLE_LABELS } from "../constants/roles";
|
||||
import { useAuth } from "../context/AuthContext";
|
||||
import { demoUsers } from "../data/mockAppData";
|
||||
import { Button } from "../components/UI/Button";
|
||||
import { OtpLoginForm } from "../components/auth/OtpLoginForm";
|
||||
|
||||
const DEMO_ROLE_ORDER = ["logistician", "driver", "manager", "admin"];
|
||||
|
||||
export const LoginPage = () => {
|
||||
const { user, isOtpSent, isLoading, authError, requestOtp, verifyOtp } = useAuth();
|
||||
const { user, isOtpSent, isLoading, authError, isDemoMode, requestOtp, verifyOtp, loginAsDemoUser } = useAuth();
|
||||
const [email, setEmail] = React.useState("");
|
||||
const [otp, setOtp] = React.useState("");
|
||||
const [error, setError] = React.useState("");
|
||||
|
|
@ -29,12 +34,37 @@ export const LoginPage = () => {
|
|||
setError("");
|
||||
};
|
||||
|
||||
const handleDemoLogin = (role) => {
|
||||
const demoUser = demoUsers.find((u) => u.role === role);
|
||||
if (!demoUser) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isDemoMode) {
|
||||
setEmail(demoUser.email);
|
||||
requestOtp({ email: demoUser.email, roleHint: role }).then((requestResponse) => {
|
||||
if (!requestResponse.success) {
|
||||
setError(requestResponse.error?.message || "Ошибка демо-входа");
|
||||
return;
|
||||
}
|
||||
|
||||
verifyOtp({ email: demoUser.email, otp: "000000" }).then((verifyResponse) => {
|
||||
if (!verifyResponse.success) {
|
||||
setError(verifyResponse.error?.message || "Ошибка демо-входа");
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
loginAsDemoUser(demoUser);
|
||||
}
|
||||
};
|
||||
|
||||
if (user) {
|
||||
return <Navigate to="/dashboard" replace />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center px-3 py-6 sm:px-4 sm:py-10">
|
||||
<div className="flex min-h-screen flex-col items-center justify-center gap-6 px-3 py-6 sm:px-4 sm:py-10">
|
||||
<OtpLoginForm
|
||||
email={email}
|
||||
setEmail={setEmail}
|
||||
|
|
@ -46,6 +76,29 @@ export const LoginPage = () => {
|
|||
onVerifyOtp={handleVerifyOtp}
|
||||
error={displayError}
|
||||
/>
|
||||
|
||||
{(isDemoMode || import.meta.env.DEV) ? (
|
||||
<div className="w-full max-w-md space-y-3">
|
||||
<p className="text-center text-sm text-[var(--color-text-muted)]">
|
||||
{isDemoMode ? "Демо-режим — войдите под любой ролью" : "Быстрый вход (только для разработки)"}
|
||||
</p>
|
||||
<div className="flex flex-wrap justify-center gap-2">
|
||||
{DEMO_ROLE_ORDER.map((role) => {
|
||||
const demoUser = demoUsers.find((u) => u.role === role);
|
||||
return (
|
||||
<Button
|
||||
key={role}
|
||||
variant="secondary"
|
||||
onClick={() => handleDemoLogin(role)}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{ROLE_LABELS[role]}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
|
@ -79,11 +79,11 @@ Deno.serve(async (request) => {
|
|||
throw invitationError;
|
||||
}
|
||||
|
||||
const { error: updateError } = await supabase
|
||||
const { error: updateError } = await supabase
|
||||
.from("orders")
|
||||
.update({
|
||||
status: orderUpdate?.status,
|
||||
delivery_agreement_status: body.note || orderUpdate?.deliveryAgreementStatus,
|
||||
delivery_agreement_status: orderUpdate?.deliveryAgreementStatus,
|
||||
})
|
||||
.eq("id", body.orderId);
|
||||
|
||||
|
|
@ -91,15 +91,16 @@ Deno.serve(async (request) => {
|
|||
throw updateError;
|
||||
}
|
||||
|
||||
const { error: historyError } = await supabase.from("order_history").insert({
|
||||
const { error: historyError } = await supabase.from("order_history").insert({
|
||||
order_id: body.orderId,
|
||||
action: targetStatus === "Платное хранение" ? "Перевод на платное хранение" : "Передача заказа логисту",
|
||||
old_status: currentOrder.status,
|
||||
new_status: orderUpdate?.status,
|
||||
metadata: {
|
||||
old_delivery_agreement_status: currentOrder.delivery_agreement_status,
|
||||
new_delivery_agreement_status: body.note || orderUpdate?.deliveryAgreementStatus,
|
||||
new_delivery_agreement_status: orderUpdate?.deliveryAgreementStatus,
|
||||
reason: body.reason || null,
|
||||
note: body.note || null,
|
||||
target_status: targetStatus,
|
||||
},
|
||||
});
|
||||
|
|
@ -126,7 +127,7 @@ Deno.serve(async (request) => {
|
|||
ok: true,
|
||||
orderId: body.orderId,
|
||||
status: orderUpdate?.status,
|
||||
deliveryAgreementStatus: body.note || orderUpdate?.deliveryAgreementStatus,
|
||||
deliveryAgreementStatus: orderUpdate?.deliveryAgreementStatus,
|
||||
},
|
||||
200,
|
||||
corsHeaders,
|
||||
|
|
|
|||
Loading…
Reference in New Issue