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 React from "react";
|
||||||
import { getDeliveryAgreementComment, getOrderStatusComment, getStatusTone } from "../../constants/deliveryWorkflow";
|
import { getAvailableTransitionsByRole, getDeliveryAgreementComment, getOrderStatusComment, getStatusTone } from "../../constants/deliveryWorkflow";
|
||||||
import { demoUsers } from "../../data/mockAppData";
|
import { demoUsers } from "../../data/mockAppData";
|
||||||
import { formatDateTime } from "../../utils/formatters";
|
import { formatDateTime } from "../../utils/formatters";
|
||||||
import { Badge } from "../UI/Badge";
|
import { Badge } from "../UI/Badge";
|
||||||
|
import { Button } from "../UI/Button";
|
||||||
import { Panel } from "../UI/Panel";
|
import { Panel } from "../UI/Panel";
|
||||||
|
|
||||||
const getUsers = (users) => (Array.isArray(users) && users.length ? users : demoUsers);
|
const getUsers = (users) => (Array.isArray(users) && users.length ? users : demoUsers);
|
||||||
|
|
@ -31,7 +32,7 @@ const splitItem = (item) => {
|
||||||
return { name: "Позиция", quantity: "" };
|
return { name: "Позиция", quantity: "" };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const OrderDetailPanel = ({ order, users }) => {
|
export const OrderDetailPanel = ({ order, users, currentUser, onStatusChange, onAssignDriver }) => {
|
||||||
if (!order) {
|
if (!order) {
|
||||||
return (
|
return (
|
||||||
<Panel className="flex min-h-[460px] items-center justify-center">
|
<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 orderItems = Array.isArray(order.items) ? order.items.map(splitItem) : [];
|
||||||
const orderHistory = Array.isArray(order.history) ? order.history : [];
|
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 (
|
return (
|
||||||
<div className="space-y-5">
|
<div className="space-y-5">
|
||||||
|
|
@ -169,6 +174,45 @@ export const OrderDetailPanel = ({ order, users }) => {
|
||||||
</Panel>
|
</Panel>
|
||||||
) : null}
|
) : 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 ? (
|
{orderHistory.length ? (
|
||||||
<Panel className="space-y-3 p-5">
|
<Panel className="space-y-3 p-5">
|
||||||
<strong>История</strong>
|
<strong>История</strong>
|
||||||
|
|
|
||||||
|
|
@ -209,6 +209,10 @@ export const AuthProvider = ({ children }) => {
|
||||||
requestOtp,
|
requestOtp,
|
||||||
verifyOtp,
|
verifyOtp,
|
||||||
signOut,
|
signOut,
|
||||||
|
loginAsDemoUser: (demoUser) => {
|
||||||
|
setUser(demoUser);
|
||||||
|
setAuthError("");
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,7 @@ export const DashboardPage = () => {
|
||||||
filters,
|
filters,
|
||||||
setFilters,
|
setFilters,
|
||||||
updateStatus,
|
updateStatus,
|
||||||
|
assignDriver,
|
||||||
users,
|
users,
|
||||||
isLoading,
|
isLoading,
|
||||||
loadError,
|
loadError,
|
||||||
|
|
@ -204,7 +205,15 @@ export const DashboardPage = () => {
|
||||||
Закрыть
|
Закрыть
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,15 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Navigate } from "react-router-dom";
|
import { Navigate } from "react-router-dom";
|
||||||
import { OtpLoginForm } from "../components/auth/OtpLoginForm";
|
import { ROLE_LABELS } from "../constants/roles";
|
||||||
import { useAuth } from "../context/AuthContext";
|
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 = () => {
|
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 [email, setEmail] = React.useState("");
|
||||||
const [otp, setOtp] = React.useState("");
|
const [otp, setOtp] = React.useState("");
|
||||||
const [error, setError] = React.useState("");
|
const [error, setError] = React.useState("");
|
||||||
|
|
@ -29,12 +34,37 @@ export const LoginPage = () => {
|
||||||
setError("");
|
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) {
|
if (user) {
|
||||||
return <Navigate to="/dashboard" replace />;
|
return <Navigate to="/dashboard" replace />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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
|
<OtpLoginForm
|
||||||
email={email}
|
email={email}
|
||||||
setEmail={setEmail}
|
setEmail={setEmail}
|
||||||
|
|
@ -46,6 +76,29 @@ export const LoginPage = () => {
|
||||||
onVerifyOtp={handleVerifyOtp}
|
onVerifyOtp={handleVerifyOtp}
|
||||||
error={displayError}
|
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>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
@ -79,11 +79,11 @@ Deno.serve(async (request) => {
|
||||||
throw invitationError;
|
throw invitationError;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { error: updateError } = await supabase
|
const { error: updateError } = await supabase
|
||||||
.from("orders")
|
.from("orders")
|
||||||
.update({
|
.update({
|
||||||
status: orderUpdate?.status,
|
status: orderUpdate?.status,
|
||||||
delivery_agreement_status: body.note || orderUpdate?.deliveryAgreementStatus,
|
delivery_agreement_status: orderUpdate?.deliveryAgreementStatus,
|
||||||
})
|
})
|
||||||
.eq("id", body.orderId);
|
.eq("id", body.orderId);
|
||||||
|
|
||||||
|
|
@ -91,15 +91,16 @@ Deno.serve(async (request) => {
|
||||||
throw updateError;
|
throw updateError;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { error: historyError } = await supabase.from("order_history").insert({
|
const { error: historyError } = await supabase.from("order_history").insert({
|
||||||
order_id: body.orderId,
|
order_id: body.orderId,
|
||||||
action: targetStatus === "Платное хранение" ? "Перевод на платное хранение" : "Передача заказа логисту",
|
action: targetStatus === "Платное хранение" ? "Перевод на платное хранение" : "Передача заказа логисту",
|
||||||
old_status: currentOrder.status,
|
old_status: currentOrder.status,
|
||||||
new_status: orderUpdate?.status,
|
new_status: orderUpdate?.status,
|
||||||
metadata: {
|
metadata: {
|
||||||
old_delivery_agreement_status: currentOrder.delivery_agreement_status,
|
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,
|
reason: body.reason || null,
|
||||||
|
note: body.note || null,
|
||||||
target_status: targetStatus,
|
target_status: targetStatus,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
@ -126,7 +127,7 @@ Deno.serve(async (request) => {
|
||||||
ok: true,
|
ok: true,
|
||||||
orderId: body.orderId,
|
orderId: body.orderId,
|
||||||
status: orderUpdate?.status,
|
status: orderUpdate?.status,
|
||||||
deliveryAgreementStatus: body.note || orderUpdate?.deliveryAgreementStatus,
|
deliveryAgreementStatus: orderUpdate?.deliveryAgreementStatus,
|
||||||
},
|
},
|
||||||
200,
|
200,
|
||||||
corsHeaders,
|
corsHeaders,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue