@@ -0,0 +1,3 @@ + + + @@ -0,0 +1,3 @@ + + + @@ -23,6 +23,7 @@ --new-ui-btn-danger-bg: #ff23721a; --new-ui-ctrl-f-button-bg: #f9f9fc; --new-ui-ctrl-f-button-border: 1px solid #c4cbd8; + --new-ui-table-cell-text: #97989f; } :root[data-theme='dark'] { @@ -50,6 +51,7 @@ --new-ui-btn-danger-bg: #ff23721a; --new-ui-ctrl-f-button-bg: #242428; --new-ui-ctrl-f-button-border: 1px solid #303035; + --new-ui-table-cell-text: #97989f; } * { @@ -158,10 +160,8 @@ p { /* Изменение цвета активного элемента в выпадающем списке */ .MuiAutocomplete-option.Mui-selected { - background-color: var(--new-ui-main-color); - /* Замените #your-selected-color на цвет активного элемента */ - color: var(--new-ui-main-color); - /* Замените #your-selected-text-color на цвет текста активного элемента */ + background-color: var(--new-ui-main-color); /* Замените #your-selected-color на цвет активного элемента */ + color: var(--new-ui-main-color); /* Замените #your-selected-text-color на цвет текста активного элемента */ } .introjs-tooltiptext { @@ -426,6 +426,26 @@ textarea { transition-duration: 250ms; } +button { + padding: 0; + border: none; + font: inherit; + color: inherit; + background-color: transparent; + text-align: left; + cursor: pointer; +} + +button { + padding: 0; + border: none; + font: inherit; + color: inherit; + background-color: transparent; + text-align: left; + cursor: pointer; +} + .my-node-enter { opacity: 0; } @@ -442,4 +462,4 @@ textarea { .my-node-exit-active { opacity: 0; transition: opacity 400ms; -} \ No newline at end of file +} @@ -0,0 +1,4 @@ + + + + \ No newline at end of file @@ -0,0 +1,8 @@ + + + + \ No newline at end of file @@ -29,8 +29,6 @@ export const ApiKeyModal = ({ const [title, setTitle] = useState('') const theme = useAppSelector((state) => state.theme.theme) - const { error, showError } = useShowData() - const [isLoading, setIsLoading] = useState(false) const { data, status } = useSession() @@ -101,7 +99,6 @@ export const ApiKeyModal = ({ )} - ) } @@ -13,6 +13,7 @@ import { ButtonGray, ButtonUI, Error, InputStyleDark, InputStyleLight, Loader, M import { useShowData } from '#/shared/lib/hooks' import styles from './add-business-group.module.scss' +import { useShowDataStore } from '#/shared/lib/hooks/use-show-data' interface InviteModalProps extends ModalProps { // showNewPersons: (persons: ResponseGetPersons) => void @@ -29,7 +30,7 @@ export const AddBusinessGroup: FC = ({ open, onClose, company_ const [limit, setLimit] = useState('0') const [title, setTitle] = useState('') const theme = useAppSelector((state) => state.theme.theme) - const { error, showError } = useShowData() + const { showMessage } = useShowDataStore() const { data: session } = useSession() const onSubmit = async (data: any) => { @@ -37,7 +38,7 @@ export const AddBusinessGroup: FC = ({ open, onClose, company_ const res = await addBusinessGroup(title, limit, company_uid, session?.access) if (res === null) { - showError('Что-то пошло не так') + showMessage('Что-то пошло не так') setIsLoading(false) return } @@ -52,11 +53,9 @@ export const AddBusinessGroup: FC = ({ open, onClose, company_ onClose() reset() - - // showNewPersons(res) } const checkError: SubmitErrorHandler = (data) => { - showError(Object.values(data)[0].message || 'Неверные данные') + showMessage(Object.values(data)[0].message || 'Неверные данные') } const handleEmailChange = (event: any) => { @@ -111,7 +110,6 @@ export const AddBusinessGroup: FC = ({ open, onClose, company_ )} - ) } @@ -19,6 +19,7 @@ import { API_URL } from '#/shared/lib/constants' import { useShowData } from '#/shared/lib/hooks' import styles from './add-business-group.module.scss' +import { useShowDataStore } from '#/shared/lib/hooks/use-show-data' interface InviteModalProps extends ModalProps { // showNewPersons: (persons: ResponseGetPersons) => void @@ -30,14 +31,7 @@ interface InviteModalProps extends ModalProps { current_group: ResponseGetBusinessGroups } -export const ChangeBusinessGroup: FC = ({ - open, - onClose, - company_uid, - current_group, - setChanges, - changes, -}) => { +export const ChangeBusinessGroup: FC = ({ open, onClose, company_uid, current_group, setChanges, changes }) => { const { handleSubmit, register, reset, setValue, getValues } = useForm() const [isLoading, setIsLoading] = useState(false) const [limit, setLimit] = useState('') @@ -45,7 +39,7 @@ export const ChangeBusinessGroup: FC = ({ const [accounts, setAccounts] = useState() const [addUsers, setAddUsers] = useState() const theme = useAppSelector((state) => state.theme.theme) - const { error, showError } = useShowData() + const { showMessage } = useShowDataStore() const { data: session } = useSession() useEffect(() => { @@ -53,7 +47,7 @@ export const ChangeBusinessGroup: FC = ({ if (res !== null) { setTitle(res.title) setLimit(res.token_limit) - setAccounts(res.accounts) + setAccounts(res.accounts as any) } }) }, [current_group, session?.access]) @@ -93,7 +87,7 @@ export const ChangeBusinessGroup: FC = ({ const res = await changeBusinessGroup(title, limit, current_group.uid, session?.access) if (res === null) { - showError('Что-то пошло не так') + showMessage('Что-то пошло не так') setIsLoading(false) return } else { @@ -113,7 +107,7 @@ export const ChangeBusinessGroup: FC = ({ reset() } const checkError: SubmitErrorHandler = (data) => { - showError(Object.values(data)[0].message || 'Неверные данные') + showMessage(Object.values(data)[0].message || 'Неверные данные') } return ( @@ -211,7 +205,6 @@ export const ChangeBusinessGroup: FC = ({ )} - ) } @@ -22,8 +22,6 @@ export const DownloadModal = ({ const [endDate, setEndDate] = useState() const [startDate, setStartDate] = useState() - const { error, showError } = useShowData() - const [isLoading, setIsLoading] = useState(false) const { data: session } = useSession() @@ -76,7 +74,6 @@ export const DownloadModal = ({ )} - ) } @@ -0,0 +1,18 @@ +import axios from 'axios' + +import { API_URL } from '#/shared/lib/constants' + +export const changePassword = async (email: string, password: string, token?: string) => { + return await axios.patch( + API_URL + `/auth/business-host/account/change-pass/${email}`, + { + password_1: password, + password_2: password, + }, + { + headers: { + Authorization: `Bearer ${token}`, + }, + } + ) +} @@ -0,0 +1,42 @@ +import { useSession } from 'next-auth/react' +import { changePassword } from '../api/change-password' +import { useState } from 'react' +import { getModalById, PLATE_CHANGE_PASSWORD } from '#/features/modals' +import { useShowDataStore } from '#/shared/lib/hooks/use-show-data' +import { useForm } from 'react-hook-form' + +interface ChangePasswordForm { + password: string +} + +export const useChangePassword = () => { + const modal = getModalById(PLATE_CHANGE_PASSWORD) + const { + register, + formState: { errors }, + reset, + trigger, + watch, + handleSubmit, + setValue, + } = useForm() + const { showMessage } = useShowDataStore() + const { data: session } = useSession() + + const onSubmit = handleSubmit(async (data, event) => { + if (!session) return + + const respose = await changePassword(modal.getStoreProperty('email')!, data.password, session.access) + + if (respose.status == 200) { + showMessage('Пароль изменен успешно!', 'success') + modal.setState(false) + } else { + showMessage(respose.data, 'error') + } + + reset() + }) + + return { onSubmit, register, watch, errors, trigger, setValue } +} @@ -0,0 +1,4 @@ +interface ResponseChangePassword { + data: any + status: number +} @@ -1,7 +1,20 @@ -.modalContainer { - min-width: 300px; - min-height: 200px; - background: var(--background-color-additional); - border-radius: 15px; - padding: 15px; -} \ No newline at end of file +.container { + padding-bottom: 0; +} + +.modal { + min-width: 450px; + width: 100%; + + &__buttons { + display: flex; + margin-top: 45px; + gap: 10px; + } + &__header { + margin-bottom: 35px; + font-size: 30px; + font-weight: 600; + letter-spacing: -0.02em; + } +} @@ -0,0 +1,49 @@ +import { getModalById, PLATE_CHANGE_PASSWORD, PlateTemplate } from '#/features/modals' +import styles from './change-password.module.scss' +import React, { useEffect } from 'react' +import { CommonButton } from '#/shared/ui/button' +import { CommonInput } from '#/shared/ui/common-input' +import { useChangePassword } from '../lib/use-change-password' + +export const ChangePasswordPlate = () => { + const modal = getModalById(PLATE_CHANGE_PASSWORD) + const { onSubmit, register, errors, trigger, watch, setValue } = useChangePassword() + + return ( + + + Смена пароля + { + setValue('password', e.target.value) + trigger('password') + }} + label='Новый пароль' + variant='outline' + placeholder='Пароль' + error={errors.password} + // onInput={() => trigger('password')} + /> + + + Сменить пароль + + { + modal.setState(false) + }} + type='button' + > + Отмена + + + + + ) +} @@ -0,0 +1 @@ +export * from './change-password' \ No newline at end of file @@ -0,0 +1 @@ +export * from './ui' \ No newline at end of file @@ -12,7 +12,7 @@ export const invitePerson = async ( token?: string ): Promise => { try { - const { data } = await axios.post<{ email: string; token_limit: string }, AxiosResponse>( + const { data, status } = await axios.post<{ email: string; token_limit: string }, AxiosResponse>( API_URL + '/auth/business-host', { parent_company: group === '' ? null : group, @@ -26,6 +26,10 @@ export const invitePerson = async ( } ) + // лютый говнокод 😎 + // ПОМОГИ + data.status = status + return data } catch (err) { return null @@ -3,6 +3,9 @@ export type PersonInBusiness = 'business_host' | 'business_account' | 'business_ export type AcceptanceStatus = 'pending' | 'accepted' | 'rejected' | 'cancelled' export type ResponseGetPersons = { + status: number + detail: string + uid: string email: string account_type: PersonInBusiness acceptance_status: AcceptanceStatus @@ -27,6 +27,7 @@ import { InviteRoles, inviteRoles } from '../lib/constants' import { ResponseGetBusinessGroups, ResponseGetPersons } from '../model/types' import styles from './invite-modal.module.scss' +import { useShowDataStore } from '#/shared/lib/hooks/use-show-data' interface InviteModalProps extends ModalProps { showNewPersons: (persons: ResponseGetPersons) => void @@ -34,10 +35,10 @@ interface InviteModalProps extends ModalProps { export const InviteModal: FC = ({ open, onClose, showNewPersons }) => { const [role, setRole] = useState('Сотрудник') const { handleSubmit, register, reset, setValue, getValues } = useForm() - const { error, showError } = useShowData() const [isLoading, setIsLoading] = useState(false) const [businessGroups, setBusinessGroups] = useState() const [currentGroup, setCurrentGroup] = useState('') + const { showMessage } = useShowDataStore() const { data: session } = useSession() const onSubmit = async (data: any) => { @@ -46,11 +47,17 @@ export const InviteModal: FC = ({ open, onClose, showNewPerson const group: string[] | undefined = businessGroups?.map((el) => (el.title === currentGroup ? el.uid : '')) const res = await invitePerson(role, data.limit, data.email, group && group[0], session?.access) + if (res && res?.status >= 300) { + showMessage(res?.detail) + setIsLoading(false) + return + } if (res === null) { - showError('Что-то пошло не так') + showMessage('Что-то пошло не так') setIsLoading(false) return } + setIsLoading(false) onClose() @@ -59,7 +66,7 @@ export const InviteModal: FC = ({ open, onClose, showNewPerson showNewPersons(res) } const checkError: SubmitErrorHandler = (data) => { - showError(Object.values(data)[0].message || 'Неверные данные') + showMessage(Object.values(data)[0].message || 'Неверные данные') } const handleEmailChange = (event: any) => { @@ -85,11 +92,7 @@ export const InviteModal: FC = ({ open, onClose, showNewPerson {/* Поле с выбором роли сотрудника */} Выберите роль - setRole(e.target.value as InviteRoles)} - value={role} - > + setRole(e.target.value as InviteRoles)} value={role}> {/* Поле с выбором группы */} Выберите бизнес-группу @@ -125,7 +128,6 @@ export const InviteModal: FC = ({ open, onClose, showNewPerson )} - ) } @@ -0,0 +1 @@ +export * from './modals.config' \ No newline at end of file @@ -0,0 +1,2 @@ +export const PLATE_CHANGE_PASSWORD = 'plate-change-password' +export const RESEND_INVATION_PASSWORD = 'resend-invation-password' @@ -0,0 +1 @@ +export * from './store.modals' \ No newline at end of file @@ -0,0 +1,78 @@ +import { create } from "zustand"; + +type Set = { + (partial: (state: PlatesStore) => Partial): void; +}; + +type Get = () => PlatesStore; + +function makeModalInstance(set: Set, get: Get, key: string) { + const ModalInstance: Modal = { + id: key, + state: false, + store: {}, + setState(state, store) { + const modals = get().modals; + + store = store ?? {}; + + set(() => ({ + modals: { ...modals, [this.id]: { ...modals[key], state, store } }, + })); + }, + setStoreProperty(key, value) { + const modals = get().modals; + + this.store[key] = value; + + set(() => ({ + modals: { ...modals, [this.id]: { ...modals[key], store: this.store } }, + })); + }, + getStoreProperty(key) { + return this.store[key]; + }, + }; + + return ModalInstance; +} + +export interface Modal { + id: string; + state: boolean; + store: Record; + setState: (state: boolean, store?: Record) => void; + setStoreProperty: (key: string, value: any) => void; + getStoreProperty: (key: string) => T | undefined; +} + +export interface PlatesStore { + modals: Record; + setModal: (key: string) => void; + getModal: (key: string) => Modal; +} + +export const usePlatesStore = create((set, get) => { + const modals: Record = {}; + + function setModal(key: string) { + set((state) => ({ + modals: { + ...state.modals, + [key]: Object.assign({}, makeModalInstance(set, get, key)), + }, + })); + } + + function getModal(key: string) { + return ( + get().modals[key] ?? Object.assign({}, makeModalInstance(set, get, key)) + ); + } + + return { + modals, + setModal, + getModal, + }; +}); @@ -0,0 +1 @@ +export { default as PlateTemplate } from "./template"; \ No newline at end of file @@ -0,0 +1,105 @@ +.template { + position: fixed; + width: 100%; + max-width: calc(100dvw); + height: 100dvh; + background-color: rgba(0, 0, 0, 0.4); + top: 0; + left: 0; + transition: all 300ms ease-in-out; + display: flex; + overflow: hidden; + opacity: 0; + visibility: hidden; + + &_visible { + opacity: 1; + visibility: visible; + z-index: 9999; + } + + &_invisible { + opacity: 0; + visibility: hidden; + z-index: -1; + } + + &_align-x-left { + justify-content: flex-start; + + > * { + margin-left: 10px; + } + } + + &_align-x-center { + justify-content: center; + } + + &_align-x-right { + justify-content: flex-end; + + > * { + margin-right: 10px; + } + } + + &_align-y-top { + align-items: flex-start; + + > * { + margin-top: 10px; + } + } + + &_align-y-center { + align-items: center; + } + + &_align-y-bottom { + align-items: flex-end; + + > * { + margin-bottom: 10px; + } + } + + &__content { + background-color: var(--new-ui-main-color); + border-radius: 20px; + position: relative; + overflow: hidden; + height: fit-content; + transition: all 0.5s ease-in-out; + } + + &__header { + display: flex; + justify-content: space-between; + padding: 25px; + } + + &__close-button { + position: relative; + z-index: 1; + transform: rotate(45deg); + } + + &__content-body { + transition: all 0.5s ease-in-out; + padding: 0px 32px 32px 32px; + overflow: hidden; + + @media screen and (max-width: 1000px) { + padding: 0px 10px 10px 10px; + } + } + + &__animation { + transition: all 0.3s ease-in-out; + } + + &__animation-behavior { + transform: translateX(50px); + } +} @@ -0,0 +1,78 @@ +'use client' +import { useEffect } from 'react' +import { c } from '#/shared' +import { usePlatesStore } from '../model' +import styles from './template.module.scss' + +import PlusIcon from '#/assets/svg/plus.svg?react' +import { useThemeAndDevice } from '#/shared/lib/hooks' + +export type AlignX = 'left' | 'center' | 'right' +export type AlignY = 'top' | 'center' | 'bottom' + +export interface PlatesTemplateProps { + id: string + children: React.ReactNode + title?: string + closeModal?: () => void + hasTemplate?: boolean + hasBg?: boolean + alignX?: AlignX + alignY?: AlignY + animationClass?: string + animationBehaviorClass?: string + headerClassName?: string +} + +export default function PlatesTemplate({ + id, + title, + children, + alignX = 'center', + alignY = 'center', + hasTemplate = true, + hasBg = true, + animationClass = styles['template__animation'], + animationBehaviorClass = styles['template__animation-behavior'], + headerClassName, +}: PlatesTemplateProps) { + const { setModal, getModal } = usePlatesStore() + + const modal = getModal(id) + + const { theme } = useThemeAndDevice() + + useEffect(() => setModal(id), []) + + return ( + { + if (hasBg) modal.setState(false) + }} + > + {hasTemplate ? ( + e.stopPropagation()} + > + + {title} + modal.setState(false)}> + + + + {children} + + ) : ( + {children} + )} + + ) +} @@ -0,0 +1,9 @@ +import { usePlatesStore } from './model'; + +export function getModalById(id: string) { + const { getModal } = usePlatesStore(); + return getModal(id); +} + +export * from './ui' +export * from './config' \ No newline at end of file @@ -15,11 +15,12 @@ import { useShowData } from '#/shared/lib/hooks' import { nameOptions } from '../../lib/constants-step-contact' import { createBusinessCompany, setContact } from '../../model/stepper-slice' +import { useShowDataStore } from '#/shared/lib/hooks/use-show-data' export const StepContact = () => { const { phone, email, name, job_title } = useAppSelector((state) => state.stepper.dataForCreate) const theme = useAppSelector((state) => state.theme.theme) - const { error, showError } = useShowData() + const { showMessage } = useShowDataStore() const methods = useForm({ defaultValues: { phone, @@ -31,7 +32,7 @@ export const StepContact = () => { const dispatch = useAppDispatch() const checkError: SubmitErrorHandler = (data) => { - showError(Object.values(data)[0].message || 'Неверные данные') + showMessage(Object.values(data)[0].message || 'Неверные данные') } const { data: session } = useSession() @@ -78,7 +79,6 @@ export const StepContact = () => { - ) } @@ -4,12 +4,7 @@ import { FormProvider, useForm } from 'react-hook-form' import { SubmitErrorHandler } from 'react-hook-form/dist/types/form' import { Box, TextField, Typography } from '@mui/material' -import { - FieldActivity, - FieldActivitySelect, - Frequency, - FrequencySelect, -} from '#/features/register-business/lib/constants-step-information' +import { FieldActivity, FieldActivitySelect, Frequency, FrequencySelect } from '#/features/register-business/lib/constants-step-information' import { IEmailForms } from '#/features/register-by-email/model/types' import { useAppDispatch, useAppSelector } from '#/app/store/store' import { ButtonUI, InputStyleDark, InputStyleLight } from '#/shared' @@ -20,12 +15,13 @@ import { SelectUI } from '#/shared/ui/select' import { setFieldActivity, setFrequency, setNumberStuff, switchNextStep } from '../../model/stepper-slice' import styles from './step-information.module.scss' +import { useShowDataStore } from '#/shared/lib/hooks/use-show-data' export const StepInformation = () => { const dispatch = useAppDispatch() const { frequency, fieldActivity, numberStuff } = useAppSelector((state) => state.stepper.dataForCreate) - const { error, showError } = useShowData() + const { showMessage } = useShowDataStore() const methods = useForm({ mode: 'onSubmit', @@ -41,7 +37,7 @@ export const StepInformation = () => { } const checkError: SubmitErrorHandler = (data) => { - showError(Object.values(data)[0].message || 'Неверные данные') + showMessage(Object.values(data)[0].message || 'Неверные данные') } const theme = useAppSelector((state) => state.theme.theme) @@ -81,7 +77,6 @@ export const StepInformation = () => { - ) @@ -39,7 +39,7 @@ export const StepLegalInformative = () => { const [rulesData, isIP] = useCheckTypeCompany(typeof selected === 'string' ? selected : selected?.value) - const { error, showError } = useShowData() + const { showMessage } = useShowDataStore() const onSubmit = (data: any, e: any) => { e.preventDefault() dispatch(setCompanyName(data.companyName)) @@ -54,7 +54,7 @@ export const StepLegalInformative = () => { const checkError: SubmitErrorHandler = (data, event) => { event!.preventDefault() - showError(Object.values(data)[0].message || 'Неверные данные') + showMessage(Object.values(data)[0].message || 'Неверные данные') } return ( @@ -104,7 +104,6 @@ export const StepLegalInformative = () => { - ) } @@ -16,6 +16,7 @@ import { StepInformation } from '../step-information/step-information' import { StepLegalInformative } from '../step-legal-informative/step-legal-informative' import styles from './stepper.module.scss' +import { useShowDataStore } from '#/shared/lib/hooks/use-show-data' export const Stepper = () => { const activeStep = useAppSelector((state) => state.stepper.activeStep) @@ -23,7 +24,7 @@ export const Stepper = () => { const dispatch = useAppDispatch() - const { error, showError } = useShowData() + const { showMessage } = useShowDataStore() const processCreate = useAppSelector((state) => state.stepper.loadingCreate) @@ -31,7 +32,7 @@ export const Stepper = () => { useEffect(() => { if (processCreate === 'failed') { - showError('Ошибка создания аккаунта') + showMessage('Ошибка создания аккаунта') } if (processCreate === 'succeeded') { @@ -112,7 +113,6 @@ export const Stepper = () => { )} - )} > @@ -22,6 +22,7 @@ import { useShowData } from '#/shared/lib/hooks' import { useThemeAndDevice } from '#/shared/lib/hooks' import OpenedEyeSvg from '#/assets/svg/opened-eye.svg?react' import ClosedEyeSvg from '#/assets/svg/closed-eye.svg?react' +import { useShowDataStore } from '#/shared/lib/hooks/use-show-data' interface IRegisterEmailFormProps { successLogin: () => void @@ -34,7 +35,7 @@ export const RegisterEmailForm: React.FC = ({ successLo const referral = useAppSelector((state) => state.user.referral) - const { error, showError } = useShowData() + const { showMessage } = useShowDataStore() const { push } = useRouter() @@ -49,7 +50,7 @@ export const RegisterEmailForm: React.FC = ({ successLo setLoading(true) if (!data.rules) { - showError('Запрос отправлен', true) + showMessage('Запрос отправлен', true) try { const { status } = await axios.get( API_URL + `/auth/mail/whitelist?email=${data.email}` @@ -58,7 +59,7 @@ export const RegisterEmailForm: React.FC = ({ successLo setIsEmailWhite(true) } } catch (e) { - showError('Примите пользовательское соглашение', true) + showMessage('Примите пользовательское соглашение', true) setLoading(false) return } @@ -97,12 +98,12 @@ export const RegisterEmailForm: React.FC = ({ successLo } } catch (err: any) { setLoading(false) - showError(err.response.data.detail) + showMessage(err.response.data.detail) } } const checkError: SubmitErrorHandler = (data) => { - showError(Object.values(data)[0].message || 'Неверные данные') + showMessage(Object.values(data)[0].message || 'Неверные данные') } const handleInputChangeTrim = (event: any) => { @@ -299,7 +300,6 @@ export const RegisterEmailForm: React.FC = ({ successLo - ) } @@ -1,27 +1,13 @@ -import axios, { AxiosResponse } from 'axios' - -import { ResponseGetPersons } from '#/features/invite-person-in-business' +import axios from 'axios' import { API_URL } from '#/shared/lib/constants' -export const removePerson = async (email: string, token?: string): Promise => { - if (!token) { - return null - } - - try { - const { data } = await axios.put<{ status: 'cancelled' }, AxiosResponse>( - API_URL + `/auth/business-host/accounts/${email}`, - { - status: 'cancelled', - }, - { - headers: { - Authorization: `Bearer ${token}`, - }, - } - ) - return data - } catch (err) { - return null - } +export const removePerson = async (person_uid?: string, token?: string) => { + return await axios.delete(API_URL + `/auth/business-host`, { + data: { + uid: person_uid, + }, + headers: { + Authorization: `Bearer ${token}`, + }, + }) } @@ -1,27 +1,52 @@ -import { Dispatch, SetStateAction, useState } from 'react' +import { Dispatch, SetStateAction, useState, useEffect } from 'react' import { useSession } from 'next-auth/react' import { ResponseGetPersons } from '#/features/invite-person-in-business' import { removePerson } from '../api/remove' +import { useShowDataStore } from '#/shared/lib/hooks/use-show-data' export const useRemove = ( - updateList: (persons: ResponseGetPersons) => void, - email: string -): [open: boolean, setOpen: Dispatch>, removeFn: () => void] => { + updateList: (persons: ResponseGetPersons | null) => void, + person: ResponseGetPersons | null +): [open: boolean, setOpen: Dispatch>, remove: () => void] => { const [open, setOpen] = useState(false) - + const { showMessage } = useShowDataStore() const { data } = useSession() const remove = async () => { - const result = await removePerson(email, data?.access) + const response = await removePerson(person?.uid, data?.access) - if (result === null) { + if (response.status === 200) { + showMessage('Cотрудник удален!', false) + setOpen(false) + updateList(person) return [open, setOpen] + } else { + showMessage(response.data.detail) } - setOpen(false) - updateList(result) } return [open, setOpen, remove] } + +// export const useChangePassword = () => { +// const modal = getModalById(PLATE_CHANGE_PASSWORD) +// const callback = modal.getStoreProperty('callback')! +// const [password, setPassword] = useState('') +// const { error, showError } = useShowData() +// const { data } = useSession() + +// const change = async () => { +// const respose = await changePassword(modal.getStoreProperty('email')!, password, data?.access) + +// if (respose.status == 200) { +// showError('Пароль изменен успешно!', true) +// modal.setState(false) +// } else { +// showError(respose.data, false) +// } +// } + +// return { change, password, setPassword, error } +// } @@ -6,13 +6,13 @@ import { ButtonGray, ButtonUI, Modal } from '#/shared' import { useRemove } from '../../model/remove' export const XMark = ({ - email, + person, setNewPersons, }: { - email: string - setNewPersons: (persons: ResponseGetPersons) => void + person: ResponseGetPersons | null + setNewPersons: (persons: ResponseGetPersons | null) => void }) => { - const [open, setOpen, removeFn] = useRemove(setNewPersons, email) + const [open, setOpen, remove] = useRemove(setNewPersons, person) return ( e.stopPropagation()} sx={{ marginTop: 0.5, marginLeft: 0.8 }}> @@ -21,9 +21,9 @@ export const XMark = ({ e.stopPropagation() setOpen(true) }} - src={'/x-mark.svg'} - width={15} - height={15} + src={'/svg/trash-outline.svg'} + width={16} + height={16} alt={'Удалить'} /> - Вы дейсвительно хотите удалить пользователя {email} ? + Вы дейсвительно хотите удалить пользователя {person?.email} ? { + onClick={(e: any) => { e.stopPropagation() - await removeFn() + remove() }} text={'Удалить'} /> @@ -0,0 +1,14 @@ +import { API_URL } from '#/shared/lib/constants' +import axios from 'axios' + +export const resendInvation = async (email: string, token?: string) => { + return await axios.post( + API_URL + `/auth/business-host/re-invite/${email}`, + {}, + { + headers: { + Authorization: `Bearer ${token}`, + }, + } + ) +} @@ -0,0 +1,23 @@ +import { useSession } from 'next-auth/react' +import { resendInvation } from '../api/resend-invation' +import { getModalById, RESEND_INVATION_PASSWORD } from '#/features/modals' +import { useShowDataStore } from '#/shared/lib/hooks/use-show-data' + +export const useResendInvation = () => { + const modal = getModalById(RESEND_INVATION_PASSWORD) + const { showMessage } = useShowDataStore() + const { data } = useSession() + + const resend = async () => { + const response = await resendInvation(modal.getStoreProperty('email')!, data?.access) + + if (response.status == 200) { + showMessage('Приглашение переотправлено!', false) + modal.setState(false) + } else { + showMessage(response.data) + } + } + + return [resend] +} @@ -0,0 +1,4 @@ +interface ResponseResendInvation { + data: any, + status: number +} \ No newline at end of file @@ -0,0 +1 @@ +export * from './resend-invation' \ No newline at end of file @@ -0,0 +1,25 @@ +.container { + padding-bottom: 0; +} + +.modal { + min-width: 450px; + width: 100%; + + &__buttons { + display: flex; + margin-top: 45px; + gap: 10px; + } + &__header { + margin-bottom: 15px; + font-size: 30px; + font-weight: 600; + letter-spacing: -0.02em; + } + &__message { + font-size: 15px; + color: var(--text-color-main); + width: 70%; + } +} @@ -0,0 +1,32 @@ +import { getModalById, RESEND_INVATION_PASSWORD, PlateTemplate } from '#/features/modals' +import styles from './resend-invation.module.scss' +import React from 'react' +import { CommonButton } from '#/shared/ui/button' +import { CommonInput } from '#/shared/ui/common-input' +import { useResendInvation } from '../lib/use-resend-invation' +import { useShowDataStore } from '#/shared/lib/hooks/use-show-data' + +interface ResendInvationPlateProps {} + +export const ResendInvationPlate = ({}: ResendInvationPlateProps) => { + const modal = getModalById(RESEND_INVATION_PASSWORD) + + const [resend] = useResendInvation() + + return ( + + + Переотправить приглашение + При переотправке приглашения у сотрудника будет заменен отправленный пароль. + + resend()}> + Переотправить + + modal.setState(false)}> + Отмена + + + + + ) +} @@ -0,0 +1 @@ +export * from './ui' \ No newline at end of file @@ -17,6 +17,7 @@ import '#/app/styles/globals.css' import '#/app/styles/styles-pages/system.scss' import { NextPage } from 'next' import { pingFangFont } from '#/shared/lib/constants/font/font' +import { Error } from '#/shared' axios.defaults.httpsAgent = new https.Agent({ rejectUnauthorized: false, @@ -39,13 +40,16 @@ function App({ Component, pageProps: { session, ...pageProps } }: AppPropsWithLa const getLayout = Component.getLayout ?? ((page) => page) if ('serviceWorker' in navigator) { - navigator.serviceWorker.getRegistrations().then(function (registrations) { - for (let registration of registrations) { - registration.unregister(); - } - }).catch(function (error) { - console.error('Ошибка при размонтировании service worker:', error); - }); + navigator.serviceWorker + .getRegistrations() + .then(function (registrations) { + for (let registration of registrations) { + registration.unregister() + } + }) + .catch(function (error) { + console.error('Ошибка при размонтировании service worker:', error) + }) } return ( @@ -62,6 +66,7 @@ function App({ Component, pageProps: { session, ...pageProps } }: AppPropsWithLa {getLayout()} + @@ -1,19 +1,45 @@ import React, { useState } from 'react' +import { create } from 'zustand' -export const useShowData = (): { - error: string - showError: (message: string, isErrorMessage?: boolean) => void - isError: boolean -} => { - const [message, setError] = React.useState('') +export type Variant = 'error' | 'success' - const [isError, setIsError] = useState(false) +interface ShowDataStore { + message: string + variant: Variant + isOpened: boolean + setOpened: (isOpened: boolean) => void + setMessage: (message: string) => void + setVariant: (variant: Variant) => void + showMessage: (message: string, variant: Variant) => void +} - function showError(message: string = 'Произошла ошибка', isErrorMessage: boolean = false) { - setIsError(isErrorMessage) - setError(message) - setTimeout(() => setError(''), 5000) +export const useShowDataStore = create((set, get) => { + function setVariant(variant: Variant = 'error') { + set({ ...get(), variant }) } - return { error: message, showError, isError } -} + function setMessage(message: string) { + set({ ...get(), message }) + } + + function setOpened(isOpened: boolean) { + set({ ...get(), isOpened }) + } + + function showMessage(message: string = 'Произошла ошибка', variant: Variant = 'error') { + setVariant(variant) + setOpened(true) + setMessage(message) + setTimeout(() => setOpened(false), 5000) + } + + return { + message: '', + variant: 'error', + isOpened: false, + setOpened, + setVariant, + setMessage, + showMessage, + } +}) @@ -4,12 +4,11 @@ background-color: transparent; padding: 15px 25px; cursor: pointer; - box-sizing: content-box; &_outline { border-radius: 15px; - border: 0.15rem solid var(--new-ui-text-color); - color: var(--new-ui-text-color); + border: 1px solid white; + color: white; font-weight: 600; transition: color 0.3s ease-in-out, background-color 0.3s ease-in-out; font-size: 15px; @@ -45,14 +44,34 @@ &_primary { background-color: var(--air-color); - color: var(--new-ui-text-color); + color: white; border-radius: 12px; font-weight: 500; font-size: 15px; transition: color 0.3s ease-in-out, background-color 0.3s ease-in-out; &:hover { background-color: var(--text-color-purple); - color: var(--new-ui-text-color); + &:hover { + opacity: 0.7; + } + } + } + + &_gray { + background-color: var(--bg-color-button-gray); + color: var(--air-color); + border-radius: 12px; + font-weight: 500; + font-size: 15px; + transition: color 0.3s ease-in-out, background-color 0.3s ease-in-out; + &:hover { + opacity: 0.7; } } } + +body[data-theme='dark'] { + .button_gray { + color: white; + } +} @@ -1,7 +1,7 @@ import { c } from '#/shared/lib/helpers' import styles from './common-button.module.scss' -export type ButtonVariant = 'outline' | 'primary' | 'primary-outline' +export type ButtonVariant = 'outline' | 'primary' | 'primary-outline' | 'gray' export interface CommonButtonProps extends React.ButtonHTMLAttributes { children?: React.ReactNode @@ -0,0 +1,61 @@ +.box { + display: flex; + flex-direction: column; +} + +.header { + display: flex; + align-items: center; + justify-content: space-between; + padding-top: 10px; + + &__error { + transform: translateY(16px); + font-weight: bold; + color: red; + opacity: 0; + transition: all 300ms; + + &_visible { + opacity: 1; + transform: translateY(0); + } + } +} + +.label { + color: #97989f; + font-size: 14px; + margin-bottom: 8px; +} + +.input { + background-color: transparent; + font-size: 16px; + &:focus { + border: none; + outline: none; + } + + &_outline { + color: var(--text-color-main); + padding: 20px; + border: 2px solid var(--background-color-table); + border-radius: 13px; + &::placeholder { + color: #97989f; + } + &:focus { + border: 2px solid var(--background-color-table); + } + } +} + +html[data-theme='light'] { + .input_outline { + border: 2px solid #f2f2f8; + &:focus { + border: 2px solid #f2f2f8; + } + } +} @@ -0,0 +1,44 @@ +import { c } from '#/shared/lib/helpers' +import { FieldError } from 'react-hook-form' +import styles from './common-input.module.scss' +import { useEffect } from 'react' + +export type InputVariant = 'outline' + +export interface CommonInputProps extends React.InputHTMLAttributes { + children?: React.ReactNode + variant?: InputVariant + label?: string + error?: FieldError +} + +export const CommonInput = ({ children, variant = 'outline', label, error, className, disabled, ...props }: CommonInputProps) => { + return ( + + + {label && {label}} + {error ? error?.message : ''} + + + + ) +} + +{ + /* + {label && {label}} + + {error ? error?.message : ""} + + */ +} @@ -0,0 +1 @@ +export * from './common-input' \ No newline at end of file @@ -0,0 +1 @@ +export * from './ui' \ No newline at end of file @@ -1,55 +1,59 @@ -import React, { useRef } from 'react' +import React, { useEffect, useRef } from 'react' import { Alert, Slide, Snackbar, Typography } from '@mui/material' import Image from 'next/image' import { useThemeAndDevice } from '#/shared/lib/hooks' import { CSSTransition } from 'react-transition-group' +import { useShowDataStore } from '../lib/hooks/use-show-data' +import SuccessSvg from '#/assets/svg/success.svg?react' export interface IError { - error: string - open: boolean handleClose?: () => void } const ErrorIcon = () => { const { theme } = useThemeAndDevice() - return ( - - ) + return } -export const Error: React.FC = ({ error, handleClose, open }) => { +export const Error: React.FC = ({ handleClose }) => { + const { message, isError, isOpened } = useShowDataStore() const { theme } = useThemeAndDevice() const nodeRef = useRef(null) return ( - - - } - severity='error' - sx={{ - backgroundColor: theme === 'light' ? 'white' : '#404040', - border: '1px solid #F15179', - color: theme === 'light' ? '#666666' : '#C7C7C7', - borderRadius: '12px', - }} - > - {error} - + + + {isError ? ( + } + severity='error' + sx={{ + backgroundColor: theme === 'light' ? 'white' : '#404040', + border: '1px solid #F15179', + color: theme === 'light' ? '#666666' : '#C7C7C7', + borderRadius: '12px', + }} + > + {message} + + ) : ( + } + severity='error' + sx={{ + backgroundColor: theme === 'light' ? 'white' : '#404040', + border: '1px solid #5ef151', + color: theme === 'light' ? '#666666' : '#C7C7C7', + borderRadius: '12px', + }} + > + {message} + + )} ) @@ -1,60 +0,0 @@ -import React, { useMemo } from 'react' -import { Alert, Slide, Snackbar, Typography } from '@mui/material' -import Image from 'next/image' - -import { useThemeAndDevice } from '#/shared/lib/hooks' - -interface ISuccess { - message: string - open: boolean - handleClose?: () => void - isError?: boolean -} -export const Success: React.FC = ({ message, handleClose, open, isError = false }) => { - const { theme } = useThemeAndDevice() - - const WidgetIcon = useMemo(() => { - if (isError) { - return theme === 'dark' ? '/error2.svg' : '/error2_white.svg' - } - - return '/svg/alert/success.svg' - }, [theme, isError]) - - return ( - - - - ), - }} - severity='success' - sx={ - !isError - ? { - backgroundColor: theme === 'light' ? 'white' : '#294239', - border: '2px solid #22B47F', - color: theme === 'light' ? '#666666' : '#C7C7C7', - borderRadius: '12px', - } - : { - backgroundColor: theme === 'light' ? 'white' : '#404040', - border: '1px solid #F15179', - color: theme === 'light' ? '#666666' : '#C7C7C7', - borderRadius: '12px', - } - } - > - {message} - - - - ) -} @@ -25,7 +25,6 @@ export { ReferralBlock } from './ui/referral-block' export { Search } from './ui/search/search' export { SelectUI as Select } from './ui/select' export { Slider } from './ui/slider/slider' -export { Success } from './ui/success' export { SwitchCustom } from './ui/switch/switch' export { TooltipCustom } from './ui/tooltip/tooltip' export { TooltipFreeTokens } from './ui/tooltip-free-tokens' @@ -12,7 +12,7 @@ import { getAllInfo, unfollowEmail } from '#/entities/user-account/model/user-ty import { useAppDispatch, useAppSelector } from '#/app/store/store' import styles2 from '#/app/styles/accountTabs.module.css' import styles from '#/app/styles/business.module.scss' -import { ButtonUI, Input, Loader, Modal, Success, SwitchCustom, useShowData } from '#/shared' +import { ButtonUI, Error, Input, Loader, Modal, SwitchCustom, useShowData } from '#/shared' import { accountApi } from '#/shared/api/account-endpoints' import { API_URL } from '#/shared/lib/constants/constants' import { getDeviceType } from '#/shared/lib/helpers' @@ -24,6 +24,7 @@ import { Referral } from '#/widgets/referral' import { NextPageWithLayout } from '#/pages/_app' import { DownloadModal } from '#/features/business-security-download' import { scopes } from '../config' +import { useShowDataStore } from '#/shared/lib/hooks/use-show-data' const Account: NextPageWithLayout = () => { const { @@ -61,7 +62,8 @@ const Account: NextPageWithLayout = () => { const dispatch = useAppDispatch() - const { error, showError, isError } = useShowData() + // const { error, showMessage, isError } = useShowData() + const { showMessage } = useShowDataStore() const handleFileChange = async (event: any) => { const formData = new FormData() @@ -76,11 +78,11 @@ const Account: NextPageWithLayout = () => { }, }) dispatch(getAllInfo(data?.access)) - showError('Изображение успешно загружено!') + showMessage('Изображение успешно загружено!') setLoading(false) } catch (e) { setLoading(false) - showError('Ошибка загрузки изображения на сервере!', true) + showMessage('Ошибка загрузки изображения на сервере!', true) } } @@ -129,13 +131,13 @@ const Account: NextPageWithLayout = () => { const changePassword = async () => { if (!data) return if (newPassword1 !== newPassword2) { - showError('Укажите одинаковые новы пароли!', true) + showMessage('Укажите одинаковые новы пароли!', true) return } const status = await accountApi.changePassword(data.access, newPassword1, newPassword2, currentPassword) if (status === 200) { - showError('Пароль успешно изменён!') + showMessage('Пароль успешно изменён!') setNewPassword1('') setNewPassword2('') setCurrentPassword('') @@ -143,7 +145,7 @@ const Account: NextPageWithLayout = () => { return } - showError('К сожалению, произошла ошибка', true) + showMessage('К сожалению, произошла ошибка', true) } function body() { @@ -155,11 +157,7 @@ const Account: NextPageWithLayout = () => { ) } if (type === 'business_account') { - return ( - - Информация о корп.аккаунте доступна только владельцу и администраторам. - - ) + return Информация о корп.аккаунте доступна только владельцу и администраторам. } if (type === 'business_host') { return @@ -191,15 +189,12 @@ const Account: NextPageWithLayout = () => { } } - const isUserDataChange = useMemo( - () => name !== first_name || last_name !== lastName || username !== userName, - [name, lastName, userName] - ) + const isUserDataChange = useMemo(() => name !== first_name || last_name !== lastName || username !== userName, [name, lastName, userName]) const promocodeActivate = async () => { - if (!data) return + if (!data) return if (!promocode.trim()) { - showError('Введите корректный промокод', true) + showMessage('Введите корректный промокод', true) return } let resStatus = 404 @@ -213,26 +208,26 @@ const Account: NextPageWithLayout = () => { } catch (e) {} if (resStatus === 200) { - showError('Промокод успешно активирован! Токены уже зачислены!') + showMessage('Промокод успешно активирован! Токены уже зачислены!') dispatch(getUserBalance(data?.access)) return } if (resStatus === 403) { - showError('Промокод уже был активирован!', true) + showMessage('Промокод уже был активирован!', true) return } if (resStatus === 404) { - showError('Промокод не найден!', true) + showMessage('Промокод не найден!', true) return } } async function changeUserData() { - if (!data) return + if (!data) return if (!isUserDataChange) { - showError('Вы не изменили данные', true) + showMessage('Вы не изменили данные', true) return } @@ -248,13 +243,13 @@ const Account: NextPageWithLayout = () => { { headers: { Authorization: `Bearer ${data.access}` } } ) - showError('Данные успешно изменены!') + showMessage('Данные успешно изменены!') dispatch(getAllInfo(data.access)) } catch (e) {} } const deleteAccount = async () => { - if (!data) return + if (!data) return try { const { status } = await axios.delete(API_URL + '/auth/remove', { headers: { @@ -318,11 +313,7 @@ const Account: NextPageWithLayout = () => { disableRipple onClick={() => changeScope(el.scope)} key={el.title} - className={ - scope === el.scope - ? styles2.wrap_toggle_button_active - : styles2.wrap_toggle_button - } + className={scope === el.scope ? styles2.wrap_toggle_button_active : styles2.wrap_toggle_button} value={el.scope} label={el.title} /> @@ -358,12 +349,7 @@ const Account: NextPageWithLayout = () => { }} > {!loading || !(userInfoLoaded === 'succeeded') ? ( - + ) : ( )} @@ -400,25 +386,15 @@ const Account: NextPageWithLayout = () => { /> - - - Email - - + + Email + { - if (!data) return + if (!data) return await dispatch(unfollowEmail(data.access)) - showError('Данные изменены!') + showMessage('Данные изменены!') }} /> { fullWidth /> - + {/*referral_code.code.trim() && ( Ваша реферальная ссылка: @@ -483,9 +455,7 @@ const Account: NextPageWithLayout = () => { Изменить пароль - - Текущий пароль - + Текущий пароль { }} > - - Новый пароль - + Новый пароль { /> - - Подтвердить пароль - + Подтвердить пароль { )} Удаление аккаунта - - Удаление аккаунта приведет к потере всех настроек - + Удаление аккаунта приведет к потере всех настроек setConfirmDeleteModal(true)} @@ -554,15 +518,8 @@ const Account: NextPageWithLayout = () => { setConfirmDeleteModal(false)}> Удаление аккаунта - - Вы действительно хотите удалить ваш аккаунт? - - + Вы действительно хотите удалить ваш аккаунт? + Удалить @@ -591,7 +548,6 @@ const Account: NextPageWithLayout = () => { ) : ( <>> )} - @@ -28,6 +28,7 @@ import { IModelTag, IModel } from '#/entities/model-entity' import { useThemeAndDevice } from '#/shared/lib/hooks' import { SvgIcon } from '#/shared/ui/svg' import { AllChatWindow } from '#/widgets/chat-window' +import { useShowDataStore } from '#/shared/lib/hooks/use-show-data' export interface ChatBotPageProps {} @@ -40,7 +41,7 @@ const Page: NextPageWithLayout = () => { const [params, setParams] = React.useState(false) const [file, setFile] = React.useState(null) - const { error, showError } = useShowData() + const { showMessage } = useShowDataStore() const deviceType = getDeviceType() const deviceOs = getOs() @@ -64,7 +65,7 @@ const Page: NextPageWithLayout = () => { const { messages, sendMessage, loading, getMessagesPagination, deleteMessage } = useModel( currentChat, - showError, + showMessage, modelType ) @@ -177,11 +178,11 @@ const Page: NextPageWithLayout = () => { const onSendMessage = (input: string, required: (string | null)[]) => { if (required.includes('text') && (input === '' || input === null)) { - showError('Введите сообщение!') + showMessage('Введите сообщение!') return false } if (required.includes('image') && file === null) { - showError('Прикрепите изображение!') + showMessage('Прикрепите изображение!') return false } @@ -446,7 +447,6 @@ const Page: NextPageWithLayout = () => { )} - > @@ -27,6 +27,7 @@ import { ArrowDownScroll } from '#/shared/ui/icon-components/scroll-down-arrow' import { SvgIcon } from '#/shared/ui/svg' import { ImageMessagesList,useImagesPagination } from '#/widgets/messages' import { ModelInput } from '#/features/model-input' +import { useShowDataStore } from '#/shared/lib/hooks/use-show-data' const ImageModelPage: NextPageWithLayout = () => { const { query } = useRouter() @@ -46,7 +47,7 @@ const ImageModelPage: NextPageWithLayout = () => { const { ios, desktop } = useThemeAndDevice(deviceType, deviceOs) - const { error, showError } = useShowData() + const { showMessage } = useShowDataStore() const router = useRouter() @@ -68,7 +69,7 @@ const ImageModelPage: NextPageWithLayout = () => { } = useImageBotPagination(deviceType) const { createImage, isComplete, createLoading } = useImageBotCreateImage( - showError, + showMessage, modelType, deviceType, setMessages, @@ -456,7 +457,6 @@ const ImageModelPage: NextPageWithLayout = () => { )} - > ) @@ -22,6 +22,7 @@ import { ERROR_MAPPING } from '#/pages/api/auth/constants' import OpenedEyeSvg from '#/assets/svg/opened-eye.svg?react' import ClosedEyeSvg from '#/assets/svg/closed-eye.svg?react' import * as Sentry from '@sentry/nextjs' +import { useShowDataStore } from '#/shared/lib/hooks/use-show-data' const Login: NextPageWithLayout = () => { const device = getDeviceType() @@ -34,7 +35,7 @@ const Login: NextPageWithLayout = () => { Object.entries(query).forEach(([key, value]) => setCookie(key, value)) }, []) - const { error, showError } = useShowData() + const { showMessage } = useShowDataStore() const [loading, setLoading] = React.useState(false) @@ -64,7 +65,7 @@ const Login: NextPageWithLayout = () => { (prev, curr) => ({ ...prev, [ERROR_MAPPING[curr]]: curr }), {} as Record )[resp.error] - showError(expectedError ?? 'Непредвиденная ошибка') + showMessage(expectedError ?? 'Непредвиденная ошибка') if (!expectedError) Sentry.captureMessage(resp.error) setLoading(false) @@ -74,7 +75,7 @@ const Login: NextPageWithLayout = () => { } const checkError: SubmitErrorHandler = (data) => { - showError(Object.values(data)[0].message || 'Неверные данные') + showMessage(Object.values(data)[0].message || 'Неверные данные') } const handleKeyDown = (event: any) => { @@ -359,7 +360,6 @@ const Login: NextPageWithLayout = () => { - @@ -11,13 +11,14 @@ import { Error, Input } from '#/shared' import { API_URL } from '#/shared/lib/constants/constants' import { getDeviceType, getRandomImage } from '#/shared/lib/helpers' import { NextPageWithLayout } from '#/pages/_app' +import { useShowDataStore } from '#/shared/lib/hooks/use-show-data' const Reset: NextPageWithLayout = () => { const device = getDeviceType() const [input_email, set_Email] = useState('') const [isSend, setIsSend] = useState(false) - const [isError, setIsError] = React.useState(false) + const {showMessage, } = useShowDataStore() const sendMail = () => { axios.post(API_URL + '/auth/update-pass', { email: input_email, @@ -29,8 +30,7 @@ const Reset: NextPageWithLayout = () => { } }) .catch(function (error) { - setIsError(true) - setTimeout(() => setIsError(false), 4000) + showMessage('Аккаунт с такой почтой не найден') }) } @@ -141,7 +141,6 @@ const Reset: NextPageWithLayout = () => { Восстановить аккаунт - ) : ( @@ -9,7 +9,7 @@ import { getAll, ResponseAllInfo } from '#/entities/user-account/model/user-type import { ResponseGetPersons } from '#/features/invite-person-in-business' import { useAppSelector } from '#/app/store/store' import styles from '#/app/styles/business.module.scss' -import { InputStyleDark, InputStyleLight } from '#/shared' +import { Error, InputStyleDark, InputStyleLight } from '#/shared' import { API_URL } from '#/shared/lib/constants' import { DateInput } from '#/shared/ui/date-input/date-input' import { Info } from '#/widgets/business-info' @@ -23,6 +23,7 @@ import { IpList } from '#/widgets/business-persons/ui/persons-list/ip-list' import { LogList } from '#/widgets/business-persons/ui/persons-list/log-list' import { SecurityList } from '#/widgets/business-persons/ui/persons-list/security-list' import ArrowUpOrDown from '#/widgets/top-bar-model/ui/arrow-up-or-down' +import { useShowDataStore } from '#/shared/lib/hooks/use-show-data' type ListType = 'personal' | 'security' @@ -35,6 +36,7 @@ export default function BusinessHost() { const [mailing, setMailing] = useState() const [toDate, setToDate] = useState('') const { data } = useSession() + const { message, isOpened } = useShowDataStore() useEffect(() => { getPersons(data?.access, true).then((res) => setSecurityList(res ? res?.reverse() : null)) @@ -63,13 +65,10 @@ export default function BusinessHost() { } const download = () => { - axios.get( - API_URL + `/auth/business-host/download-expenses?type=employees&from_date=${fromDate}&to_date=${toDate}`, - { - responseType: 'arraybuffer', - headers: { Authorization: `Bearer ${data?.access}` }, - } - ).then((res) => { + axios.get(API_URL + `/auth/business-host/download-expenses?type=employees&from_date=${fromDate}&to_date=${toDate}`, { + responseType: 'arraybuffer', + headers: { Authorization: `Bearer ${data?.access}` }, + }).then((res) => { const file = new Blob([res.data], { type: 'application/ms-excel;charset=utf-8', }) @@ -83,11 +82,7 @@ export default function BusinessHost() { } useEffect(() => { - axios.put( - API_URL + '/auth/business-host', - { token_cap_enabled: mailing }, - { headers: { Authorization: `Bearer ${data?.access}` } } - ) + axios.put(API_URL + '/auth/business-host', { token_cap_enabled: mailing }, { headers: { Authorization: `Bearer ${data?.access}` } }) }, [mailing]) return ( @@ -102,11 +97,7 @@ export default function BusinessHost() { Затраты - setShowPersons((prev) => !prev)} - className={styles2.arrow} - /> + setShowPersons((prev) => !prev)} className={styles2.arrow} /> {showPersons && ( @@ -150,20 +141,12 @@ const MailingBlock = ({ info, mailing }: { info: InfoBusiness | null | undefined const addMailingUser = async () => { if (newMail !== '') { - if (allMails && allMails?.length !== 0) { - setAllMails([...allMails, newMail]) - } else { - setAllMails([newMail]) - } + setAllMails((emails) => [...(emails ?? []), newMail]) } } const req = async (mailingArr: string[]) => { - await axios.put( - API_URL + '/auth/business-host', - { token_cap_emails: mailingArr }, - { headers: { Authorization: `Bearer ${data?.access}` } } - ) + await axios.put(API_URL + '/auth/business-host', { token_cap_emails: mailingArr }, { headers: { Authorization: `Bearer ${data?.access}` } }) } return ( @@ -190,11 +173,7 @@ const MailingBlock = ({ info, mailing }: { info: InfoBusiness | null | undefined placeholder={'Введите почту'} onChange={(e) => setNewMail(e.target.value)} fullWidth - sx={ - theme === 'light' - ? { ...InputStyleLight } - : { ...InputStyleDark } - } + sx={theme === 'light' ? { ...InputStyleLight } : { ...InputStyleDark }} /> @@ -10,3 +10,23 @@ export const translateEmailStatus = (status: AcceptanceStatus) => { return 'Приглашен' } } + +export const formatDateStatus = (dateString: string) => { + if (!dateString) return '' + + const date = new Date(dateString) + + const day = date.getDate().toString().padStart(2, '0') + const month = (date.getMonth() + 1).toString().padStart(2, '0') + const year = date.getFullYear().toString().slice(-2) + + return `${day}.${month}.${year}` +} + +export const formatEmail = (email: string | undefined) => { + if (email !== undefined && email.length > 24) { + return email.slice(0, 22) + '...' + } else { + return email + } +} @@ -11,7 +11,7 @@ import { useSession } from 'next-auth/react' import { ResponseGetPersons } from '#/features/invite-person-in-business' import { ResponseGetIpList } from '#/features/invite-person-in-business/model/types' -import { Search, Success, useShowData } from '#/shared' +import { Search, useShowData } from '#/shared' import styles from '#/widgets/business-models/ui/models-list/models-list.module.scss' import ArrowUpOrDown from '#/widgets/top-bar-model/ui/arrow-up-or-down' @@ -1,6 +1,138 @@ -.modalWrap { - text-align: center; - @media (max-width: 800px) { - width: 85vw; +.wrap { + margin-top: 40px; + font-size: 16px; + .title { + display: flex; + align-items: center; + .count { + margin-left: 5px; + font-weight: 600; + } + p { + color: var(--text-color-main); + font-size: 20px; + font-weight: 500; + } + .arrow { + margin-top: 3px; + cursor: pointer; + color: #9b9b9b; + } } + .listWrap { + margin-top: 15px; + border-radius: 15px; + padding: 20px; + background-color: var(--new-ui-main-color); + border: 1px solid var(--border-color2); + .search { + margin: 5px 15px; + } + } + .table { + background-color: inherit; + box-shadow: none; + color: var(--new-ui-table-cell-text); + .tableEmail { + color: var(--text-color-main); + font-weight: 600; + } + .change { + color: var(--air-color); + max-width: 100px; + button:hover { + text-decoration: underline; + } + } + .tableLimit { + cursor: pointer; + + color: var(--text-color-purple); + .change { + text-decoration: none; + margin-left: 5px; + } + &:hover { + text-decoration: underline; + } + } + td { + color: var(--new-ui-table-cell-text); + border-color: var(--border-color2); + vertical-align: top; + padding: 16px 12px; + } + th { + color: var(--new-ui-table-cell-text); + font-weight: 400; + border-bottom: none; + } + .tableSwitch { + div { + display: flex; + flex-direction: column; + justify-content: center; + max-width: 50px; + margin: auto; + } + } + } + + .title2 { + display: flex; + justify-content: space-between; + align-items: flex-end; + p { + color: var(--text-color-purple); + font-size: 16px; + cursor: pointer; + } + div { + @extend .title; + margin-top: 15px; + @media (max-width: 1000px) { + margin-top: 0px; + } + } + } + .zero_person { + width: 100%; + display: flex; + align-items: center; + justify-content: center; + p { + margin-top: 40px; + margin-bottom: 20px; + font-size: 17px; + color: var(--text-color-additional-two); + } + } + .titleModalLimit:global { + border: 1px solid red; + margin-bottom: 10px; + } +} + +.logsList { + margin-top: 15px; + display: flex; + justify-content: flex-start; + align-items: flex-end; + flex-wrap: wrap; + gap: 15px; +} +.hint { + margin-top: 15px; + font-size: 15px !important; + font-weight: 500 !important; + margin-bottom: 5px; +} + +.refreshStatusButton { + font-size: 14px; + display: flex; + gap: 1.5px; + align-items: center; + color: var(--air-color); + cursor: pointer; } @@ -8,18 +8,22 @@ import TableContainer from '@mui/material/TableContainer' import TableHead from '@mui/material/TableHead' import TableRow from '@mui/material/TableRow' import { useSession } from 'next-auth/react' +import Image from 'next/image' import { PersonInBusiness } from '#/features/business-security-download' import { LimitModal } from '#/features/change-limit' import { InviteModal, ResponseGetPersons, RoleSelect } from '#/features/invite-person-in-business' import { InviteRoles } from '#/features/invite-person-in-business/lib/constants' import { XMark } from '#/features/remove-person' -import { Search, Success, useShowData } from '#/shared' -import styles from '#/widgets/business-models/ui/models-list/models-list.module.scss' +import { Error, formatDate, Search, useShowData } from '#/shared' +import styles from './persons-list.module.scss' import { getPersons } from '#/widgets/business-persons/api/get-persons' import ArrowUpOrDown from '#/widgets/top-bar-model/ui/arrow-up-or-down' -import { translateEmailStatus } from '../../lib/lib' +import { translateEmailStatus, formatDateStatus, formatEmail } from '../../lib/lib' +import { getModalById, PLATE_CHANGE_PASSWORD, RESEND_INVATION_PASSWORD } from '#/features/modals' +import { ResponseGetBusinessGroups } from '#/features/invite-person-in-business/model/types' +import { useShowDataStore } from '#/shared/lib/hooks/use-show-data' export const PersonsList = ({ personsList, @@ -34,7 +38,10 @@ export const PersonsList = ({ const [searchPersons, setSearchPersons] = useState(persons) const [inviteModal, setInviteModal] = useState(false) const [currentPerson, setCurrentPerson] = useState(null) - // const { data } = useSession() + + const passChangeModal = getModalById(PLATE_CHANGE_PASSWORD) + const passResendModal = getModalById(RESEND_INVATION_PASSWORD) + const { data: session } = useSession() useEffect(() => { setPersons(personsList) @@ -49,14 +56,14 @@ export const PersonsList = ({ return [data] }) - showError('Пользователь успешно приглашен!') + showMessage('Пользователь успешно приглашен!') } const [search, setSearch] = useState('') const [showPersons, setShowPersons] = useState(true) - const { error: message, showError, isError } = useShowData() + const { showMessage } = useShowDataStore() const updateLimit = (limit: string, email?: string) => { setPersons((prev) => @@ -70,7 +77,7 @@ export const PersonsList = ({ }) ) setCurrentPerson(null) - showError(`Лимит пользователя ${email} успешно изменён!`) + showMessage(`Лимит пользователя ${email} успешно изменён!`) } useEffect(() => { @@ -99,11 +106,7 @@ export const PersonsList = ({ Сотрудники {persons?.length !== 0 && {persons?.length}} - setShowPersons((prev) => !prev)} - className={styles.arrow} - /> + setShowPersons((prev) => !prev)} className={styles.arrow} /> setInviteModal(true)}>Добавить сотрудника @@ -128,15 +131,17 @@ export const PersonsList = ({ Пользователь - Роль - Статус - Лимит токенов - - + Роль + Статус + Лимит токенов + + + - {searchPersons?.map((person) => { + {searchPersons?.map((person: ResponseGetPersons) => { + const statusPerson = translateEmailStatus(person.acceptance_status) return ( - {person.email} - - - {RoleSelect[person.account_type]} + {formatEmail(person.email)} - - {translateEmailStatus(person.acceptance_status)} + {RoleSelect[person.account_type]} + + + {statusPerson + ' '} + {statusPerson === 'Приглашен' ? formatDateStatus(person.created_at) : null} + + {statusPerson === 'Приглашен' ? ( + { + passResendModal.setState(true, { + email: person.email, + }) + }} + > + + Отправить повторно + + ) : null} - - {Math.floor(+person.token_limit)} + {Math.floor(+person.token_limit)} + + { + passChangeModal.setState(true, { + email: person.email, + }) + }} + > + Сменить пароль + - setCurrentPerson( - persons?.find( - (el) => el.email === person.email - ) || null - ) + setCurrentPerson(persons?.find((el) => el.email === person.email) || null) } className={styles.tableLimit} align='center' > Изменить - + + setNewPersons={(person: ResponseGetPersons | null) => setPersons( (prev) => - prev?.filter( - (el) => - el.email !== - persons.email - ) || null + prev?.filter((el) => el.email !== person?.email) || null ) } - email={person.email} + person={person || null} /> @@ -201,7 +220,6 @@ export const PersonsList = ({ )} )} - setInviteModal(false)} showNewPersons={showNewPerson} /> {currentPerson && ( (persons) const [inviteModal, setInviteModal] = useState(false) const [currentPerson, setCurrentPerson] = useState(null) - // const { data } = useSession() + + const passChangeModal = getModalById(PLATE_CHANGE_PASSWORD) + const passResendModal = getModalById(RESEND_INVATION_PASSWORD) useEffect(() => { setPersons(securityList) @@ -71,14 +77,14 @@ export const SecurityList = ({ return [data] }) - showError('Пользователь успешно приглашен!') + showMessage('Пользователь успешно приглашен!') } const [search, setSearch] = useState('') const [showPersons, setShowPersons] = useState(true) - const { error: message, showError, isError } = useShowData() + const { showMessage } = useShowDataStore() const { data } = useSession() const updateLimit = (limit: string, email?: string) => { @@ -93,7 +99,7 @@ export const SecurityList = ({ }) ) setCurrentPerson(null) - showError(`Лимит пользователя ${email} успешно изменён!`) + showMessage(`Лимит пользователя ${email} успешно изменён!`) } useEffect(() => { @@ -121,11 +127,7 @@ export const SecurityList = ({ Сотрудники безопасности {persons?.length !== 0 && {persons?.length}} - setShowPersons((prev) => !prev)} - className={styles.arrow} - /> + setShowPersons((prev) => !prev)} className={styles.arrow} /> setInviteModal(true)}>Добавить сотрудника @@ -150,15 +152,17 @@ export const SecurityList = ({ Пользователь - Роль - Статус - Лимит токенов - - + Роль + Статус + Лимит токенов + + + - {searchPersons?.map((person) => { + {searchPersons?.map((person: ResponseGetPersons) => { + const statusPerson = translateEmailStatus(person.acceptance_status) return ( - {person.email} - - - {RoleSelect[person.account_type]} + {formatEmail(person.email)} - - {translateEmailStatus(person.acceptance_status)} + {RoleSelect[person.account_type]} + + + {statusPerson + ' '} + {statusPerson === 'Приглашен' ? formatDateStatus(person.created_at) : null} + + {statusPerson === 'Приглашен' ? ( + { + passResendModal.setState(true, { + callback: () => { + console.log('Смена пароля') + }, + }) + }} + > + + Отправить повторно + + ) : null} - - {Math.floor(+person.token_limit)} + {Math.floor(+person.token_limit)} + + { + passChangeModal.setState(true, { + callback: () => { + console.log('Смена пароля') + }, + }) + }} + > + Сменить пароль + - setCurrentPerson( - persons?.find( - (el) => el.email === person.email - ) || null - ) + setCurrentPerson(persons?.find((el) => el.email === person.email) || null) } className={styles.tableLimit} align='center' > Изменить - - { - axios.put< - { - token_limit: string - role: string - }, - AxiosResponse - >( - API_URL + - `/auth/business-host/accounts/${person.email}`, - { - account_privileges: 'regular', - }, - { - headers: { - Authorization: `Bearer ${data?.access}`, - }, - } - ).then((res) => { - if (securityList) - setSecurityList( - securityList.filter( - (el) => - el.email !== - res.data.email - ) - ) - addList(res.data, 'personal') - }) - }} - src={'/x-mark.svg'} - width={15} - height={15} - alt={'Удалить'} + + + setPersons( + (prev) => + prev?.filter((el) => el.email !== persons?.email) || null + ) + } + person={person} /> @@ -246,12 +245,8 @@ export const SecurityList = ({ )} )} - - setInviteModal(false)} - showNewPersons={showNewPerson} - /> + + setInviteModal(false)} showNewPersons={showNewPerson} /> {currentPerson && ( = ({ open, onClose, showNewPersons }) => { const [role, setRole] = useState('Сотрудник безопасности') const { handleSubmit, register, reset, setValue, getValues } = useForm() - const { error, showError } = useShowData() + const { showMessage } = useShowDataStore() const [isLoading, setIsLoading] = useState(false) const [businessGroups, setBusinessGroups] = useState() const [currentGroup, setCurrentGroup] = useState('') @@ -286,7 +281,7 @@ export const InviteSecurityModal: FC = ({ open, onClose, showN const res = await invitePerson(role, data.limit, data.email, group && group[0], session?.access) if (res === null) { - showError('Что-то пошло не так') + showMessage('Что-то пошло не так') setIsLoading(false) return } @@ -298,7 +293,7 @@ export const InviteSecurityModal: FC = ({ open, onClose, showN showNewPersons(res) } const checkError: SubmitErrorHandler = (data) => { - showError(Object.values(data)[0].message || 'Неверные данные') + showMessage(Object.values(data)[0].message || 'Неверные данные') } const handleEmailChange = (event: any) => { @@ -324,11 +319,7 @@ export const InviteSecurityModal: FC = ({ open, onClose, showN {/* Поле с выбором роли сотрудника */} Выберите роль - setRole(e.target.value as InviteRoles)} - value={role} - > + setRole(e.target.value as InviteRoles)} value={role}> {/* Поле с выбором группы */} Выберите бизнес-группу @@ -364,7 +355,6 @@ export const InviteSecurityModal: FC = ({ open, onClose, showN )} - ) } @@ -21,16 +21,12 @@ import { SideMenu } from '#/widgets/side-menu' import { getDeviceType } from '#/shared/lib/helpers' import { LayoutProps } from '../types' import { change } from '#/features/pending' +import { ChangePasswordPlate } from '#/features/change-password-corp' +import { ResendInvationPlate } from '#/features/resend-invation-corp ' const freeRoutes = ['/login', '/register', '/reset', '/change-password'] -export const Layout: React.FC = ({ - children, - titlePage, - title = titlePage, - isLoader, - device = getDeviceType(), -}) => { +export const Layout: React.FC = ({ children, titlePage, title = titlePage, isLoader, device = getDeviceType() }) => { const { data: sessionData } = useSession() const appState = useAppSelector((state) => state) const dispatch = useAppDispatch() @@ -68,10 +64,7 @@ export const Layout: React.FC = ({ }, [data]) useEffect(() => { - if ( - (sessionData && !sessionStorage.getItem('firstRender')) || - (sessionData && !localStorage.getItem('global_settings')) - ) { + if ((sessionData && !sessionStorage.getItem('firstRender')) || (sessionData && !localStorage.getItem('global_settings'))) { dispatch(getUserAccountSettings(sessionData.access)) sessionStorage.setItem('firstRender', 'true') } @@ -96,22 +89,13 @@ export const Layout: React.FC = ({ targetDevice: device, targetType: 'sidemenu', }) - if (setting) - setting?.value?.sidemenu_state === 'opened' - ? setSidemenuDefaultOpen(true) - : setSidemenuDefaultOpen(false) + if (setting) setting?.value?.sidemenu_state === 'opened' ? setSidemenuDefaultOpen(true) : setSidemenuDefaultOpen(false) } }, [appState.settings.state]) if (userLoading || isLoader || status === 'loading') { return ( - + ) @@ -143,10 +127,7 @@ export const Layout: React.FC = ({ > {desktop ? ( - + = ({ )} + + + > ) @@ -5,7 +5,7 @@ import Image from 'next/image' import Link from 'next/link' import { useAppSelector } from '#/app/store/store' -import { ClientOnly, Success, TooltipCustom, useShowData } from '#/shared' +import { ClientOnly, Error, TooltipCustom, useShowData } from '#/shared' import styles from './image-messages-list.module.scss' import { createPortal } from 'react-dom' @@ -13,6 +13,7 @@ import { useMessages } from '../model/use-messages' import { ImageIcons } from './image-icons' import { ImageModal } from '#/features/image-modal' import { Message } from '#/entities/message' +import { useShowDataStore } from '#/shared/lib/hooks/use-show-data' interface MessagesList { device: 'mobile' | 'desktop' @@ -22,7 +23,7 @@ interface MessagesList { } export const ImageMessagesList: React.FC = memo(({ device, images, getMessagesPagination }) => { - const { error, showError, isError } = useShowData() + const { showMessage } = useShowDataStore() const [modal, setModal] = useState(false) const theme = useAppSelector((state) => state.theme.theme) @@ -45,7 +46,6 @@ export const ImageMessagesList: React.FC = memo(({ device, images, )} - { const user = useAppSelector((state) => state.user) const { push } = useRouter() const url = API_HOST + '/r/' + user.email - const { error, showError, isError } = useShowData() + const { showMessage } = useShowDataStore() + const { data } = useSession() const [referrals, setReferrals] = useState(null) @@ -55,17 +57,8 @@ export const Referral = ({ device }: IProps) => { }, [data?.access]) return ( - - + + { width={100} height={100} onClick={() => { - navigator.clipboard - .writeText(url) - .then(() => showError('Ссылка скопирована!')) + navigator.clipboard.writeText(url).then(() => showMessage('Ссылка скопирована!')) }} /> - + { Регистраций - - {referrals?.registrations_count || 0} - + {referrals?.registrations_count || 0} @@ -251,9 +235,7 @@ export const Referral = ({ device }: IProps) => { Бонусов - - {referrals?.accrued_bonuses_amount || 0} - + {referrals?.accrued_bonuses_amount || 0} @@ -302,9 +284,7 @@ export const Referral = ({ device }: IProps) => { whiteSpace: 'nowrap', }} > - {el.username.length > 20 - ? el.username.slice(0, 20) + '...' - : el.username} + {el.username.length > 20 ? el.username.slice(0, 20) + '...' : el.username} { )} - ) } @@ -18,6 +18,7 @@ import { getAudioList } from '#/widgets/whisper/api/getAudioList' import { Card } from '../card/card' import styles2 from './whisper.module.scss' +import { useShowDataStore } from '#/shared/lib/hooks/use-show-data' export interface WhisperResponse { audio_link: string @@ -64,7 +65,7 @@ export const WhisperWidget = () => { } catch (err) {} } - const { error, showError } = useShowData() + const { showMessage } = useShowDataStore() const transcript = async (e: any) => { if (!audio) { @@ -88,7 +89,7 @@ export const WhisperWidget = () => { setLoading(false) } catch (err) { setList([]) - showError(decodeError(err, 'Ошибка генерации')) + showMessage(decodeError(err, 'Ошибка генерации')) setLoading(false) } } @@ -151,9 +152,7 @@ export const WhisperWidget = () => { height={27} width={27} style={{ marginTop: 4, cursor: 'pointer' }} - src={`/svg/chatgpt/send_message${ - theme === 'light' ? '' : '_dark' - }.svg`} + src={`/svg/chatgpt/send_message${theme === 'light' ? '' : '_dark'}.svg`} alt={''} /> ) : ( @@ -175,9 +174,7 @@ export const WhisperWidget = () => { {list?.length === 0 ? ( - - У вас пока нет расшифрованных записей - + У вас пока нет расшифрованных записей ) : ( <> @@ -197,7 +194,6 @@ export const WhisperWidget = () => { > )} - > ) } @@ -5,6 +5,6 @@ "trailingComma": "es5", "jsxBracketSameLine": false, "semi": false, - "printWidth": 100, + "printWidth": 150, "jsxSingleQuote": true } @@ -55,7 +55,7 @@ "react-cookie": "^4.1.1", "react-dom": "18.2.0", "react-draft-wysiwyg": "^1.15.0", - "react-hook-form": "^7.43.9", + "react-hook-form": "^7.54.2", "react-i18next": "^13.0.3", "react-markdown": "^8.0.7", "react-redux": "^8.0.5", @@ -66,7 +66,8 @@ "sharp": "^0.34.1", "styled-components": "^5.3.9", "swiper": "^11.2.1", - "typescript": "5.1.3" + "typescript": "5.1.3", + "zustand": "^5.0.3" }, "devDependencies": { "@svgr/webpack": "^8.1.0", @@ -11044,18 +11045,19 @@ } }, "node_modules/react-hook-form": { - "version": "7.48.2", - "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.48.2.tgz", - "integrity": "sha512-H0T2InFQb1hX7qKtDIZmvpU1Xfn/bdahWBN1fH19gSe4bBEqTfmlr7H3XWTaVtiK4/tpPaI1F3355GPMZYge+A==", + "version": "7.54.2", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.54.2.tgz", + "integrity": "sha512-eHpAUgUjWbZocoQYUHposymRb4ZP6d0uwUnooL2uOybA9/3tPUvoAKqEWK1WaSiTxxOfTpffNZP7QwlnM3/gEg==", + "license": "MIT", "engines": { - "node": ">=12.22.0" + "node": ">=18.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/react-hook-form" }, "peerDependencies": { - "react": "^16.8.0 || ^17 || ^18" + "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "node_modules/react-i18next": { @@ -13340,6 +13342,35 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/zustand": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.3.tgz", + "integrity": "sha512-14fwWQtU3pH4dE0dOpdMiWjddcH+QzKIgk1cl8epwSE7yag43k/AD/m4L6+K7DytAOr9gGBe3/EXj9g7cdostg==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + }, "node_modules/zwitch": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", @@ -76,7 +76,7 @@ "react-cookie": "^4.1.1", "react-dom": "18.2.0", "react-draft-wysiwyg": "^1.15.0", - "react-hook-form": "^7.43.9", + "react-hook-form": "^7.54.2", "react-i18next": "^13.0.3", "react-markdown": "^8.0.7", "react-redux": "^8.0.5", @@ -87,7 +87,8 @@ "sharp": "^0.34.1", "styled-components": "^5.3.9", "swiper": "^11.2.1", - "typescript": "5.1.3" + "typescript": "5.1.3", + "zustand": "^5.0.3" }, "devDependencies": { "@svgr/webpack": "^8.1.0",
Смена пароля
{title}
Переотправить приглашение
При переотправке приглашения у сотрудника будет заменен отправленный пароль.
{error ? error?.message : ''}
+ {error ? error?.message : ""} +