@@ -1,9 +1,7 @@ -import { store } from '#/app/store/store' import { render } from '@testing-library/react' -import { Provider } from 'react-redux' export function jestRender(component: React.ReactElement) { - return render({component}) + return render(component) } @@ -8,9 +8,11 @@ import { useRouter } from 'next/router' import { signOut, useSession } from 'next-auth/react' import { useTranslation } from 'next-i18next' -import { change } from '#/entities/theme' import styles from '#/app/layout/styles/styles.module.css' -import { useAppDispatch, useAppSelector } from '#/app/store/store' + +import { useBalanceStore } from '#/entities/balance/model/use-balance-store' +import { useThemeStore } from '#/entities/theme/model/use-theme-store' +import { useUserStore } from '#/entities/user-account' import { InputStyleDark, InputStyleLight, TooltipCustom } from '#/shared' import { ShortModel } from '#/shared/api/models/models' import { API_URL } from '#/shared/lib/constants' @@ -29,36 +31,19 @@ const languages: Record = { English: 'en', } -async function getModels(token?: string): Promise { - try { - const { data } = await axios.get(API_URL + '/ml_models/', { - headers: { - Authorization: `Bearer ${token}`, - }, - }) - - return data - } catch (e) { - return [] - } -} - const InfoBar: React.FC = ({ device }) => { - const theme = useAppSelector((state) => state.theme.theme) + const theme = useThemeStore((state) => state.theme) + const { change: changeTheme } = useThemeStore() - const balance = useAppSelector((state) => state.balance.balance) + const balance = useBalanceStore((state) => state.balance) - const show_balance = useAppSelector((state) => state.user.show_balance) + const { getUser } = useUserStore() const { pathname, replace } = useRouter() const { data } = useSession() - const { email, first_name, last_name, profile_picture_link, account_type } = useAppSelector( - (state) => state.user - ) - - const dispatch = useAppDispatch() + const { email, first_name, last_name, profile_picture_link, account_type, show_balance } = getUser() const [anchorEl, setAnchorEl] = React.useState(null) @@ -119,14 +104,9 @@ const InfoBar: React.FC = ({ device }) => { > - Мы уже работаем над этой проблемой. Попробуйте перезайти в - аккаунт + Мы уже работаем над этой проблемой. Попробуйте перезайти в аккаунт - @@ -134,7 +114,7 @@ const InfoBar: React.FC = ({ device }) => { dispatch(change(null))} + onClick={() => changeTheme()} style={{ cursor: 'pointer', marginLeft: '30px' }} src={'/sleep.svg'} width={20} @@ -152,10 +132,7 @@ const InfoBar: React.FC = ({ device }) => { {show_balance && ( - + {declineToken(balance.toString())} @@ -183,8 +160,7 @@ const InfoBar: React.FC = ({ device }) => { sx={{ marginTop: '6px' }} PaperProps={{ style: { - backgroundColor: - theme === 'dark' ? '#151518' : 'white', + backgroundColor: theme === 'dark' ? '#151518' : 'white', borderRadius: '13px', boxShadow: 'none', }, @@ -203,12 +179,10 @@ const InfoBar: React.FC = ({ device }) => { }} > - + - router.push('/account?scope=setting') - } + onClick={() => router.push('/account/settings')} > = ({ device }) => { - + - router.push( - '/account?scope=business' - ) - } + onClick={() => router.push('/account/business')} > {''} = ({ device }) => { {account_type === 'regular' && ( - + - router.push( - '/account?scope=referral' - ) + router.push('/account/referral') } > {''} = ({ device }) => { )} - + - router.push( - '/account?scope=subscribe' - ) - } + onClick={() => router.push('/account/subscribe')} > { - if (Object.keys(action.payload).length === 0) { - state.params = {} - return - } - if (Object.keys(state.params).length === 0) { - state.params = action.payload - return - } else { - state.params = { ...state.params, ...action.payload } - return - } - }, - }, -}) - -export const { setParams } = paramsStore.actions - -export default paramsStore.reducer @@ -1,37 +0,0 @@ -import { createSlice, PayloadAction } from '@reduxjs/toolkit' - -const defaultDuration = 4000 - -type Notification = { - id: number - text: string - type: 'error' | 'success' - duration?: number -} - -export interface State { - notifications: Notification[] -} - -const initialState: State = { - notifications: [], -} - -export const notificationSlice = createSlice({ - name: 'notificaionSlice', - initialState, - reducers: { - add: (state, action: PayloadAction) => { - const { duration, id } = action.payload - - state.notifications = [...state.notifications, action.payload] - setTimeout(() => { - state.notifications = state.notifications.filter((el) => el.id !== id) - }, duration ?? defaultDuration) - }, - }, -}) - -export const { add } = notificationSlice.actions - -export default notificationSlice.reducer @@ -1,36 +0,0 @@ -import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux' -import { configureStore } from '@reduxjs/toolkit' - -import { balanceSlice } from '#/entities/balance' -import { themeSlice } from '#/entities/theme' -import { userSlice } from '#/entities/user-account' -import { settingsSlice } from '#/entities/user-account/model/settings' -import { stepperSlice } from '#/features/register-business' -import { copySlice } from '#/features/use-copy/copy-slice' -import { paramsStore } from '#/app/store/model-parametres-store' - -import { notificationSlice } from './notification-slice' -import { pendingSlice } from '#/features/pending' - -export const store = configureStore({ - reducer: { - theme: themeSlice.reducer, - balance: balanceSlice.reducer, - stepper: stepperSlice.reducer, - user: userSlice.reducer, - notification: notificationSlice.reducer, - params: paramsStore.reducer, - settings: settingsSlice.reducer, - copy: copySlice.reducer, - loading: pendingSlice.reducer, - }, -}) - -export type RootState = ReturnType - -export type AppDispatch = typeof store.dispatch - -export type loadingThunk = 'idle' | 'pending' | 'succeeded' | 'failed' - -export const useAppDispatch: () => AppDispatch = useDispatch -export const useAppSelector: TypedUseSelectorHook = useSelector @@ -0,0 +1,36 @@ +import { create } from 'zustand' + +type Notification = { + id: number + text: string + type: 'error' | 'success' + duration?: number +} + +export interface NotificationStore { + notifications: Notification[] + add: (notification: Notification) => void +} + +export const useNotificationStore = create((set, get) => { + const defaultDuration = 4000 + + function add(notification: Notification) { + const { duration, id } = notification + + set((state) => ({ + notifications: [...state.notifications, notification], + })) + + setTimeout(() => { + set((state) => ({ + notifications: state.notifications.filter((el) => el.id !== id), + })) + }, duration ?? defaultDuration) + } + + return { + notifications: [], + add, + } +}) @@ -0,0 +1,29 @@ +import { create } from 'zustand' + +export interface ParamsStore { + params: Record + setParams: (params: Record) => void +} + +export const useParamsStore = create((set, get) => { + function setParams(newParams: Record) { + const currentParams = get().params + + if (Object.keys(newParams).length === 0) { + set({ params: {} }) + return + } + + if (Object.keys(currentParams).length === 0) { + set({ params: newParams }) + return + } + + set({ params: { ...currentParams, ...newParams } }) + } + + return { + params: {}, + setParams, + } +}) @@ -0,0 +1,9 @@ +// Zustand stores - замена Redux Toolkit +export { useThemeStore } from '#/entities/theme/model/use-theme-store' +export { useBalanceStore } from '#/entities/balance/model/use-balance-store' +export { useNotificationStore } from './use-notification-store' +export { useParamsStore } from './use-params-store' +export { usePendingStore } from '#/features/pending/model/use-pending-store' +export { useSettingsStore } from '#/entities/user-account/model/use-settings-store' +export { useUserReferralStore } from '#/entities/user-account/model/use-user-referral-store' +export { useUserStore } from '#/entities/user-account/model/use-user-store' @@ -12,8 +12,8 @@ } } .main { - width: 58vw; - margin: 0 auto; + width: calc(100% - 30px); + // margin: 0 auto; @media (max-width: 1000px) { width: 92vw; @@ -1,15 +0,0 @@ -import { Template } from '#/domains/copywrite/proxy/types/template' - -export const emptyTemplate: Template = { - id: 1000, - title: 'Пустой шаблон', - content: '', - keywords: [], - tov: '', - language: '', - theme: '', - resources_urls: [], - picture: '123', - target_audience: '', - description: '', -} @@ -1,6 +0,0 @@ -import { ContentState, EditorState } from 'draft-js' - -export const toEditorState = (text: string) => { - const newContentState = ContentState.createFromText(text) - return EditorState.createWithContent(newContentState) -} @@ -1,13 +0,0 @@ -export interface Template { - id: number - title: string - description: string - picture: string - theme: string - content: string - target_audience: string - resources_urls: string[] - keywords: string[] - tov: string - language: string -} @@ -1,34 +0,0 @@ -import axios from 'axios' - -import { Template } from '#/domains/copywrite/proxy/types/template' -import { API_URL } from '#/shared/lib/constants' -import { Message } from '#/shared/lib/types/model' - -export class CopywriteProxy { - token?: string - - constructor(token: string | undefined) { - this.token = token - } - - static async getGeneration(token?: string): Promise { - const { data } = await axios.get(API_URL + '/copywrite/', { - headers: { Authorization: `Bearer ${token}` }, - }) - return data - } - - static async getTemplates(token?: string): Promise { - const { data } = await axios.get(API_URL + '/copywrite/templates/', { - headers: { Authorization: `Bearer ${token}` }, - }) - return data - } - - static async createTemplates(token?: string): Promise { - const { data } = await axios.post(API_URL + '/copywrite/templates/', { - headers: { Authorization: `Bearer ${token}` }, - }) - return data - } -} @@ -1,44 +0,0 @@ -import React from 'react' -import { Typography } from '@mui/material' - -import { Input, Slider } from '#/shared' - -import { FiltersProps } from './types' - -export function Filters({ - strength, - setStrength, - upscale, - setUpscale, - negative_prompt, - num_inference_steps, - guidance_scale, - setGuidanceScale, - setNegative_prompt, - setSteps, -}: FiltersProps) { - return ( - <> - - - - - Запрос для исключения из генерации - - - ) -} @@ -1,17 +0,0 @@ -import { ChangeEvent } from 'react' - -export interface Setting { - strength: number - upscale: number - negative_prompt: string - num_inference_steps: number - guidance_scale: number -} - -export interface FiltersProps extends Setting { - setStrength: (e: Event, cur: number | number[]) => void - setUpscale: (e: Event, cur: number | number[]) => void - setGuidanceScale: (e: Event, cur: number | number[]) => void - setNegative_prompt: (e: ChangeEvent) => void - setSteps: (e: Event, cur: number | number[]) => void -} @@ -1,57 +0,0 @@ -import React from 'react' -import { Stack, Typography } from '@mui/material' - -import { Input, Slider } from '#/shared' -import { SelectUI } from '#/shared/ui/select' - -import { Filters } from './types' - -const sizes = [128, 256, 384, 448, 512, 576, 640, 704, 768, 832, 896, 960, 1024] - -export function EpicPhotoFilters({ - guidance_scale, - height, - negative_prompt, - num_inference_steps, - num_outputs, - setGuidance_scale, - setHeight, - setNegative_prompt, - setNum_inference_steps, - setNum_outputs, - setWidth, - width, -}: Filters) { - return ( - <> - - - - - - Запрос для исключения из генерации - - - ) -} @@ -1,2 +0,0 @@ -export * from './epic-photo-filters' -export * from './types' @@ -1,20 +0,0 @@ -import { ChangeEvent, ChangeEventHandler } from 'react' -import { SelectChangeEvent } from '@mui/material' - -export interface Setting { - num_outputs: number - negative_prompt: string - width: number - height: number - num_inference_steps: number - guidance_scale: number -} - -export interface Filters extends Setting { - setWidth: (e: SelectChangeEvent) => void - setHeight: (e: SelectChangeEvent) => void - setNum_outputs: (e: Event, cur: number | number[]) => void - setNum_inference_steps: (e: Event, cur: number | number[]) => void - setGuidance_scale: (e: Event, cur: number | number[]) => void - setNegative_prompt: (e: ChangeEvent) => void -} @@ -1,2 +0,0 @@ -export * from './kandinsky-filters' -export * from './types' @@ -1,83 +0,0 @@ -import React from 'react' -import { Box, Typography } from '@mui/material' - -import { Input, Slider, SwitchCustom } from '#/shared' -import { SelectUI } from '#/shared/ui/select' - -import { Filters } from './types' - -export function KandinskyFilters({ - height, - isTranslate, - negativePrompt, - num_outputs, - setHeight, - setNegative_prompt, - setNumber, - setSteps, - setWidth, - steps, - width, - setIsTranslate, -}: Filters) { - return ( - <> - Настройки - - - - - - - - - - - - - - - - Переводить запрос - - - - - - - Запрос для исключения из генерации - - - - - ) -} @@ -1,20 +0,0 @@ -import { ChangeEvent } from 'react' -import { SelectChangeEvent } from '@mui/material' - -export interface Setting { - steps: number - num_outputs: number - width: number - height: number - isTranslate: boolean - negativePrompt: string -} - -export interface Filters extends Setting { - setWidth: (e: SelectChangeEvent) => void - setHeight: (e: SelectChangeEvent) => void - setSteps: (e: Event, cur: number | number[]) => void - setNumber: (e: Event, cur: number | number[]) => void - setNegative_prompt: (e: ChangeEvent) => void - setIsTranslate: () => void -} @@ -1,47 +0,0 @@ -import React from 'react' -import { Typography } from '@mui/material' - -import { Input, Slider } from '#/shared' -import { SelectUI } from '#/shared/ui/select' - -import { FiltersProps } from './types' - -const sizes = [384, 512, 576, 640, 704, 768] - -export function Filters({ - height, - negative_prompt, - num_inference_steps, - num_outputs, - setHeight, - setNegative_prompt, - setNumOutputs, - setSteps, - setWidth, - width, -}: FiltersProps) { - return ( - <> - - - - - Запрос для исключения из генерации - - - ) -} @@ -1,2 +0,0 @@ -export * from './filters' -export * from './types' @@ -1,18 +0,0 @@ -import { ChangeEvent } from 'react' -import { SelectChangeEvent } from '@mui/material' - -export interface Setting { - width: number - height: number - num_outputs: number - negative_prompt: string - num_inference_steps: number -} - -export interface FiltersProps extends Setting { - setWidth: (e: SelectChangeEvent) => void - setHeight: (e: SelectChangeEvent) => void - setNumOutputs: (e: Event, cur: number | number[]) => void - setSteps: (e: Event, cur: number | number[]) => void - setNegative_prompt: (e: ChangeEvent) => void -} @@ -1,16 +0,0 @@ -import { ShortModel } from '#/entities/model-entity' -import { API_URL } from '#/shared/lib/constants' -import axios from 'axios' - -export async function getAudio(token?: string): Promise { - try { - const { data } = await axios.get(API_URL + '/ml_models/?category=audio', { - headers: { - Authorization: `Bearer ${token}`, - }, - }) - return data - } catch (e) { - return [] - } -} @@ -1 +0,0 @@ -export * from './audio-model-api' \ No newline at end of file @@ -1 +0,0 @@ -export * from './api' \ No newline at end of file @@ -15,3 +15,4 @@ export const getBalance = async (token: string): Promise => { return 0 } } + @@ -1,26 +0,0 @@ -import { createAsyncThunk, createSlice } from '@reduxjs/toolkit' - -import { getBalance } from '../api/get-balance' -export const getUserBalance = createAsyncThunk('users/fetchByIdStatus', async (token: string | undefined | null) => { - if (!token) { - return 0 - } - return await getBalance(token) -}) - -const initialState = { - balance: 0, -} - -export const balanceSlice = createSlice({ - name: 'balance', - initialState, - reducers: {}, - extraReducers: (builder) => { - builder.addCase(getUserBalance.fulfilled, (state, action) => { - state.balance = action.payload - }) - }, -}) - -export default balanceSlice.reducer @@ -0,0 +1,33 @@ +import { create } from 'zustand' + +import { getBalance } from '../api/balance.routes' + +export interface BalanceStore { + balance: number + loading: boolean + getUserBalance: (token: string | undefined | null) => Promise +} + +export const useBalanceStore = create((set, get) => { + async function getUserBalance(token: string | undefined | null) { + if (!token) { + set({ balance: 0 }) + return + } + + try { + set({ loading: true }) + const balance = await getBalance(token) + set({ balance, loading: false }) + } catch (error) { + set({ loading: false }) + console.error('Error fetching balance:', error) + } + } + + return { + balance: 0, + loading: false, + getUserBalance, + } +}) @@ -9,11 +9,15 @@ export async function postImageMessage(uid: string, dto: MessageSend) { return await api.post(`/media/image/${uid}`, objectToFormdata(dto)) } -export async function getImagesBySlug(slug: string, token: string, offset?: number, limit = 10) { - return await axios.get(API_URL + `/media/image/${slug}?limit=${limit}&offset=${offset}`, { - validateStatus: (status) => status < 500, - headers: { - Authorization: `Bearer ${token}`, - }, - }) +// export async function getImagesBySlug(slug: string, token: string, offset?: number, limit = 10) { +// return await axios.get(API_URL + `/media/image/${slug}?limit=${limit}&offset=${offset}`, { +// validateStatus: (status) => status < 500, +// headers: { +// Authorization: `Bearer ${token}`, +// }, +// }) +// } + +export async function getImagesBySlug(slug: string, offset?: number, limit = 10) { + return await api.get(`/media/image/${slug}`, { params: { offset, limit } }) } @@ -3,7 +3,7 @@ import { Message, MessageSend } from '../types' import { makePrivateRequest } from '#/shared/api' import { postMessage } from '../api' import { getBlobFromUrl } from '#/shared' -import { useAppSelector } from '#/app/store/store' +import { useParamsStore } from '#/app/store/use-params-store' import { useChatBot } from '#/entities/model-entity' import { useShowDataStore } from '#/shared/lib/hooks/use-show-data' import { useChatBotMessages } from './chat-bot-messages.store' @@ -12,7 +12,7 @@ import { useModelInputStore } from '#/features/model-input/model' import { useChatWindowContext } from '#/widgets/chat-window/model' export function useUserMessageActions(message: Message) { - const includeParams = useAppSelector((state) => state.params.params) + const includeParams = useParamsStore((state) => state.params) const { showMessage } = useShowDataStore() @@ -1,4 +1,4 @@ -import { useAppSelector } from '#/app/store/store' + import { TooltipCustom, c } from '#/shared' import { Grow, Box } from '@mui/material' import { Message } from '../types' @@ -1,14 +1,15 @@ import React, { useMemo } from 'react' import Image from 'next/image' import Link from 'next/link' -import LockSvg from '#/assets/svg/lock.svg?react' -import BlockedSvg from '#/assets/svg/blocked.svg?react' import styles from './card-chat.module.scss' -import { c } from '#/shared/lib/helpers' + +import BlockedSvg from '#/assets/svg/blocked.svg?react' +import LockSvg from '#/assets/svg/lock.svg?react' import { ShortModel } from '#/entities/model-entity' -import { SvgIcon } from '#/shared/ui/svg' +import { c } from '#/shared/lib/helpers' import { CommonButton } from '#/shared/ui/button' +import { SvgIcon } from '#/shared/ui/svg' export interface ChatModelCardProps extends ShortModel { accessed_models: string[] | null @@ -25,7 +26,7 @@ export function ChatModelCard({ }: ChatModelCardProps) { const link = useMemo(() => { return accessed_models && !accessed_models.includes(slug) - ? '/account?scope=subscribe' + ? '/account/subscribe' : `chat-bot/${slug}` }, [accessed_models]) @@ -53,9 +54,7 @@ export function ChatModelCard({ {!enabled && (
- - Модель недоступна - + Модель недоступна
)} {accessed_models && !accessed_models.includes(slug) && ( @@ -2,15 +2,16 @@ import React, { useEffect, useMemo } from 'react' import { Avatar, Box, Button, Typography } from '@mui/material' import Image from 'next/image' import Link from 'next/link' -import LockSvg from '#/assets/svg/lock.svg?react' -import BlockedSvg from '#/assets/svg/blocked.svg?react' import styles from './card.module.scss' -import { useThemeAndDevice } from '#/shared/lib/hooks' -import { c } from '#/shared/lib/helpers' + +import BlockedSvg from '#/assets/svg/blocked.svg?react' +import LockSvg from '#/assets/svg/lock.svg?react' import { ShortModel } from '#/entities/model-entity' -import { SvgIcon } from '#/shared/ui/svg' +import { c } from '#/shared/lib/helpers' +import { useThemeAndDevice } from '#/shared/lib/hooks' import { CommonButton } from '#/shared/ui/button' +import { SvgIcon } from '#/shared/ui/svg' export interface ImageModelCardProps extends ShortModel { accessed_models: string[] | null @@ -26,9 +27,7 @@ export function ImageModelCard({ tags, }: ImageModelCardProps) { const link = useMemo(() => { - return accessed_models && !accessed_models.includes(slug) - ? '/account?scope=subscribe' - : `images/${slug}` + return accessed_models && !accessed_models.includes(slug) ? '/account/subscribe' : `images/${slug}` }, [accessed_models]) return ( @@ -1,29 +0,0 @@ -import { createSlice } from '@reduxjs/toolkit' - -export interface Theme { - theme: 'light' | 'dark' -} - -const initialState: Theme = { - theme: 'dark', -} - -export const themeSlice = createSlice({ - name: 'themeSlice', - initialState, - reducers: { - change: (state, action) => { - if (action.payload) { - state.theme = action.payload - localStorage.setItem('theme', state.theme) - return - } - state.theme = state.theme === 'dark' ? 'light' : 'dark' - localStorage.setItem('theme', state.theme) - }, - }, -}) - -export const { change } = themeSlice.actions - -export default themeSlice.reducer @@ -0,0 +1,22 @@ +import { create } from 'zustand' + +export interface ThemeStore { + theme: 'light' | 'dark' + change: (theme?: 'light' | 'dark') => void +} + +export const useThemeStore = create((set, get) => { + const savedTheme = typeof window !== 'undefined' ? localStorage.getItem('theme') as 'light' | 'dark' : 'dark' + + function change(theme?: 'light' | 'dark') { + const currentTheme = get().theme + const newTheme = theme || (currentTheme === 'dark' ? 'light' : 'dark') + set({ theme: newTheme }) + localStorage.setItem('theme', newTheme) + } + + return { + theme: savedTheme || 'dark', + change, + } +}) @@ -1,15 +1,14 @@ import React from 'react' -import { change } from '#/entities/theme' -import { useAppDispatch, useAppSelector } from '#/app/store/store' +import { useThemeStore } from '#/entities/theme/model/use-theme-store' export const useTheme = () => { - const theme = useAppSelector((state) => state.theme.theme) + const theme = useThemeStore((state) => state.theme) + - const dispatch = useAppDispatch() React.useEffect(() => { - dispatch(change(localStorage.getItem('theme') || 'light')) + // TODO: Add theme change to Zustand store document.documentElement.dataset.theme = theme }, [theme]) } @@ -1,2 +1,2 @@ -export { change, themeSlice } from './model/theme' export { useTheme } from './model/use-theme' +export { useThemeStore } from './model/use-theme-store' \ No newline at end of file @@ -1,25 +0,0 @@ -import axios, { AxiosResponse } from 'axios' - -import { API_URL } from '#/shared/lib/constants' - -import { AccountType } from '../model/types' -export const getAccountType = async (token: string | null | undefined): Promise => { - if (!token) { - return 'regular' - } - - try { - const { data } = await axios.get>( - API_URL + '/auth/account-type', - { - headers: { - Authorization: `Bearer ${token}`, - }, - } - ) - - return data.status - } catch (err) { - return 'regular' - } -} @@ -1,19 +0,0 @@ -import axios from 'axios' - -import { API_URL } from '#/shared/lib/constants' - -import { IUserSetting } from '../model/types' - -export const getUserSettings = async (token: string): Promise => { - try { - const { data } = await axios.get(API_URL + '/api/users/settings/', { - headers: { - Authorization: `Bearer ${token}`, - }, - }) - - return data - } catch (err) { - return [] - } -} @@ -1 +1,2 @@ -export * from './settings.routes' \ No newline at end of file +export * from './settings.routes' +export * from './user.routes' \ No newline at end of file @@ -0,0 +1,10 @@ +import { api } from '#/shared/api' +import { UserDTO } from '../types' + +export function getUserApi() { + return api.get('/auth/me') +} + +export function getUserServer(token: string) { + return api.get('/auth/me', { headers: { Authorization: `Bearer ${token}` } }) +} @@ -1,2 +1,4 @@ export * from './settings-context' -export * from './use-global-settings' \ No newline at end of file +export * from './use-global-settings' +export * from './use-user-store' +export * from './use-user' \ No newline at end of file @@ -1,9 +1,15 @@ import { createUseContext } from '#/shared' -import { createContext } from 'react' +import { createContext, useContext } from 'react' import { useGlobalSettings } from './use-global-settings' export const UserSettingsContext = createContext | null>(null) -export const useUserSettingsContext = createUseContext(UserSettingsContext) +export const useUserSettingsContext = () => { + const contextValue = useContext(UserSettingsContext) + if (!contextValue) { + throw new Error("useContext must be inside a Provider with a value") + } + return contextValue + } export const UserSettingsContextProvider = UserSettingsContext.Provider @@ -1,93 +0,0 @@ -import { createAsyncThunk, createSlice } from '@reduxjs/toolkit' - -import { addUserSettings } from '../api/add-user-settings' -import { getUserSettings } from '../api/get-user-settings' -import { updateUserSettings } from '../api/update-user-settings' -import { getUpdatedSettingsLocal } from '../lib/helpers/update-setting-local' - -import { IUserSetting, SettingValueType } from './types' -import { useLocalStorageSave } from '#/shared/lib/helpers/local-storage-helper' - -interface IAddSettings { - token: string | undefined | null - setting: Omit -} - -export const getUserAccountSettings = createAsyncThunk( - 'users/getSettings', - async (token: string | undefined | null) => { - if (!token) { - return initialState.state - } - return await getUserSettings(token) - } -) - -export const addUserAccountSettings = createAsyncThunk( - 'users/addSettings', - async ({ token, setting }: IAddSettings) => { - if (!token) { - return - } - return await addUserSettings(token, setting) - } -) - -export const updateUserAccountSettings = createAsyncThunk( - 'users/updateSettings', - async ({ - token, - id, - value, - settings, - }: { - token: string | undefined | null - id: string - value: SettingValueType - settings: IUserSetting[] | null - }) => { - if (!token) { - return initialState.state - } - if (!settings) { - return initialState.state - } - return await updateUserSettings(token, id, value, settings) - } -) - -const initialState: { state: IUserSetting[] | null } = { - state: null, -} - -export const settingsSlice = createSlice({ - name: 'balance', - initialState, - reducers: { - setSettings: (state, action) => { - state.state = action.payload - }, - }, - extraReducers: (builder) => { - builder.addCase(getUserAccountSettings.fulfilled, (state, action) => { - state.state = action.payload - localStorage.setItem('global_settings', JSON.stringify(action.payload)) - }) - builder.addCase(addUserAccountSettings.fulfilled, (state, action) => { - if (state.state === null) state.state = [] - if (action.payload) { - state.state = [...state.state, action.payload] - localStorage.setItem('global_settings', JSON.stringify([...state.state, action.payload])) - } - }) - - builder.addCase(updateUserAccountSettings.fulfilled, (state, action) => { - if (action.payload) { - state.state = action.payload - localStorage.setItem('global_settings', JSON.stringify(action.payload)) - } - }) - }, -}) - -export const { setSettings } = settingsSlice.actions @@ -0,0 +1,105 @@ +import { create } from 'zustand' + +import { addUserSettings } from '../api/add-user-settings' +import { updateUserSettings } from '../api/update-user-settings' + +import { IUserSetting, SettingValueType } from './types' + +interface IAddSettings { + token: string | undefined | null + setting: Omit +} + +export interface SettingsStore { + settings: IUserSetting[] + loading: boolean + getUserAccountSettings: (token: string | undefined | null) => Promise + addUserAccountSettings: (params: IAddSettings) => Promise + updateUserAccountSettings: (params: { + token: string | undefined | null + id: string + value: SettingValueType + settings: IUserSetting[] + }) => Promise + setSettings: (settings: IUserSetting[]) => void +} + +export const useSettingsStore = create((set, get) => { + async function getUserAccountSettings(token: string | undefined | null) { + if (!token) { + set({ settings: [] }) + return + } + + try { + set({ loading: true }) + + const settings: IUserSetting[] = [] + set({ settings, loading: false }) + localStorage.setItem('global_settings', JSON.stringify(settings)) + } catch (error) { + set({ loading: false }) + console.error('Error fetching settings:', error) + } + } + + async function addUserAccountSettings({ token, setting }: IAddSettings) { + if (!token) return + + try { + set({ loading: true }) + const newSetting = await addUserSettings(token, setting) + if (newSetting) { + const currentSettings = get().settings || [] + const updatedSettings = [...currentSettings, newSetting] + set({ settings: updatedSettings, loading: false }) + localStorage.setItem('global_settings', JSON.stringify(updatedSettings)) + } + } catch (error) { + set({ loading: false }) + console.error('Error adding settings:', error) + } + } + + async function updateUserAccountSettings({ + token, + id, + value, + settings, + }: { + token: string | undefined | null + id: string + value: SettingValueType + settings: IUserSetting[] + }) { + if (!token || !settings.length) { + set({ settings: [] }) + return + } + + try { + set({ loading: true }) + const updatedSettings = await updateUserSettings(token, id, value, settings) + if (updatedSettings) { + set({ settings: updatedSettings, loading: false }) + localStorage.setItem('global_settings', JSON.stringify(updatedSettings)) + } + } catch (error) { + set({ loading: false }) + console.error('Error updating settings:', error) + } + } + + function setSettings(settings: IUserSetting[]) { + set({ settings }) + } + + return { + settings: [], + loading: false, + getUserAccountSettings, + addUserAccountSettings, + updateUserAccountSettings, + setSettings, + } +}) @@ -0,0 +1,194 @@ +import axios, { AxiosResponse } from 'axios' +import { create } from 'zustand' + +import { API_URL } from '#/shared/lib/constants' + +export type ResponseAllInfo = { + uid: string + first_name: string + last_name: string + username: string + created_at: string + email: string + is_active: boolean + is_staff: boolean + show_balance: boolean + is_confirmed: boolean + is_social: boolean + social_auth: string[] + is_subscribed_to_emails: boolean + profile_picture_link: string | null + account_type: string + referral_code: { + code: string + } + token: { + access: string + refresh: string + } + payment_plan: { + uid: string + plan: { + uid: string + price: string + tokens_per_plan: string + title: string + duration: string + accessed_models: string[] | null + } + last_payment_at: string + next_payment_at: string + current_token_balance: number + } +} + +export interface UserReferralStore { + status: 'idle' | 'pending' | 'succeeded' | 'failed' + referral: string + userInfo: ResponseAllInfo | null + addReferral: (referral: string) => void + getAllInfo: (token: string | null | undefined) => Promise + unfollowEmail: (token: string | null | undefined) => Promise + getAll: (token: string | null | undefined) => Promise +} + +export const useUserReferralStore = create((set, get) => { + const getAll = async (token: string | null | undefined): Promise => { + if (!token) { + return { + uid: '', + first_name: '', + last_name: '', + username: '', + created_at: '', + email: '', + is_active: false, + is_staff: false, + show_balance: true, + is_confirmed: false, + is_social: false, + social_auth: [], + is_subscribed_to_emails: false, + profile_picture_link: null, + account_type: 'regular', + referral_code: { code: '' }, + token: { access: '', refresh: '' }, + payment_plan: { + uid: '', + plan: { + uid: '', + price: '', + tokens_per_plan: '', + title: '', + duration: '', + accessed_models: null, + }, + last_payment_at: '', + next_payment_at: '', + current_token_balance: 0, + }, + } + } + + try { + const { data } = await axios.get>(API_URL + '/auth/me', { + headers: { + Authorization: `Bearer ${token}`, + }, + validateStatus: (status) => status === 200, + }) + + return data + } catch (error) { + console.error('Error fetching user info:', error) + return { + uid: '', + first_name: '', + last_name: '', + username: '', + created_at: '', + email: '', + is_active: false, + is_staff: false, + show_balance: true, + is_confirmed: false, + is_social: false, + social_auth: [], + is_subscribed_to_emails: false, + profile_picture_link: null, + account_type: 'regular', + referral_code: { code: '' }, + token: { access: '', refresh: '' }, + payment_plan: { + uid: '', + plan: { + uid: '', + price: '', + tokens_per_plan: '', + title: '', + duration: '', + accessed_models: null, + }, + last_payment_at: '', + next_payment_at: '', + current_token_balance: 0, + }, + } + } + } + + async function getAllInfo(token: string | null | undefined) { + if (!token) return + + try { + set({ status: 'pending' }) + const userInfo = await getAll(token) + set({ + status: 'succeeded', + referral: '', + userInfo, + }) + } catch (error) { + set({ status: 'failed' }) + console.error('Error fetching user info:', error) + } + } + + async function unfollowEmail(token: string | null | undefined) { + if (!token) return + + try { + await axios.patch( + API_URL + '/auth/email-sub', + {}, + { headers: { Authorization: `Bearer ${token}` } } + ) + + const currentUserInfo = get().userInfo + if (currentUserInfo) { + set({ + userInfo: { + ...currentUserInfo, + is_subscribed_to_emails: !currentUserInfo.is_subscribed_to_emails, + }, + }) + } + } catch (error) { + console.error('Error unfollowing email:', error) + } + } + + function addReferral(referral: string) { + set({ referral }) + } + + return { + status: 'idle', + referral: '', + userInfo: null, + addReferral, + getAllInfo, + unfollowEmail, + getAll, + } +}) @@ -0,0 +1,44 @@ +import { create } from 'zustand' +import { UserDTO } from '../types' + +export interface UserStore { + user: UserDTO | null + setUser: (user: UserDTO) => void + getUser: () => UserDTO + setLoading: (loading: boolean) => void + setLoaded: (loaded: boolean) => void + loading: boolean + loaded: boolean +} + +export const useUserStore = create((set, get) => { + function setUser(user: UserDTO) { + set({ ...get(), user }) + } + + function getUser() { + const { user } = get() + if (!user) throw new Error('user is not defined') + return user + } + + function setLoading(loading: boolean) { + set({ ...get(), loading }) + } + + function setLoaded(loaded: boolean) { + set({ ...get(), loaded }) + } + + return { + // s + user: null, + loaded: false, + loading: true, + // f + setUser, + setLoading, + setLoaded, + getUser, + } +}) @@ -0,0 +1,41 @@ +import { patchEmail } from '#/features/auth' +import { makePrivateRequest } from '#/shared/api' +import { useShowDataStore } from '#/shared/lib/hooks' +import { getUserApi } from '../api' +import { useUserStore } from './use-user-store' + +export function useUser() { + const { setUser, user, loaded, loading, setLoaded, setLoading, getUser } = useUserStore() + + const { showMessage } = useShowDataStore() + + const fetchUser = makePrivateRequest(async () => { + setLoading(true) + const { data, status } = await getUserApi() + setLoading(false) + + if (status !== 200) return showMessage('Ошибка получения данных пользователя') + + setUser(data) + + setLoaded(true) + }) + + const unfollowEmail = makePrivateRequest(async () => { + const { status } = await patchEmail() + + if (status >= 400) return showMessage('Ошибка обновления email') + }) + + // const addReferral = makePrivateRequest() + + return { + fetchUser, + user, + loaded, + loading, + unfollowEmail, + getUser, + setUser + } +} @@ -1,180 +0,0 @@ -import { createAsyncThunk, createSlice } from '@reduxjs/toolkit' -import { createAction } from '@reduxjs/toolkit/src' -import axios, { AxiosResponse } from 'axios' - -import { loadingThunk } from '#/app/store/store' -import { API_URL } from '#/shared/lib/constants' - -type UserState = { - status: loadingThunk - referral: string -} - -export type ResponseAllInfo = { - uid: string - first_name: string - last_name: string - username: string - created_at: string - email: string - is_active: boolean - is_staff: boolean - show_balance: boolean - is_confirmed: boolean - is_social: boolean - social_auth: string[] - is_subscribed_to_emails: boolean - profile_picture_link: string | null - account_type: string - referral_code: { - code: string - } - token: { - access: string - refresh: string - } - payment_plan: { - uid: string - plan: { - uid: string - price: string - tokens_per_plan: string - title: string - duration: string - accessed_models: string[] | null - } - last_payment_at: string - next_payment_at: string - current_token_balance: number - } -} - -const initialState: UserState & ResponseAllInfo = { - status: 'idle', - referral: '', - uid: '', - first_name: '', - last_name: '', - username: '', - created_at: '', - email: '', - show_balance: true, - is_active: false, - social_auth: [], - is_staff: false, - is_confirmed: false, - referral_code: { - code: '', - }, - token: { - access: '', - refresh: '', - }, - is_subscribed_to_emails: false, - is_social: false, - profile_picture_link: null, - account_type: 'regular', - payment_plan: { - uid: '', - plan: { - uid: '', - price: '', - tokens_per_plan: '', - duration: '', - title: '', - accessed_models: null, - }, - last_payment_at: '', - next_payment_at: '', - current_token_balance: 0, - }, -} - -export const getAll = async (token: string | null | undefined): Promise => { - const { data } = await axios.get>(API_URL + '/auth/me', { - headers: { - Authorization: `Bearer ${token}`, - }, - validateStatus: (status) => status === 200, - }) - - return data -} - -export const getAllInfo = createAsyncThunk( - 'user/getAllInfo', - async (token: string | null | undefined) => { - return await getAll(token) - } -) - -export const unfollowEmail = createAsyncThunk( - 'user/unfollowEmail', - async (token: string | null | undefined) => { - await axios.patch( - API_URL + '/auth/email-sub', - {}, - { headers: { Authorization: `Bearer ${token}` } } - ) - } -) - -export const userSlice = createSlice({ - name: 'userSlice', - initialState, - reducers: { - addReferral: (state, action) => { - state.referral = action.payload - }, - }, - extraReducers: (builder) => { - builder.addCase(getAllInfo.fulfilled, (state, action) => { - state.status = 'succeeded' - state.referral = '' - const { - uid, - is_subscribed_to_emails, - email, - account_type, - first_name, - last_name, - profile_picture_link, - is_active, - is_staff, - created_at, - username, - payment_plan, - is_confirmed, - is_social, - referral_code, - show_balance, - } = action.payload - state.is_subscribed_to_emails = is_subscribed_to_emails - state.account_type = account_type - state.email = email - state.payment_plan = payment_plan - state.is_confirmed = is_confirmed - state.first_name = first_name - state.last_name = last_name - state.profile_picture_link = profile_picture_link - state.is_active = is_active - state.is_staff = is_staff - state.created_at = created_at - state.username = username - state.is_social = is_social - state.referral_code = referral_code - state.show_balance = show_balance - }) - builder.addCase(getAllInfo.pending, (state) => { - state.status = 'pending' - }) - builder.addCase(getAllInfo.rejected, (state) => { - state.status = 'failed' - }) - builder.addCase(unfollowEmail.fulfilled, (state, action) => { - state.is_subscribed_to_emails = !state.is_subscribed_to_emails - }) - }, -}) - -export const { addReferral } = userSlice.actions @@ -0,0 +1 @@ +export * from './user.types' \ No newline at end of file @@ -0,0 +1,38 @@ +export interface UserDTO { + uid: string + first_name: string + last_name: string + username: string + created_at: string + email: string + is_active: boolean + is_staff: boolean + show_balance: boolean + is_confirmed: boolean + is_social: boolean + social_auth: string[] + is_subscribed_to_emails: boolean + profile_picture_link: string | null + account_type: string + referral_code: { + code: string + } + token: { + access: string + refresh: string + } + payment_plan: { + uid: string + plan: { + uid: string + price: string + tokens_per_plan: string + title: string + duration: string + accessed_models: string[] | null + } + last_payment_at: string + next_payment_at: string + current_token_balance: number + } +} \ No newline at end of file @@ -0,0 +1,2 @@ +export * from './model' +export * from './types' \ No newline at end of file @@ -1,2 +0,0 @@ -export { getAllInfo, userSlice } from './model/user-type-slice' -export * from './model' \ No newline at end of file @@ -5,7 +5,7 @@ import { Dayjs } from 'dayjs' import { useRouter } from 'next/navigation' import { useSession } from 'next-auth/react' -import { useAppSelector } from '#/app/store/store' +import { useThemeStore } from '#/entities/theme/model/use-theme-store' import { ButtonGray, ButtonUI, Error, InputStyleDark, InputStyleLight, Loader, Modal } from '#/shared' import { accountApi } from '#/shared/api/account-endpoints' import { API_URL } from '#/shared/lib/constants' @@ -26,7 +26,7 @@ export const ApiKeyModal = ({ }) => { const [endDate, setEndDate] = useState() const [title, setTitle] = useState('') - const theme = useAppSelector((state) => state.theme.theme) + const theme = useThemeStore((state) => state.theme) const [isLoading, setIsLoading] = useState(false) @@ -0,0 +1,27 @@ +import { api } from '#/shared/api' +import { AuthUserByCredentialsDTO } from '../types' + +export function authUser({ username, password }: AuthUserByCredentialsDTO) { + return api.post('/auth/login', { email: username, password }) +} + +export function authUserByYandex(token: string) { + const { NEXT_PUBLIC_CLIENT_ID_YANDEX, NEXT_PUBLIC_CLIENT_SECRET_YANDEX } = process.env + + return api.post('/auth/login-social/convert-token', { + grant_type: 'convert_token', + client_id: NEXT_PUBLIC_CLIENT_ID_YANDEX, + client_secret: NEXT_PUBLIC_CLIENT_SECRET_YANDEX, + backend: 'yandex-oauth2', + token, + }) +} + +export function refresh(refresh: string) { + console.log('refresh::', refresh) + return api.post('auth/token/refresh', { refresh }) +} + +export function patchEmail() { + return api.patch('/auth/email-sub') +} @@ -0,0 +1 @@ +export * from './auth.routes' \ No newline at end of file @@ -0,0 +1,34 @@ +import { api } from '#/shared/api' +import axios from 'axios' +import { signOut, useSession } from 'next-auth/react' +import { useRouter } from 'next/router' +import { useEffect } from 'react' + +export function useAuthSession() { + const { data, update } = useSession() + + const { push } = useRouter() + + useEffect(() => { + // if (!data || !update) return + // axios.defaults.validateStatus = () => true + // axios.interceptors.response.use(async ({ status, ...r }) => { + // if (status !== 401) return { status, ...r } + + // const session = await update('refresh') + + // console.log('session::', status) + + // if (!session) window.location.href = '/login' + + // r.config.headers.Authorization = `Bearer ${session?.access}` + + // const response = await axios.request(r.request) + + // console.log('response::', response) + + // return response + // }) + + }, [data, update]) +} @@ -0,0 +1,6 @@ + + +export interface AuthUserByCredentialsDTO { + username: string + password: string +} \ No newline at end of file @@ -0,0 +1 @@ +export * from './auth.types' \ No newline at end of file @@ -0,0 +1,2 @@ +export * from './api' +export * from './types' \ No newline at end of file @@ -1,5 +1,4 @@ -import { setParams } from '#/app/store/model-parametres-store' -import { useAppSelector, useAppDispatch } from '#/app/store/store' +import { useParamsStore } from '#/app/store/use-params-store' import { InferenceParams } from '#/entities/model-entity' import React, { useEffect } from 'react' @@ -9,12 +8,10 @@ interface BotParamsMap { } export const useBotParamsMap = ({ params, currentVersion }: BotParamsMap) => { - const includeParams = useAppSelector((state) => state.params.params) - const dispatch = useAppDispatch() - - const setNewParam = (payload: { [key: string]: string | number | number[] | boolean }) => { - dispatch(setParams(payload)) - } + const { params: includeParams, setParams: setNewParam } = useParamsStore((state) => ({ + params: state.params, + setParams: state.setParams + })) useEffect(() => { if (currentVersion && params) { @@ -1,35 +0,0 @@ -import axios, { AxiosResponse } from 'axios' - -import { API_URL } from '#/shared/lib/constants' - -import { ResponseGetBusinessGroups, ResponseGetPersons } from '../model/types' - -export const addBusinessGroup = async ( - title: string, - token_limit: string, - parent_company?: string, - token?: string -): Promise => { - try { - const { data } = await axios.post< - { uid: string; title: string; token_limit: string }, - AxiosResponse - >( - API_URL + '/auth/business-groups/', - { - title: title, - token_limit: token_limit, - parent_company: parent_company, - }, - { - headers: { - Authorization: `Bearer ${token}`, - }, - } - ) - - return data - } catch (err) { - return null - } -} @@ -1,23 +0,0 @@ -import axios, { AxiosResponse } from 'axios' - -import { API_URL } from '#/shared/lib/constants' - -import { ResponseGetBusinessGroups, ResponseGetPersons } from '../model/types' - -export const addBusinessGroupUser = async ( - group_id: string, - email: string, - token?: string -): Promise => { - try { - const { data } = await axios.post(API_URL + `/auth/business-groups/${group_id}/accounts/${email}/`, { - headers: { - Authorization: `Bearer ${token}`, - }, - }) - - return data - } catch (err) { - return null - } -} @@ -0,0 +1,31 @@ +import { api } from '#/shared/api' +import { ResponseGetBusinessGroups } from '../model/types' +import { AddBusinessGroupDTO, ChangeBusinessGroupDTO } from '../types' + +export function getBusinessGroup(group_id: string) { + return api.get(`/auth/business-groups/${group_id}/`) +} + +export function addBusinessGroup(dto: AddBusinessGroupDTO) { + return api.post('/auth/business-groups/', dto) +} + +export function getBusinessGroups() { + return api.get('/auth/business-groups') +} + +export function deleteBusinessGroup(id: string) { + return api.delete(`/auth/business-groups/${id}/`) +} + +export function addBusinessGroupUser(id: string, email: string) { + return api.post(`/auth/business-groups/${id}/accounts/${email}/`) +} + +export function deleteBusinessGroupUser(group_id: string, email: string) { + return api.delete(`/auth/business-groups/${group_id}/accounts/${email}/`) +} + +export function changeBusinessGroup(group_id: string, dto: ChangeBusinessGroupDTO) { + return api.put(`/auth/business-groups/${group_id}/`, dto) +} @@ -1,31 +0,0 @@ -import axios, { AxiosResponse } from 'axios' - -import { API_URL } from '#/shared/lib/constants' - -import { ResponseGetBusinessGroups } from '../model/types' - -export const changeBusinessGroup = async ( - title: string, - token_limit: string, - group_id: string, - token?: string -): Promise => { - try { - const { data } = await axios.put( - API_URL + `/auth/business-groups/${group_id}/`, - { - title: title, - token_limit: token_limit, - }, - { - headers: { - Authorization: `Bearer ${token}`, - }, - } - ) - - return data - } catch (err) { - return null - } -} @@ -1,23 +0,0 @@ -import axios, { AxiosResponse } from 'axios' - -import { API_URL } from '#/shared/lib/constants' - -import { ResponseGetBusinessGroups, ResponseGetPersons } from '../model/types' - -export const deleteBusinessGroupUser = async ( - group_id: string, - email: string, - token?: string -): Promise => { - try { - const { data } = await axios.delete(API_URL + `/auth/business-groups/${group_id}/accounts/${email}/`, { - headers: { - Authorization: `Bearer ${token}`, - }, - }) - - return data - } catch (err) { - return null - } -} @@ -1,22 +0,0 @@ -import axios, { AxiosResponse } from 'axios' - -import { API_URL } from '#/shared/lib/constants' - -import { ResponseGetBusinessGroups } from '../model/types' - -export const getBusinessGroup = async (group_id: string, token?: string): Promise => { - try { - const { data } = await axios.get( - API_URL + `/auth/business-groups/${group_id}/`, - { - headers: { - Authorization: `Bearer ${token}`, - }, - } - ) - - return data - } catch (err) { - return null - } -} @@ -0,0 +1 @@ +export * from './business-group.route' \ No newline at end of file @@ -0,0 +1,3 @@ +export * from './use-change-business-group-form' +export * from './use-business-host-account' +export * from './use-add-business-group-form' \ No newline at end of file @@ -0,0 +1,52 @@ +import { useForm } from 'react-hook-form' +import { AddBusinessGroupForm } from '../types' +import { makePrivateRequest } from '#/shared/api' +import { Dispatch, SetStateAction, useState } from 'react' +import { ResponseGetBusinessGroups } from './types' +import { useShowDataStore } from '#/shared/lib/hooks' +import { withPending } from '#/shared' +import { addBusinessGroup } from '../api' + +export function useAddBusinessGroupForm( + setGroups: Dispatch>, + company_uid?: string, + onClose?: Function, +) { + const { + handleSubmit, + register, + reset, + setValue, + getValues, + formState: { errors }, + } = useForm() + + const { showMessage } = useShowDataStore() + + const [onSubmit, loading] = withPending( + handleSubmit( + makePrivateRequest(async (dto) => { + const { data, status } = await addBusinessGroup({ ...dto, parent_company: company_uid }) + + if (![201, 200].includes(status)) return showMessage('Что-то пошло не так') + + setGroups((g) => [...g, data]) + + onClose && onClose() + + reset() + }), + (errors) => showMessage(Object.values(errors)[0].message ?? 'Неверные данные') + ) + ) + + return { + register, + reset, + setValue, + getValues, + onSubmit, + loading, + errors, + } +} @@ -0,0 +1,75 @@ +import { ResponseGetPersons } from '#/features/invite-person-in-business' +import { makePrivateRequest } from '#/shared/api' +import { useState } from 'react' +import { getBusinessHostAccounts } from '../../business-host-data/api' +import { useShowDataStore } from '#/shared/lib/hooks' +import { ChangeBusinessGroupForm } from '#/features/business-group/types' +import { UseFormSetValue } from 'react-hook-form' +import { + addBusinessGroupUser, + deleteBusinessGroupUser, + getBusinessGroup, +} from '#/features/business-group/api' +import { ResponseGetBusinessGroups } from '#/features/invite-person-in-business/model/types' + +export function useBusinessHostAccount( + currentGroup: ResponseGetBusinessGroups, + setValue: UseFormSetValue +) { + const [accounts, setAccounts] = useState([]) + const [addUsers, setAddUsers] = useState([]) + + const { showMessage } = useShowDataStore() + + const fetchBusinessHostAccount = makePrivateRequest(async () => { + const { data, status } = await getBusinessHostAccounts() + + if (status !== 200) return showMessage('Ошибка получения списка пользователей группы') + + setAddUsers(data) + }) + + const fetchBusinessGroup = makePrivateRequest(async () => { + if (!currentGroup) return + + const { data, status } = await getBusinessGroup(currentGroup.uid) + + if (status !== 200) return showMessage('Ошибка получения списка пользователей группы') + + setAccounts(data.accounts) + + setValue('title', data.title) + setValue('token_limit', data.token_limit) + }) + + const addNewUser = makePrivateRequest(async (user: ResponseGetPersons) => { + const { status } = await addBusinessGroupUser(currentGroup.uid, user.email) + + if (status >= 400) return showMessage('Ошибка добавления пользователя в аккаунт') + + setAccounts((a) => [...a, user]) + + setAddUsers((users) => users.filter((el) => el.email !== user.email)) + }) + + const deleteUser = makePrivateRequest(async (user: ResponseGetPersons) => { + const { data, status } = await deleteBusinessGroupUser(currentGroup.uid, user.email) + + if (status >= 400) return showMessage('Ошибка удаления пользователя из аккаунта') + + setAddUsers((u) => [...u, user]) + + setAccounts((accounts) => accounts.filter((el) => el.email !== user.email)) + }) + + return { + addUsers, + accounts, + fetchBusinessHostAccount, + fetchBusinessGroup, + setAddUsers, + setAccounts, + addNewUser, + deleteUser + } +} @@ -0,0 +1,56 @@ +import { useForm } from 'react-hook-form' +import { ChangeBusinessGroupForm } from '../types' +import { makePrivateRequest } from '#/shared/api' +import { Dispatch, SetStateAction, useState } from 'react' +import { ResponseGetBusinessGroups } from './types' +import { useShowDataStore } from '#/shared/lib/hooks' +import { withPending } from '#/shared' +import { changeBusinessGroup } from '../api' + +export function useChangeBusinessGroupForm( + current_group: ResponseGetBusinessGroups, + changes: ResponseGetBusinessGroups[], + setChanges: Dispatch>, + onClose: Function +) { + const { + handleSubmit, + register, + reset, + setValue, + getValues, + formState: { errors }, + } = useForm() + + const { showMessage } = useShowDataStore() + + const [onSubmit, loading] = withPending( + handleSubmit( + makePrivateRequest(async (dto) => { + if (!changes) return + + const { data, status } = await changeBusinessGroup(current_group.uid, dto) + + if (![201, 200].includes(status)) return showMessage('Что-то пошло не так') + + setChanges((c) => [...c.filter((g) => g.uid !== current_group.uid), data]) + + onClose() + + reset() + }), + (errors) => showMessage(Object.values(errors)[0].message ?? 'Неверные данные') + ) + ) + + return { + handleSubmit, + register, + reset, + setValue, + getValues, + onSubmit, + loading, + errors, + } +} @@ -0,0 +1,3 @@ +import { AddBusinessGroupDTO } from './group.type' + +export interface AddBusinessGroupForm extends AddBusinessGroupDTO {} @@ -0,0 +1,3 @@ +import { ChangeBusinessGroupDTO } from './group.type' + +export interface ChangeBusinessGroupForm extends ChangeBusinessGroupDTO {} @@ -0,0 +1,10 @@ +export interface AddBusinessGroupDTO { + title: string + token_limit: string + parent_company?: string +} + +export interface ChangeBusinessGroupDTO { + title: string, + token_limit: string, +} \ No newline at end of file @@ -0,0 +1,3 @@ +export * from './group.type' +export * from './change-business-group-form.types' +export * from './add-business-group-form.types' \ No newline at end of file @@ -1,7 +1,21 @@ +.input{ + &::placeholder{ + font-size: 16px; + } +} + .wrap { + padding-top: 50px; + min-width: 359px; + &__inputs{ + display: flex; + flex-direction: column; + gap: 10px; + } .title { - font-size: 21px; + font-size: 24px; font-weight: 500; + margin-bottom: 20px; } .hint { margin-top: 15px; @@ -13,9 +27,7 @@ display: flex; align-items: center; justify-content: flex-end; - div { - margin-right: 10px; - } + gap: 10px; width: 100%; margin-top: 15px; text-align: right; @@ -1,114 +1,74 @@ -import React, { FC, useState } from 'react' -import { useForm } from 'react-hook-form' -import { SubmitErrorHandler } from 'react-hook-form/dist/types/form' -import { Box, TextField, Typography } from '@mui/material' -import { useSession } from 'next-auth/react' +import React, { FC } from 'react' -import { addBusinessGroup } from '#/features/business-group/api/add-business-group' import { ResponseGetBusinessGroups } from '#/features/business-group/model/types' -import { invitePerson } from '#/features/invite-person-in-business/api/invite-person' -import { IEmailForms } from '#/features/register-by-email/model/types' -import { useAppSelector } from '#/app/store/store' -import { ButtonGray, ButtonUI, InputStyleDark, InputStyleLight, Loader, Modal, ModalProps } from '#/shared' + +import { Loader } from '#/shared' 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 - open: boolean - onClose: () => void +import { useAddBusinessGroupForm } from '../model' +import { ADD_BUSINESS_GROUP, PlateTemplate, getModalById } from '#/features/modals' +import { CommonButton } from '#/shared/ui/button' +import { CommonInput } from '#/shared/ui/common-input' + +interface AddBusinessGroupProps { company_uid?: string - setNewGroup: React.Dispatch> - oldGroups: ResponseGetBusinessGroups[] | null | undefined + setNewGroup: React.Dispatch> } -export const AddBusinessGroup: FC = ({ open, onClose, company_uid, setNewGroup, oldGroups }) => { - const { handleSubmit, register, reset, setValue, getValues } = useForm() - const [isLoading, setIsLoading] = useState(false) - const [limit, setLimit] = useState('0') - const [title, setTitle] = useState('') - const theme = useAppSelector((state) => state.theme.theme) - const { showMessage } = useShowDataStore() - const { data: session } = useSession() - - const onSubmit = async (data: any) => { - setIsLoading(true) - const res = await addBusinessGroup(title, limit, company_uid, session?.access) - - if (res === null) { - showMessage('Что-то пошло не так') - setIsLoading(false) - return - } - if (res && oldGroups && oldGroups?.length !== 0) { - setNewGroup([...oldGroups, res]) - } else if (res) { - setNewGroup([res]) - } - setTitle('') - setLimit('0') - setIsLoading(false) - onClose() +export const AddBusinessGroup = ({ company_uid, setNewGroup }: AddBusinessGroupProps) => { + const modal = getModalById(ADD_BUSINESS_GROUP) - reset() - } - const checkError: SubmitErrorHandler = (data) => { - showMessage(Object.values(data)[0].message || 'Неверные данные') - } - - const handleEmailChange = (event: any) => { - const emailWithoutSpaces = event.target.value.trim() - setValue('email', emailWithoutSpaces) - } + const { register, onSubmit, loading, errors } = useAddBusinessGroupForm(setNewGroup, company_uid, () => + modal.setState(false) + ) return ( - -
- - Создать группу + + +
+

Создать группу

- - - Название группы - - setTitle(e.target.value)} - fullWidth - sx={theme === 'light' ? { ...InputStyleLight } : { ...InputStyleDark }} +
+ - - - - Лимит токенов - - setLimit(e.target.value)} - fullWidth - sx={theme === 'light' ? { ...InputStyleLight } : { ...InputStyleDark }} + - +
- {isLoading ? ( - + {loading ? ( +
- +
) : ( - - - - - - +
+ modal.setState(false)} + variant='gray' + > + Отмена + + + Создать +
)} -
+
-
+ ) } @@ -0,0 +1,135 @@ +import React, { FC, useEffect, useState } from 'react' +import { Box, TextField, Typography } from '@mui/material' +import { useSession } from 'next-auth/react' + +import { ResponseGetBusinessGroups } from '#/features/business-group/model/types' +import { ResponseGetPersons } from '#/features/invite-person-in-business' + +import { ButtonGray, ButtonUI, InputStyleDark, InputStyleLight, Loader, Modal, ModalProps } from '#/shared' + +import styles from './add-business-group.module.scss' +import { useBusinessHostAccount, useChangeBusinessGroupForm } from '../model' +import { CHANGE_BUSINESS_GROUP, PlateTemplate, getModalById } from '#/features/modals' +import { CommonInput } from '#/shared/ui/common-input' +import { CommonButton } from '#/shared/ui/button' + +interface ChangeBusinessGroupProps { + company_uid?: string + setChanges: React.Dispatch> + changes: ResponseGetBusinessGroups[] + current_group: ResponseGetBusinessGroups +} + +export const ChangeBusinessGroup = ({ + company_uid, + current_group, + setChanges, + changes, +}: ChangeBusinessGroupProps) => { + const modal = getModalById(CHANGE_BUSINESS_GROUP) + + const { register, setValue, errors, loading, onSubmit } = useChangeBusinessGroupForm( + current_group, + changes, + setChanges, + () => modal.setState(false) + ) + + const { accounts, addUsers, deleteUser, addNewUser, fetchBusinessHostAccount, fetchBusinessGroup } = + useBusinessHostAccount(current_group, setValue) + + + useEffect(() => { + fetchBusinessHostAccount() + fetchBusinessGroup() + }, [current_group]) + + return ( + +
+

+ {current_group.title} +

+ + + + + +
+

+ В группе: +

+
+ {accounts.length > 0 ? ( + accounts.map((el, idx) => ( +

deleteUser(el)} + className={styles.accounts} + key={idx} + > + {el.email} +

+ )) + ) : ( +

Сотрудники отсутствуют

+ )} +
+
+ +
+

+ Можно добавить: +

+
+ {addUsers && addUsers.length !== 0 ? ( + addUsers.map((el, idx) => ( +

addNewUser(el)} + className={styles.accounts} + key={idx} + > + {el.email} +

+ )) + ) : ( +

Сотрудники отсутствуют

+ )} +
+
+ + {loading ? ( +
+ +
+ ) : ( +
+ modal.setState(false)} + > + Отмена + + Сохранить +
+ )} + +
+ ) +} @@ -1,209 +0,0 @@ -import React, { FC, useEffect, useState } from 'react' -import { useForm } from 'react-hook-form' -import { SubmitErrorHandler } from 'react-hook-form/dist/types/form' -import { Box, TextField, Typography } from '@mui/material' -import axios from 'axios' -import { useSession } from 'next-auth/react' - -import { addBusinessGroupUser } from '#/features/business-group/api/add-user' -import { changeBusinessGroup } from '#/features/business-group/api/change-group-data' -import { deleteBusinessGroupUser } from '#/features/business-group/api/delete-user' -import { getBusinessGroup } from '#/features/business-group/api/get-group-data' -import { InviteRoles, inviteRoles, InviteRolesEn, RoleSelect } from '#/features/business-group/lib/constants' -import { ResponseGetBusinessGroups } from '#/features/business-group/model/types' -import { ResponseGetPersons } from '#/features/invite-person-in-business' -import { IEmailForms } from '#/features/register-by-email/model/types' -import { useAppSelector } from '#/app/store/store' -import { ButtonGray, ButtonUI, Error, InputStyleDark, InputStyleLight, Loader, Modal, ModalProps } from '#/shared' -import { API_URL } from '#/shared/lib/constants' - -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 - open: boolean - onClose: () => void - company_uid?: string - setChanges: React.Dispatch> - changes: ResponseGetBusinessGroups[] | null | undefined - current_group: ResponseGetBusinessGroups -} - -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('') - const [title, setTitle] = useState('0') - const [accounts, setAccounts] = useState() - const [addUsers, setAddUsers] = useState() - const theme = useAppSelector((state) => state.theme.theme) - const { showMessage } = useShowDataStore() - const { data: session } = useSession() - - useEffect(() => { - getBusinessGroup(current_group.uid, session?.access).then((res) => { - if (res !== null) { - setTitle(res.title) - setLimit(res.token_limit) - setAccounts(res.accounts as any) - } - }) - }, [current_group, session?.access]) - - useEffect(() => { - axios.get(API_URL + '/auth/business-host/accounts?have_group=false', { - headers: { Authorization: `Bearer ${session?.access}` }, - }).then((res) => { - setAddUsers(res.data) - }) - }, [session?.access, current_group]) - - const addNewUser = (user: ResponseGetPersons) => { - addBusinessGroupUser(current_group.uid, user.email, session?.access).then((res) => { - if (accounts && accounts?.length !== 0) { - setAccounts([...accounts, user]) - } else { - setAccounts([user]) - } - setAddUsers(addUsers?.filter((el) => el.email !== user.email)) - }) - } - - const deleteUser = (user: ResponseGetPersons) => { - deleteBusinessGroupUser(current_group.uid, user.email, session?.access).then((res) => { - if (addUsers && addUsers?.length !== 0) { - setAddUsers([...addUsers, user]) - } else { - setAddUsers([user]) - } - setAccounts(accounts?.filter((el) => el.email !== user.email)) - }) - } - - const onSubmit = async (data: any) => { - setIsLoading(true) - const res = await changeBusinessGroup(title, limit, current_group.uid, session?.access) - - if (res === null) { - showMessage('Что-то пошло не так') - setIsLoading(false) - return - } else { - setChanges( - changes?.map((el) => { - if (el.uid === current_group.uid) { - el.title = title - el.token_limit = limit - } - return el - }) - ) - } - setIsLoading(false) - onClose() - - reset() - } - const checkError: SubmitErrorHandler = (data) => { - showMessage(Object.values(data)[0].message || 'Неверные данные') - } - - return ( - - - - {current_group.title} - - - - - Название группы - - setTitle(e.target.value)} - fullWidth - sx={theme === 'light' ? { ...InputStyleLight } : { ...InputStyleDark }} - /> - - - - - Лимит токенов - - setLimit(e.target.value)} - fullWidth - sx={theme === 'light' ? { ...InputStyleLight } : { ...InputStyleDark }} - /> - - - - - В группе: - - - {accounts && accounts.length !== 0 ? ( - accounts.map((el, idx) => ( -

deleteUser(el)} className={styles.accounts} key={idx}> - {el.email} -

- )) - ) : ( -

Сотрудники отсутствуют

- )} -
-
- - - - Можно добавить: - - - {addUsers && addUsers.length !== 0 ? ( - addUsers.map((el, idx) => ( -

addNewUser(el)} className={styles.accounts} key={idx}> - {el.email} -

- )) - ) : ( -

Сотрудники отсутствуют

- )} -
-
- - {isLoading ? ( - - - - ) : ( - - - - - - - )} -
-
- ) -} @@ -2,3 +2,4 @@ export { RoleSelect } from './lib/constants' export type { PersonInBusiness } from './model/types' export type { ResponseGetPersons } from './model/types' export { AddBusinessGroup } from './ui/add-business-group' +export * from './model' \ No newline at end of file @@ -0,0 +1,104 @@ +import axios from 'axios' + +import { ResponseGetPersons } from '#/features/invite-person-in-business' +import { API_URL } from '#/shared/lib/constants' +import { InfoBusiness } from '#/widgets/business-info/api/get-info' + +export const businessHostApi = { + getSecurityPersons: async (token?: string): Promise => { + try { + const { data } = await axios.get( + API_URL + '/auth/business-host/accounts?type=sec', + { + headers: { + Authorization: `Bearer ${token}`, + }, + } + ) + return data ? data.reverse() : null + } catch (err) { + return null + } + }, + + getRegularPersons: async (token?: string): Promise => { + try { + const { data } = await axios.get( + API_URL + '/auth/business-host/accounts?type=regular&type=admin', + { + headers: { + Authorization: `Bearer ${token}`, + }, + } + ) + return data ? data.reverse() : null + } catch (err) { + return null + } + }, + + getBusinessInfo: async (token?: string): Promise => { + try { + const { data } = await axios.get(API_URL + '/auth/business-host', { + headers: { + Authorization: `Bearer ${token}`, + }, + }) + return data + } catch (err) { + return null + } + }, + + updateMailingSettings: async (token?: string, mailing?: boolean): Promise => { + try { + await axios.put( + API_URL + '/auth/business-host', + { token_cap_enabled: mailing }, + { + headers: { + Authorization: `Bearer ${token}`, + }, + } + ) + } catch (err) { + throw new Error('Ошибка обновления настроек уведомлений') + } + }, + + downloadExpenses: async (token?: string, fromDate?: string, toDate?: string): Promise => { + try { + const response = await axios.get( + API_URL + + `/auth/business-host/download-expenses?type=employees&from_date=${fromDate}&to_date=${toDate}`, + { + responseType: 'arraybuffer', + headers: { + Authorization: `Bearer ${token}`, + }, + } + ) + return new Blob([response.data], { + type: 'application/ms-excel;charset=utf-8', + }) + } catch (err) { + throw new Error('Ошибка скачивания отчета') + } + }, + + updateMailingEmails: async (token?: string, emails?: string[]): Promise => { + try { + await axios.put( + API_URL + '/auth/business-host', + { token_cap_emails: emails }, + { + headers: { + Authorization: `Bearer ${token}`, + }, + } + ) + } catch (err) { + throw new Error('Ошибка обновления списка email') + } + }, +} @@ -0,0 +1,33 @@ +import { ResponseGetPersons } from '#/features/invite-person-in-business' +import { InfoBusiness } from '#/widgets/business-info' +import { api } from '#/shared/api' + +export const getBusinessHostAccounts = () => { + return api.get('/auth/business-host/accounts?have_group=false') +} + +export function getSecurityPersons() { + return api.get('/auth/business-host/accounts?type=sec') +} + +export function getRegularPersons() { + return api.get('/auth/business-host/accounts?type=regular&type=admin') +} + +export function getBusinessInfo() { + return api.get('/auth/business-host') +} + +export function downloadExpenses(from_date?: string, to_date?: string) { + return api.get(`/auth/business-host/download-expenses?type=employees`, { + params: { from_date, to_date }, + }) +} + +export function putMailingEmails(token_cap_emails: string[]) { + return api.put('/auth/business-host', { token_cap_emails }) +} + +export function putMailingSettings(token_cap_enabled: boolean) { + return api.put('/auth/business-host', { token_cap_enabled }) +} @@ -0,0 +1,2 @@ +export * from './business-host.route' +export * from './business-host-api' \ No newline at end of file @@ -0,0 +1,39 @@ +import React, { createContext, ReactNode, useContext } from 'react' + +import { useBusinessInfoStore } from '../model/business-info.store' +import { useLoadingStore } from '../model/loading.store' +import { usePersonsStore } from '../model/persons.store' + +interface BusinessHostContextType { + info: any + isLoading: boolean + error: string | null + securityList: any[] | null + personsList: any[] | null +} + +const BusinessHostContext = createContext(null) + +export const BusinessHostProvider: React.FC<{ children: ReactNode }> = ({ children }) => { + const { info } = useBusinessInfoStore() + const { isLoading, error } = useLoadingStore() + const { securityList, personsList } = usePersonsStore() + + const value = { + info, + isLoading, + error, + securityList, + personsList, + } + + return {children} +} + +export const useBusinessHostContext = () => { + const context = useContext(BusinessHostContext) + if (!context) { + throw new Error('useBusinessHostContext must be used within BusinessHostProvider') + } + return context +} @@ -0,0 +1,2 @@ +export * from './use-business-host-data' +export * from './use-business-lists' \ No newline at end of file @@ -0,0 +1,22 @@ +import { useEffect } from 'react' +import { useSession } from 'next-auth/react' + +import { useLoadingStore } from '../model/loading.store' + +import { useBusinessHostData } from './use-business-host-data' + +export const useBusinessHostCache = () => { + const { data } = useSession() + const { isLoading, isDataLoaded } = useLoadingStore() + const { loadBusinessHostData } = useBusinessHostData() + + useEffect(() => { + if (data?.access && !isDataLoaded && !isLoading) { + loadBusinessHostData() + } + }, [data?.access, isDataLoaded, isLoading]) + + return { + hasLoaded: isDataLoaded, + } +} @@ -0,0 +1,121 @@ +import { makePrivateRequest } from '#/shared/api' +import { + downloadExpenses, + getBusinessInfo, + getRegularPersons, + getSecurityPersons, + putMailingEmails, + putMailingSettings, +} from '../api' +import { useShowDataStore } from '#/shared/lib/hooks' +import { useEffect, useState } from 'react' +import error from 'next/error' +import { useBusinessInfoStore } from '../model/business-info.store' +import { useLoadingStore } from '../model/loading.store' +import { usePersonsStore } from '../model/persons.store' + +export const useBusinessHostData = () => { + const { setSecurityList, setPersonsList, securityList, personsList } = usePersonsStore() + + const { setInfo, setMailing, mailing, info } = useBusinessInfoStore() + + const { setIsLoading, isLoading, setIsDataLoaded } = useLoadingStore() + + const [emails, setEmails] = useState([]) + + const { showMessage } = useShowDataStore() + + const fetchSecurityPersons = makePrivateRequest(async () => { + const { status, data } = await getSecurityPersons() + + if (status !== 200) return showMessage('Ошибка получения приватных пользователей') + + setSecurityList(data) + }) + + const fetchRegularPersons = makePrivateRequest(async () => { + const { status, data } = await getRegularPersons() + + if (status !== 200) return showMessage('Ошибка получения пользователей') + + setPersonsList(data) + }) + + const fetchBusinessInfo = makePrivateRequest(async () => { + const { status, data } = await getBusinessInfo() + + if (status !== 200) return showMessage('Ошибка получения пользователей') + + setInfo(data) + + setMailing(data.token_cap_enabled) + + setEmails(data.token_cap_emails) + }) + + async function fetchData() { + setIsLoading(true) + + await Promise.all([fetchBusinessInfo(), fetchRegularPersons(), fetchSecurityPersons()]) + + setIsLoading(false) + + setIsDataLoaded(true) + } + + const updateMailingSettings = makePrivateRequest(async (newMailing: boolean) => { + const { status } = await putMailingSettings(newMailing) + + if (status >= 400) return showMessage('Ошибка обновления настроек email') + + setMailing(newMailing) + }) + + const downloadExpensesReport = makePrivateRequest(async (fromDate?: string, toDate?: string) => { + const { data, status } = await downloadExpenses(fromDate, toDate) + + if (status >= 400) return showMessage('Ошибка скачивания отчета') + + const link = document.createElement('a') + + link.href = window.URL.createObjectURL(data) + + link.download = 'Expenses.xlsx' + + document.body.appendChild(link) + + link.click() + + document.body.removeChild(link) + + showMessage('Отчет успешно скачан', 'success') + }) + + const updateMailingEmails = makePrivateRequest(async (emails: string[]) => { + const { status, data } = await putMailingEmails(emails) + + if (status >= 400) return showMessage('Ошибка обновления списка emails') + + setEmails(emails) + + showMessage('Настройки уведомлений обновлены', 'success') + }) + + return { + securityList, + personsList, + info, + mailing, + isLoading, + error, + fetchData, + updateMailingSettings, + downloadExpensesReport, + updateMailingEmails, + fetchSecurityPersons, + emails, + setEmails, + setPersonsList, + setSecurityList, + } +} @@ -0,0 +1,27 @@ +import { usePersonsStore } from '../model/persons.store' + +import { ResponseGetPersons } from '#/features/invite-person-in-business' + +export const useBusinessLists = () => { + const { securityList, personsList, addPersonToList } = usePersonsStore() + + const addPersonToPersonalList = (person: ResponseGetPersons) => { + addPersonToList(person, 'personal') + } + + const addPersonToSecurityList = (person: ResponseGetPersons) => { + addPersonToList(person, 'security') + } + + const addList = (newUser: ResponseGetPersons, list: 'personal' | 'security') => { + addPersonToList(newUser, list) + } + + return { + securityList, + personsList, + addPersonToPersonalList, + addPersonToSecurityList, + addList, + } +} @@ -0,0 +1,33 @@ +import { create } from 'zustand' +import { persist } from 'zustand/middleware' + +import { InfoBusiness } from '#/widgets/business-info/api/get-info' + +export interface BusinessInfoStore { + info: InfoBusiness | null + mailing: boolean | undefined + + setInfo: (info: InfoBusiness | null) => void + setMailing: (mailing: boolean | undefined) => void + resetInfo: () => void +} + +export const useBusinessInfoStore = create()( + persist( + (set) => ({ + info: null, + mailing: undefined, + + setInfo: (info) => set({ info }), + setMailing: (mailing) => set({ mailing }), + resetInfo: () => set({ info: null, mailing: undefined }), + }), + { + name: 'business-info-storage', + partialize: (state) => ({ + info: state.info, + mailing: state.mailing, + }), + } + ) +) @@ -0,0 +1,23 @@ +import { create } from 'zustand' + +export interface LoadingStore { + isLoading: boolean + error: string | null + isDataLoaded: boolean + + setIsLoading: (isLoading: boolean) => void + setError: (error: string | null) => void + setIsDataLoaded: (isDataLoaded: boolean) => void + resetLoading: () => void +} + +export const useLoadingStore = create((set) => ({ + isLoading: false, + error: null, + isDataLoaded: false, + + setIsLoading: (isLoading) => set({ isLoading }), + setError: (error) => set({ error }), + setIsDataLoaded: (isDataLoaded) => set({ isDataLoaded }), + resetLoading: () => set({ isLoading: false, error: null, isDataLoaded: false }), +})) @@ -0,0 +1,47 @@ +import { create } from 'zustand' +import { persist } from 'zustand/middleware' + +import { ResponseGetPersons } from '#/features/invite-person-in-business' + +export interface PersonsStore { + securityList: ResponseGetPersons[] + personsList: ResponseGetPersons[] + + setSecurityList: (securityList: ResponseGetPersons[]) => void + setPersonsList: (personsList: ResponseGetPersons[]) => void + addPersonToList: (person: ResponseGetPersons, listType: 'personal' | 'security') => void + resetLists: () => void +} + +export const usePersonsStore = create()((set, get) => ({ + securityList: [], + personsList: [], + + setSecurityList: (securityList) => set({ securityList }), + setPersonsList: (personsList) => set({ personsList }), + + addPersonToList: (person, listType) => { + const state = get() + + if (listType === 'personal') { + const currentList = state.personsList || [] + set({ personsList: [...currentList, person] }) + return + } + + if (listType === 'security') { + const currentList = state.securityList || [] + set({ securityList: [...currentList, person] }) + } + }, + + resetLists: () => set({ securityList: [], personsList: [] }), + + // { + // name: 'business-persons-storage', + // partialize: (state) => ({ + // securityList: state.securityList, + // personsList: state.personsList, + // }), + // } +})) @@ -0,0 +1,31 @@ +import { create } from 'zustand' +import { persist } from 'zustand/middleware' + +export interface ReportsStore { + fromDate: string | undefined + toDate: string | undefined + + setFromDate: (fromDate: string | undefined) => void + setToDate: (toDate: string | undefined) => void + resetDates: () => void +} + +export const useReportsStore = create()( + persist( + (set) => ({ + fromDate: '', + toDate: '', + + setFromDate: (fromDate) => set({ fromDate }), + setToDate: (toDate) => set({ toDate }), + resetDates: () => set({ fromDate: '', toDate: '' }), + }), + { + name: 'business-reports-storage', + partialize: (state) => ({ + fromDate: state.fromDate, + toDate: state.toDate, + }), + } + ) +) @@ -0,0 +1,47 @@ +import React, { useState } from 'react' +import { Box, Button, Typography } from '@mui/material' +import { Dayjs } from 'dayjs' + +import businessStyles from '#/app/styles/business.module.scss' +import styles from '#/widgets/business-models/ui/models-list.module.scss' + +import { DateInput } from '#/shared/ui/date-input/date-input' +import ArrowUpOrDown from '#/widgets/top-bar-model/ui/arrow-up-or-down' + +interface ExpensesBlockProps { + onDownload: (fromDate?: string, toDate?: string) => Promise +} + +export const ExpensesBlock: React.FC = ({ onDownload }) => { + const [showPersons, setShowPersons] = useState(true) + const [fromDate, setFromDate] = useState('') + const [toDate, setToDate] = useState('') + + const handleDownload = () => { + onDownload(fromDate as string, toDate as string) + } + + return ( + + + + Затраты + setShowPersons((prev) => !prev)} + className={styles.arrow} + /> + + + {showPersons && ( + + + + + + )} + + ) +} @@ -0,0 +1,27 @@ +import React from 'react' + +import { Info } from '#/widgets/business-info' +import { InfoBusiness } from '#/widgets/business-info/api/get-info' + +interface InfoAdapterProps { + info: InfoBusiness | null | undefined + mailing: boolean | undefined + onMailingChange: (newMailing: boolean) => void +} + +export const InfoAdapter: React.FC = ({ info, mailing, onMailingChange }) => { + const handleMailingChange = ( + value: boolean | undefined | ((prev: boolean | undefined) => boolean | undefined) + ) => { + if (typeof value === 'function') { + const newValue = value(mailing) + if (newValue !== undefined) { + onMailingChange(newValue) + } + } else if (value !== undefined) { + onMailingChange(value) + } + } + + return +} @@ -0,0 +1,109 @@ +import React, { Dispatch, SetStateAction, memo, useEffect, useState } from 'react' +import { Box, Button, TextField, Typography } from '@mui/material' + +import businessStyles from '#/app/styles/business.module.scss' +import styles from '#/widgets/business-models/ui/models-list.module.scss' + +import { useThemeStore } from '#/entities/theme/model/use-theme-store' +import { InputStyleDark, InputStyleLight } from '#/shared' +import { InfoBusiness } from '#/widgets/business-info/api/get-info' +import ArrowUpOrDown from '#/widgets/top-bar-model/ui/arrow-up-or-down' + +interface MailingBlockProps { + info: InfoBusiness | null | undefined + mailing: boolean | undefined + emails: string[] + onUpdateEmails: (emails: string[]) => void +} + +export const MailingBlock: React.FC = memo(({ info, emails, mailing, onUpdateEmails }) => { + const [showPersons, setShowPersons] = useState(true) + const [newMail, setNewMail] = useState('') + const theme = useThemeStore((state) => state.theme) + + const addMailingUser = async () => { + if (newMail === '' || !info) return + onUpdateEmails([...emails, newMail]) + } + + const removeEmail = (emailToRemove: string) => { + if (!info) return + onUpdateEmails(emails.filter((item) => item !== emailToRemove)) + } + + if (!mailing) { + return null + } + + return ( + + + + Уведомления о низком балансе + setShowPersons((prev) => !prev)} + className={styles.arrow} + /> + + + {showPersons && ( + <> + + + setNewMail(e.target.value)} + fullWidth + sx={ + theme === 'light' + ? { ...InputStyleLight } + : { ...InputStyleDark } + } + /> + + + + + + {emails.map((el) => ( +
+

{el}

+ removeEmail(el)} + > + + + +
+ ))} +
+ + )} +
+ ) +}) @@ -0,0 +1,11 @@ +export * from './lib' +export * from './api' + +export { usePersonsStore } from './model/persons.store' +export { useBusinessInfoStore } from './model/business-info.store' +export { useReportsStore } from './model/reports.store' +export { useLoadingStore } from './model/loading.store' + +export { ExpensesBlock } from './ui/expenses-block' +export { InfoAdapter } from './ui/info-adapter' +export * from './ui/mailing-block' @@ -1,20 +0,0 @@ -import axios from 'axios' - -import { API_URL } from '#/shared/lib/constants' - -export const downloadFileReq = async (start_date: string, end_date: string, token?: string): Promise => { - try { - const { data } = await axios.get( - API_URL + `/auth/business-security/download-report?end_date=${end_date}&start_date=${end_date}`, - { - headers: { - Authorization: `Bearer ${token}`, - }, - } - ) - - return data - } catch (err) { - return null - } -} @@ -5,7 +5,7 @@ import Image from 'next/image' import { useCalculating } from '#/features/calculation-tokens-gpt/model/use-calculating' import TooltipCalculating from '#/features/calculation-tokens-gpt/ui/tooltip-calculating' -import { useAppSelector } from '#/app/store/store' +import { useThemeStore } from '#/entities/theme/model/use-theme-store' import { baseColor } from '#/shared/lib/constants/colors' import { ImessageContext } from '#/shared/lib/types/types-gpt' import { TypeModelGPT } from '#/widgets/filters-gpt/lib/constants' @@ -24,7 +24,7 @@ export const Calculation: React.FC = memo( ({ prompt, gptType, context, isAdditional, count, quality, model }) => { const price = useCalculating(model, count, quality, prompt, gptType, isAdditional, context) - const theme = useAppSelector((state) => state.theme.theme) + const theme = useThemeStore((state) => state.theme) return ( { - const theme = useAppSelector((state) => state.theme.theme) + const theme = useThemeStore((state) => state.theme) return ( @@ -0,0 +1,10 @@ +import { api } from '#/shared/api' +import { AllowedModels } from './types' + +export function addAccess(title: string) { + return api.put('/auth/business-host/allowed-models', { models: [title] }) +} + +export function removeAccess(title: string) { + return api.delete('/auth/business-host/allowed-models', { data: { models: [title] } }) +} @@ -0,0 +1 @@ +export * from './change-acess-to-model.routes' \ No newline at end of file @@ -1,24 +1,11 @@ import React from 'react' - import { SwitchCustom } from '#/shared' -import { changeAccess } from '../api/change-access' -import { AllowedModels } from '../api/types' type SwitchAccess = { isAllowed: boolean title: string - token?: string - updateAccessModels: (models: AllowedModels[]) => void + changeAccess: (title: string) => Promise } -export const SwitchAccess: React.FC = ({ isAllowed, title, token, updateAccessModels }) => { - const change = () => { - changeAccess(token, isAllowed, title).then((res) => { - if (res.length === 0) { - return - } - updateAccessModels(res) - }) - } - - return +export const SwitchAccess: React.FC = ({ isAllowed, title, changeAccess }) => { + return changeAccess(title)} /> } @@ -1,2 +1,3 @@ export type { AllowedModels } from './api/types' export { SwitchAccess } from './ui/switch' +export * from './api' \ No newline at end of file @@ -0,0 +1,7 @@ +import { ResponseGetPersons } from '#/features/business-group' +import { api } from '#/shared/api' +import { ChangeLimitDTO } from '../types' + +export function putLimit({ email, ...dto }: Partial) { + return api.put(`/auth/business-host/accounts/${email}`, dto) +} @@ -0,0 +1 @@ +export * from './change-limit.routes' \ No newline at end of file @@ -1,26 +0,0 @@ -import axios, { AxiosResponse } from 'axios' - -import { ResponseGetPersons } from '#/features/invite-person-in-business' -import { API_URL } from '#/shared/lib/constants' - -import { InviteRoles, inviteRoles } from '../../invite-person-in-business/lib/constants' - -export const updateLimit = async (email?: string, limit?: string, token?: string, role?: InviteRoles) => { - try { - const { data } = await axios.put<{ token_limit: string; role: string }, AxiosResponse>( - API_URL + `/auth/business-host/accounts/${email}`, - { - token_limit: limit, - account_privileges: Object.entries(inviteRoles).find(([key, value]) => value === role)![0], - }, - { - headers: { - Authorization: `Bearer ${token}`, - }, - } - ) - return data - } catch (err) { - return null - } -} @@ -0,0 +1,2 @@ +export * from './use-limit-modal' +export * from './use-limit-modal-form' \ No newline at end of file @@ -0,0 +1,62 @@ +import { ResponseGetPersons, InviteUserForm } from '#/features/invite-person-in-business' +import { invitePerson } from '#/features/invite-person-in-business/api' +import { withPending } from '#/shared' +import { makePrivateRequest } from '#/shared/api' +import { useShowDataStore } from '#/shared/lib/hooks' +import { useForm } from 'react-hook-form' +import { putLimit } from '../api' +import { LimitModalForm, LimitModalListType } from '../types' + +export function useLimitModalForm( + list: ResponseGetPersons[], + setList: (list: ResponseGetPersons[]) => void, + addList: (newUser: ResponseGetPersons, list: 'personal' | 'security') => void, + person: ResponseGetPersons | null, + onClose?: Function +) { + const { + handleSubmit, + register, + reset, + setValue, + getValues, + watch, + formState: { errors }, + } = useForm() + + const { showMessage } = useShowDataStore() + + const [onSubmit, loading] = withPending( + handleSubmit( + makePrivateRequest(async (dto) => { + if (!person) return + + if (dto.token_limit === '') dto.token_limit = undefined + + const { data, status } = await putLimit({ ...dto, email: person.email }) + + if (![201, 200].includes(status)) return showMessage((data as { detail: string }).detail) + + setList([...list.filter((e) => e.uid !== data.uid)]) + + addList(data, data.account_type === 'business_security' ? 'security' : 'personal') + + onClose && onClose() + + reset() + }), + (errors) => showMessage(Object.values(errors)[0].message ?? 'Неверные данные') + ) + ) + + return { + register, + reset, + setValue, + getValues, + onSubmit, + loading, + watch, + errors, + } +} @@ -0,0 +1,19 @@ +import { useMemo, useState } from 'react' +import { useShowDataStore } from '#/shared/lib/hooks' +import { makePrivateRequest } from '#/shared/api' +import { getBusinessGroups } from '#/features/business-group/api' +import { SelectItem } from '#/shared/ui/common-select' +import { inviteRoles } from '#/features/business-group/lib/constants' + +export function useLimitModal() { + + const personRoles = useMemo( + () => Object.entries(inviteRoles).map(([k, v]) => ({ label: v, value: k })), + [] + ) + + + return { + personRoles, + } +} @@ -0,0 +1,2 @@ +export * from './limit-modal-form' +export * from './limit-modal' \ No newline at end of file @@ -0,0 +1,4 @@ +import { ChangeLimitDTO } from "./limit-modal"; + + +export interface LimitModalForm extends Partial> {} \ No newline at end of file @@ -0,0 +1,9 @@ +export type InviteRole = 'regular' | 'admin' | 'sec' + +export interface ChangeLimitDTO { + account_privileges: InviteRole + token_limit: string + email: string +} + +export type LimitModalListType = 'personal' | 'security' \ No newline at end of file @@ -0,0 +1,31 @@ +.wrap { + padding-top: 60px; + min-width: 360px; + &__role{ + margin-bottom: 20px; + } + + &__group{ + margin-bottom: 20px; + } + + .title { + font-size: 21px; + font-weight: 500; + } + .hint { + margin-top: 15px; + font-size: 15px; + font-weight: 500; + margin-bottom: 5px; + } + .buttons { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 10px; + width: 100%; + margin-top: 15px; + text-align: right; + } +} @@ -1,115 +1,90 @@ -import React, { useEffect, useState } from 'react' -import { Box, TextField, Typography } from '@mui/material' -import { useSession } from 'next-auth/react' +import React from 'react' +import { Box, Typography } from '@mui/material' import { ResponseGetPersons } from '#/features/invite-person-in-business' -import { useAppSelector } from '#/app/store/store' -import { ButtonUI, InputStyleDark, InputStyleLight, Modal, Select } from '#/shared' -import styles from '#/widgets/business-models/ui/models-list/models-list.module.scss' -import styles2 from '#/widgets/business-persons/ui/persons-list/persons-list.module.scss' +import { Loader } from '#/shared' -import { InviteRoles, inviteRoles } from '../../invite-person-in-business/lib/constants' -import { updateLimit } from '../api/set-limit' +import styles from './limit-modal.module.scss' + +import { inviteRolesSelect } from '../../invite-person-in-business/lib/constants' +import { InviteRole } from '../types' +import { useLimitModal, useLimitModalForm } from '../model' +import { CommonInput } from '#/shared/ui/common-input' +import { CommonSelect } from '#/shared/ui/common-select' +import { CommonButton } from '#/shared/ui/button' +import { LIMIT_MODAL, PlateTemplate, getModalById } from '#/features/modals' interface LimitModalProps { person: ResponseGetPersons | null - onClose: () => void - updateLimitProp: (limit: string, email?: string) => void - list?: ResponseGetPersons[] | null - setList: React.Dispatch> - listType: 'personal' | 'security' - addList: (newUser: ResponseGetPersons, list: 'personal' | 'security') => void + list: ResponseGetPersons[] + addList: (newUser: ResponseGetPersons, list: 'personal' | 'security') => void + setList: (list: ResponseGetPersons[]) => void + id?: string } -export const LimitModal: React.FC = ({ - person, - onClose, - updateLimitProp, - list, - setList, - addList, - listType, -}) => { - const theme = useAppSelector((state) => state.theme.theme) - const [limit, setLimit] = useState(person?.token_limit ? Math.floor(+person.token_limit).toString() : '0') - const [role, setRole] = useState('Сотрудник') - const { data } = useSession() - - const handleChangeBalance = () => { - updateLimit(person?.email, limit, data?.access, role).then((res) => { - if (res === null) { - return - } else if (list && res.account_type !== person?.account_type) { - if ( - (res.account_type === 'business_host' || - res.account_type === 'business_account' || - res.account_type === 'business_admin') && - listType === 'security' - ) { - setList(list.filter((el) => el.email !== res.email)) - addList(res, 'personal') - } else { - setList(list.filter((el) => el.email !== res.email)) - addList(res, listType) - } - } +export const LimitModal: React.FC = ({ id = LIMIT_MODAL, addList, person, list, setList }) => { + const modal = getModalById(id) - updateLimitProp(res.token_limit, person?.email) - }) - } + const { register, reset, watch, setValue, errors, onSubmit, loading } = useLimitModalForm( + list, + setList, + addList, + person, + () => modal.setState(false) + ) - useEffect(() => { - if (person?.account_type === 'business_account') { - setRole('Сотрудник') - } - if (person?.account_type === 'business_admin') { - setRole('Администратор') - } - if (person?.account_type === 'business_security') { - setRole('Сотрудник безопасности') - } - }, [person]) + const { personRoles } = useLimitModal() return ( - { - setLimit('0') - onClose() - }} - > - - Сотрудник {person?.email} - + +
+
+

+ Сотрудник {person ? person.email : ''} +

+ +
+

Изменить роль

+ setValue('account_privileges', value as InviteRole)} + items={personRoles} + /> +
+ +

Изменить лимит токенов

- - - Изменение лимита токенов - - setLimit(e.target.value)} - fullWidth - sx={theme === 'light' ? { ...InputStyleLight } : { ...InputStyleDark }} - /> - - - - Изменение роли - - - + - - Отмена - - + {loading ? ( +
+ +
+ ) : ( +
+ modal.setState(false)} + variant='gray' + > + Отмена + + Отправить +
+ )} +
+
+
) } @@ -1,50 +0,0 @@ -import axios, { AxiosResponse } from 'axios' - -import { API_URL } from '#/shared/lib/constants' - -import { AllowedModels } from './types' -export const changeAccess = async ( - token: string | undefined, - value: boolean, - title: string -): Promise => { - if (!value) { - try { - const { data } = await axios.put<{ models: string }, AxiosResponse>( - API_URL + '/auth/business-host/allowed-models', - { - models: [title], - }, - { - headers: { - Authorization: `Bearer ${token}`, - }, - } - ) - return data - } catch (err) { - return [] - } - } - - if (value) { - try { - const { data } = await axios.delete<{ models: string }, AxiosResponse>( - API_URL + '/auth/business-host/allowed-models', - { - headers: { - Authorization: `Bearer ${token}`, - }, - data: { - models: [title], - }, - } - ) - return data - } catch (err) { - return [] - } - } - - return [] -} @@ -1,4 +1,4 @@ -import { useAppSelector } from '#/app/store/store' + import { MessageSend } from '#/entities/message' import { Inference, useChatBotParams } from '#/entities/model-entity' import { acceptTypes } from '#/shared/common-load-file/config' @@ -1,4 +1,4 @@ -import { useAppSelector } from '#/app/store/store' + import { getImagesBySlug, Message, useChatBotMessages } from '#/entities/message' import { Device } from '#/shared/lib/types/entities' import { getImagesGalery } from '#/widgets/messages' @@ -2,13 +2,12 @@ import React, { Dispatch, SetStateAction, createRef, useEffect, useMemo, useStat import axios from 'axios' import { useSession } from 'next-auth/react' -import { createChat, getAllChats } from '#/shared/api/endpoints' -import { API_URL } from '#/shared/lib/constants' +import { API_URL } from '../../../shared/lib/constants' import { useCurrentChat } from '.' import { useRouter } from 'next/router' -import { ChatDTO, getChats, postChat, useChatsStore } from '#/entities/chat' -import { makePrivateRequest } from '#/shared/api' -import { useShowDataStore } from '#/shared/lib/hooks' +import { ChatDTO, getChats, postChat, useChatsStore } from '../../../entities/chat' +import { makePrivateRequest } from '../../../shared/api' +import { useShowDataStore } from '../../../shared/lib/hooks' export function useChats() { const { query } = useRouter() @@ -42,7 +41,7 @@ export function useChats() { chat, chats, fetchChats, - createChat, + // createChat, setCurrentChat, chatsWithRefs, } @@ -1,7 +1,7 @@ import React, { ReactNode } from 'react' import { Menu, MenuItem } from '@mui/material' -import { useAppSelector } from '#/app/store/store' +import { useThemeStore } from '#/entities/theme/model/use-theme-store' interface IProps { children: ReactNode @@ -12,7 +12,7 @@ interface IProps { } export const ContextMenu = ({ children, pointX, pointY, clicked, handleClose }: IProps) => { - const theme = useAppSelector((state) => state.theme.theme) + const theme = useThemeStore((state) => state.theme) return ( { - const email = useAppSelector((state) => state.user.email) + const { getUser } = useUserStore() + const { email } = getUser() const [file, setFile] = React.useState() const { showMessage } = useShowDataStore() @@ -3,9 +3,9 @@ import { KeyboardArrowLeft, KeyboardArrowRight } from '@mui/icons-material' import { Box, Button, MobileStepper, Table, TableBody, TableCell, TableHead, TableRow, Typography } from '@mui/material' import CircularProgress from '@mui/material/CircularProgress' -import { change } from '#/entities/theme' + import { TranslateFields } from '#/features/get-admin-stats/lib/constants' -import { useAppDispatch } from '#/app/store/store' + import { TypographyDark, TypographyLight } from '#/shared/lib/constants/styles' import { IStatsProps } from '../model/types' @@ -18,10 +18,10 @@ export const Stats: React.FC = ({ token, device, product, group_by, const light = theme === 'light' - const dispatch = useAppDispatch() + useEffect(() => { - dispatch(change('light')) + // TODO: Add theme change to Zustand store }, []) return ( @@ -6,7 +6,7 @@ import { useImageBotMessages, useImageMessagesEvents, } from '#/entities/message' -import { useAppDispatch } from '#/app/store/store' + import { Device } from '#/shared/lib/types/entities' import { MutableRefObject, RefObject, useEffect, useState } from 'react' import { useShowDataStore } from '#/shared/lib/hooks' @@ -31,7 +31,7 @@ export function useImageBotCreateImage( const { setMessages, messages } = useImageBotMessages() - const dispatch = useAppDispatch() + const { data: session } = useSession() @@ -51,7 +51,7 @@ export function useImageBotCreateImage( if (status !== 200) return showMessage((data as { detail: string }).detail ?? 'Ошибка при получении сообщений') - dispatch(getUserBalance(session?.access)) + // TODO: Add getUserBalance to Zustand store const messages = useImageBotMessages.getState().messages @@ -1,10 +1,11 @@ -import { useAppSelector } from '#/app/store/store' + import { useState } from 'react' +import { useParamsStore } from '#/app/store/use-params-store' export function useImagesBotFilters() { const [openFiltersMobile, setOpenFiltersMobile] = useState(false) const [params, setParams] = useState(false) - const includeParams = useAppSelector((state) => state.params.params) + const includeParams = useParamsStore((state) => state.params) return { openFiltersMobile, @@ -1,4 +1,4 @@ -import { useAppSelector } from '#/app/store/store' + import { getImagesBySlug, Message, useImageBotMessages } from '#/entities/message' import { Device } from '#/shared/lib/types/entities' import { getImagesGalery } from '#/widgets/messages' @@ -9,6 +9,7 @@ import { LimitSize, Limit } from '../types' import { useRouter } from 'next/router' import { useShowDataStore } from '#/shared/lib/hooks/use-show-data' import { useImageObjectId } from '#/features/image-object-id' +import { makePrivateRequest } from '#/shared/api' export function useImageBotPagination(deviceType: Device) { const refScrollMobile = useRef(null) @@ -47,14 +48,13 @@ export function useImageBotPagination(deviceType: Device) { }, } - const fetchMessages = async (count?: number) => { + const fetchMessages = makePrivateRequest(async (count?: number) => { if (!data) return setLoading(true) const { data: answer, ...response } = await getImagesBySlug( query.slug as string, - data.access, offset.current, count || 10 ) @@ -72,7 +72,7 @@ export function useImageBotPagination(deviceType: Device) { // console.log(offset.current) setImageObjectId(answer.id) - } + }) const callback = async function (entries: IntersectionObserverEntry[]) { if (!entries[0].isIntersecting) return @@ -0,0 +1 @@ +export * from './invite-persons.routes' \ No newline at end of file @@ -1,27 +0,0 @@ -import axios, { AxiosResponse } from 'axios' - -import { API_URL } from '#/shared/lib/constants' - -import { InviteRoles, inviteRoles } from '../lib/constants' -import { ResponseGetPersons } from '../model/types' -export const invitePerson = async ( - role: InviteRoles, - limit: string, - email: string, - group?: string, - token?: string -) => { - return await axios.post<{ email: string; token_limit: string }, AxiosResponse>( - API_URL + '/auth/business-host', - { - parent_company: group === '' ? null : group, - email, - account_privileges: Object.entries(inviteRoles).find(([key, value]) => value === role)![0], - }, - { - headers: { - Authorization: `Bearer ${token}`, - }, - } - ) -} @@ -0,0 +1,8 @@ +import { inviteRoles } from '../lib/constants' +import { ResponseGetPersons } from '../model/types' +import { api } from '#/shared/api' +import { InvitePersonDTO } from '../types' + +export function invitePerson({ ...dto }: InvitePersonDTO) { + return api.post('/auth/business-host', dto) +} @@ -1,6 +1,7 @@ -import { PersonInBusiness } from '#/features/invite-person-in-business' +import { InviteRole, PersonInBusiness } from '#/features/invite-person-in-business' type Roles = 'Владелец' | 'Сотрудник' | 'Администратор' | 'Сотрудник безопасности' + export const RoleSelect: Record = { business_host: 'Владелец', business_account: 'Сотрудник', @@ -8,10 +9,16 @@ export const RoleSelect: Record = { business_security: 'Сотрудник безопасности', } -export type InviteRoles = 'Сотрудник' | 'Администратор' | 'Сотрудник безопасности' - -export const inviteRoles = { +export const inviteRoles: Record = { regular: 'Сотрудник', admin: 'Администратор', sec: 'Сотрудник безопасности', } + + +export const inviteRolesSelect: Record = { + business_account: 'regular', + business_admin: 'admin', + business_security: 'sec', + business_host: 'admin', +} @@ -0,0 +1,2 @@ +export * from './use-invite-user-form' +export * from './use-invite-modal' \ No newline at end of file @@ -0,0 +1,39 @@ +import { useMemo, useState } from 'react' +import { inviteRoles } from '../lib/constants' +import { ResponseGetBusinessGroups } from './types' +import { useShowDataStore } from '#/shared/lib/hooks' +import { makePrivateRequest } from '#/shared/api' +import { getBusinessGroups } from '#/features/business-group/api' +import { SelectItem } from '#/shared/ui/common-select' + +export function useInviteModal() { + const [businessGroups, setBusinessGroups] = useState([]) + + const { showMessage } = useShowDataStore() + + const personRoles = useMemo( + () => Object.entries(inviteRoles).map(([k, v]) => ({ label: v, value: k })), + [] + ) + + const bussinesGroupsItems = useMemo( + () => businessGroups.map((group) => ({ label: group.title, value: group.uid })), + [businessGroups] + ) + + const fetchBusinessGroups = makePrivateRequest(async () => { + const { data, status } = await getBusinessGroups() + + if (status !== 200) return showMessage('Ошибка получения бизнес груп') + + setBusinessGroups(data) + }) + + return { + fetchBusinessGroups, + businessGroups, + setBusinessGroups, + personRoles, + bussinesGroupsItems, + } +} @@ -0,0 +1,50 @@ +import { useForm } from 'react-hook-form' +import { InviteUserForm } from '../types' +import { makePrivateRequest } from '#/shared/api' +import { Dispatch, SetStateAction, useState } from 'react' +import { ResponseGetBusinessGroups, ResponseGetPersons } from './types' +import { useShowDataStore } from '#/shared/lib/hooks' +import { withPending } from '#/shared' +import { invitePerson } from '../api' + +export function useInviteUserForm(showNewPersons: (persons: ResponseGetPersons) => void, onClose?: Function) { + const { + handleSubmit, + register, + reset, + setValue, + getValues, + watch, + formState: { errors }, + } = useForm() + + const { showMessage } = useShowDataStore() + + const [onSubmit, loading] = withPending( + handleSubmit( + makePrivateRequest(async (dto) => { + const { data, status } = await invitePerson({ ...dto }) + + if (![201, 200].includes(status)) return showMessage((data as { detail: string }).detail) + + showNewPersons(data as ResponseGetPersons) + + onClose && onClose() + + reset() + }), + (errors) => showMessage(Object.values(errors)[0].message ?? 'Неверные данные') + ) + ) + + return { + register, + reset, + setValue, + getValues, + onSubmit, + loading, + watch, + errors, + } +} @@ -0,0 +1,3 @@ +export * from './persons.types' +export * from './invite-person' +export * from './invite-user-form' \ No newline at end of file @@ -0,0 +1,8 @@ +export type InviteRole = 'regular' | 'admin' | 'sec' + +export interface InvitePersonDTO { + account_privileges: InviteRole + limit: string + email: string + parent_company?: string +} @@ -0,0 +1,4 @@ +import { InvitePersonDTO } from "./invite-person"; + + +export interface InviteUserForm extends InvitePersonDTO {} @@ -0,0 +1,33 @@ +export type PersonInBusiness = 'business_host' | 'business_account' | 'business_admin' | 'business_security' + +export type AcceptanceStatus = 'pending' | 'accepted' | 'rejected' | 'cancelled' + +export type ResponseGetPersons = { + detail?: string + uid: string + email: string + account_type: PersonInBusiness + acceptance_status: AcceptanceStatus + created_at: string + updated_at: string + token_limit: string +} + +export type ResponseGetIpList = { + ips: string[] +} + +export type ResponseGetLogsList = { + action_time: string + message: string + user: string +} + +export type ResponseGetBusinessGroups = { + detail?: string + uid: string + title: string + token_limit: string + parent_company: string + accounts: ResponseGetPersons[] +} @@ -0,0 +1 @@ +export * from './invite-security-modal' \ No newline at end of file @@ -1,4 +1,14 @@ .wrap { + padding-top: 60px; + min-width: 320px; + &__role{ + margin-bottom: 20px; + } + + &__group{ + margin-bottom: 20px; + } + .title { font-size: 21px; font-weight: 500; @@ -13,9 +23,7 @@ display: flex; align-items: center; justify-content: flex-end; - div { - margin-right: 10px; - } + gap: 10px; width: 100%; margin-top: 15px; text-align: right; @@ -1,132 +1,90 @@ import React, { FC, useEffect, useState } from 'react' -import { useForm } from 'react-hook-form' -import { SubmitErrorHandler } from 'react-hook-form/dist/types/form' -import { Box, TextField, Typography } from '@mui/material' -import { useSession } from 'next-auth/react' - -import { invitePerson } from '#/features/invite-person-in-business/api/invite-person' -import { IEmailForms } from '#/features/register-by-email/model/types' -import { useAppSelector } from '#/app/store/store' -import { - ButtonGray, - ButtonUI, - emailOptions, - Error, - InputStyleDark, - InputStyleLight, - Loader, - Modal, - ModalProps, - onlyNumbersOption, - Select, -} from '#/shared' -import { getBusinessGroups } from '#/widgets/business-persons/api/get-businessGroups' - -import { InviteRoles, inviteRoles } from '../lib/constants' -import { ResponseGetBusinessGroups, ResponseGetPersons } from '../model/types' +import { Loader } from '#/shared' +import { ResponseGetPersons } from '../model/types' import styles from './invite-modal.module.scss' -import { useShowDataStore } from '#/shared/lib/hooks/use-show-data' - -interface InviteModalProps extends ModalProps { +import { useInviteModal, useInviteUserForm } from '../model' +import { CommonButton } from '#/shared/ui/button' +import { getEmailOptions } from '#/shared/lib/constants/hook-form-options' +import { CommonInput } from '#/shared/ui/common-input' +import { CommonSelect } from '#/shared/ui/common-select' +import { InviteRole } from '../types' +import { INVITE_USER, PlateTemplate, getModalById } from '#/features/modals' + +interface InviteModalProps { showNewPersons: (persons: ResponseGetPersons) => void } -export const InviteModal: FC = ({ open, onClose, showNewPersons }) => { - const [role, setRole] = useState('Сотрудник') - const { handleSubmit, register, reset, setValue, getValues } = useForm() - 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) => { - setIsLoading(true) - 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.data.detail && res.status >= 300) { - showMessage(res.data.detail) - setIsLoading(false) - return - } - if (res === null) { - showMessage('Что-то пошло не так') - setIsLoading(false) - return - } - - setIsLoading(false) - onClose() - - reset() +export const InviteModal = ({ showNewPersons }: InviteModalProps) => { + const modal = getModalById(INVITE_USER) - showNewPersons(res.data) - } - const checkError: SubmitErrorHandler = (data) => { - showMessage(Object.values(data)[0].message || 'Неверные данные') - } - - const handleEmailChange = (event: any) => { - const emailWithoutSpaces = event.target.value.trim() - setValue('email', emailWithoutSpaces) - } + const { onSubmit, register, reset, loading, watch, setValue } = useInviteUserForm(showNewPersons, () => + modal.setState(false) + ) - const theme = useAppSelector((state) => state.theme.theme) + const { personRoles, bussinesGroupsItems, fetchBusinessGroups } = useInviteModal() useEffect(() => { - if (open) { - getBusinessGroups(session?.access).then((res) => setBusinessGroups(res ? res : null)) - } - }, [session?.access, open]) + fetchBusinessGroups() + }, []) - const personRoles = { regular: 'Сотрудник', admin: 'Администратор' } + useEffect(() => { + reset() + }, [modal.state]) return ( - -
- - Пригласить сотрудника - - {/* Поле с выбором роли сотрудника */} - Выберите роль - - - {/* Поле с выбором группы */} - Выберите бизнес-группу - - - Укажите адрес электронной почты - { - handleEmailChange(e) - }} + + +
+

Пригласить сотрудника

+ +
+

Выберите роль

+ + setValue('account_privileges', value as InviteRole)} + items={personRoles} + /> +
+ +
+

Выберите бизнес-группу

+ + setValue('parent_company', value)} + items={bussinesGroupsItems} + /> +
+ + - {isLoading ? ( - + {loading ? ( +
- +
) : ( - - - - - - +
+ modal.setState(false)} + variant='gray' + > + Отмена + + Отправить +
)} -
+
-
+ ) } @@ -0,0 +1,31 @@ +.wrap { + padding-top: 60px; + min-width: 320px; + &__role{ + margin-bottom: 20px; + } + + &__group{ + margin-bottom: 20px; + } + + .title { + font-size: 21px; + font-weight: 500; + } + .hint { + margin-top: 15px; + font-size: 15px; + font-weight: 500; + margin-bottom: 5px; + } + .buttons { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 10px; + width: 100%; + margin-top: 15px; + text-align: right; + } +} @@ -0,0 +1,90 @@ +import React, { FC, useEffect, useState } from 'react' +import { Loader } from '#/shared' +import { ResponseGetPersons } from '../model/types' + +import styles from './invite-security-modal.module.scss' +import { useInviteModal, useInviteUserForm } from '../model' +import { CommonButton } from '#/shared/ui/button' +import { getEmailOptions } from '#/shared/lib/constants/hook-form-options' +import { CommonInput } from '#/shared/ui/common-input' +import { CommonSelect } from '#/shared/ui/common-select' +import { InviteRole } from '../types' +import { INVITE_SECURITY_MODAL, INVITE_USER, PlateTemplate, getModalById } from '#/features/modals' + +interface InviteModalProps { + showNewPersons: (persons: ResponseGetPersons) => void +} + +export const InviteSecurityModal = ({ showNewPersons }: InviteModalProps) => { + const modal = getModalById(INVITE_SECURITY_MODAL) + + const { onSubmit, register, reset, loading, watch, setValue } = useInviteUserForm(showNewPersons, () => + modal.setState(false) + ) + + const { personRoles, bussinesGroupsItems, fetchBusinessGroups } = useInviteModal() + + useEffect(() => { + fetchBusinessGroups() + }, []) + + useEffect(() => { + reset() + }, [modal.state]) + + return ( + +
+
+

Пригласить сотрудника

+ +
+

Выберите роль

+ + setValue('account_privileges', value as InviteRole)} + items={personRoles} + /> +
+ +
+

Выберите бизнес-группу

+ + setValue('parent_company', value)} + items={bussinesGroupsItems} + /> +
+ + + {loading ? ( +
+ +
+ ) : ( +
+ modal.setState(false)} + variant='gray' + > + Отмена + + Отправить +
+ )} +
+
+
+ ) +} @@ -2,3 +2,7 @@ export { RoleSelect } from './lib/constants' export type { PersonInBusiness } from './model/types' export type { ResponseGetPersons } from './model/types' export { InviteModal } from './ui/invite-modal' + + +export * from './ui' +export * from './types' \ No newline at end of file @@ -3,3 +3,10 @@ export const RESEND_INVATION_PASSWORD = 'resend-invation-password' export const ERROR_REPORT = 'error-report' export const GALLERY_IMAGES = 'gallery-images' + +export const CHANGE_BUSINESS_GROUP = 'change-business-group' +export const ADD_BUSINESS_GROUP = 'add-business-group' +export const INVITE_USER = 'invite-user' +export const INVITE_SECURITY_MODAL = 'invite-security' +export const LIMIT_MODAL = 'limit-modal' +export const LIMIT_SECURITY_MODAL = 'limit-security-modal' @@ -1,17 +0,0 @@ -import { createSlice } from '@reduxjs/toolkit' - -export const pendingSlice = createSlice({ - name: 'pendingSlice', - initialState: { - userLoading: true, - }, - reducers: { - change: (state, action) => { - state.userLoading = action.payload - }, - }, -}) - -export const { change } = pendingSlice.actions - -export default pendingSlice @@ -0,0 +1,25 @@ +import { create } from 'zustand' + +export interface PendingStore { + userLoading: boolean + userLoaded: boolean + change: (loading: boolean) => void + setLoaded: (loaded: boolean) => void +} + +export const usePendingStore = create((set, get) => { + function change(loading: boolean) { + set({ userLoading: loading }) + } + + function setLoaded(loaded: boolean) { + set({ userLoaded: loaded }) + } + + return { + userLoading: true, + userLoaded: false, + change, + setLoaded, + } +}) @@ -8,7 +8,7 @@ import { } from '#/features/register-business/lib/constants-step-information' import { API_URL } from '#/shared/lib/constants' -import { DataForCreate } from '../../model/stepper-slice' + import { DataForCreateBusiness } from './types' @@ -1,18 +0,0 @@ -import { useEffect } from 'react' -import { UseFormReturn } from 'react-hook-form' - -import { useAppDispatch } from '#/app/store/store' - -import { getDataCompany } from '../stepper-slice' - -export const useAutoLoadingInfo = (methods: UseFormReturn<{ companyName: string; inn: string; ogrn: string }, any>) => { - const company = methods.watch('companyName') - - const dispatch = useAppDispatch() - - useEffect(() => { - if (company.trim()) { - dispatch(getDataCompany(company)) - } - }, [company]) -} @@ -1,137 +0,0 @@ -import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit' - -import { createAccount } from '#/features/register-business/api/create-account/create-account' -import { CompanyData } from '#/features/register-business/api/get-company-date/types' -import { loadingThunk, RootState } from '#/app/store/store' - -import { getCompanyData } from '../api/get-company-date/get-company-date' -import { FieldActivity, Frequency } from '../lib/constants-step-information' - -export type DataForCreate = { - fieldActivity: FieldActivity - frequency: Frequency - numberStuff: number - companyName: string | '' - inn: string | '' - ogrn: string | '' - name: string - email: string - phone: string - job_title: string -} - -type StepperState = { - activeStep: number - loadingCreate: loadingThunk - company: CompanyData[] | [] - dataForCreate: DataForCreate -} - -const initialState: StepperState = { - activeStep: 0, - company: [], - loadingCreate: 'idle', - dataForCreate: { - fieldActivity: 'IT', - frequency: 'Часто', - numberStuff: 1, - companyName: '', - inn: '', - ogrn: '', - name: '', - email: '', - phone: '', - job_title: '', - }, -} -export const getDataCompany = createAsyncThunk('stepper/getDataCompany', async (prompt: string) => { - return await getCompanyData(prompt) -}) - -export const createBusinessCompany = createAsyncThunk( - 'stepper/createAccount', - async (token: string | null, { getState }) => { - const state = getState() - return await createAccount(state.stepper.dataForCreate, token) - } -) - -export const stepperSlice = createSlice({ - name: 'stepperSlice', - initialState, - reducers: { - switchNextStep(state) { - state.activeStep++ - }, - switchPreviousStep(state) { - state.activeStep-- - }, - switchStep(state, action: PayloadAction) { - state.activeStep = action.payload - }, - setFieldActivity(state, action: PayloadAction) { - state.dataForCreate.fieldActivity = action.payload - }, - setFrequency(state, action: PayloadAction) { - state.dataForCreate.frequency = action.payload - }, - setNumberStuff(state, action: PayloadAction) { - state.dataForCreate.numberStuff = action.payload - }, - setCompanyName(state, action: PayloadAction) { - state.dataForCreate.companyName = action.payload - }, - setInn(state, action: PayloadAction) { - state.dataForCreate.inn = action.payload - }, - setOgrn(state, action: PayloadAction) { - state.dataForCreate.ogrn = action.payload - }, - setProcessCreate(state, action: PayloadAction) { - state.loadingCreate = action.payload - }, - setContact( - state, - action: PayloadAction<{ - name: string - email: string - phone: string - job_title: string - }> - ) { - const { phone, email, name, job_title } = action.payload - state.dataForCreate.phone = phone - state.dataForCreate.email = email - state.dataForCreate.name = name - state.dataForCreate.job_title = job_title - }, - }, - extraReducers: (builder) => { - builder.addCase(getDataCompany.fulfilled, (state, action) => { - state.company = action.payload?.suggestions || [] - }), - builder.addCase(createBusinessCompany.pending, (state) => { - state.loadingCreate = 'pending' - }) - builder.addCase(createBusinessCompany.fulfilled, (state) => { - state.loadingCreate = 'succeeded' - }) - builder.addCase(createBusinessCompany.rejected, (state) => { - state.loadingCreate = 'failed' - }) - }, -}) - -export const { - setProcessCreate, - switchPreviousStep, - switchNextStep, - setFrequency, - setFieldActivity, - setNumberStuff, - setInn, - setCompanyName, - setOgrn, - setContact, - switchStep, -} = stepperSlice.actions @@ -0,0 +1,57 @@ +import { create } from 'zustand' + +export interface StepperStore { + activeStep: number + loadingCreate: 'idle' | 'pending' | 'succeeded' | 'failed' + dataForCreate: any + company: any + switchStep: (step: number) => void + switchPreviousStep: () => void + switchNextStep: () => void + setLoadingCreate: (loading: 'idle' | 'pending' | 'succeeded' | 'failed') => void + setDataForCreate: (data: any) => void + setCompany: (company: any) => void +} + +export const useStepperStore = create((set, get) => { + function switchStep(step: number) { + set({ activeStep: step }) + } + + function switchPreviousStep() { + const currentStep = get().activeStep + if (currentStep > 0) { + set({ activeStep: currentStep - 1 }) + } + } + + function switchNextStep() { + const currentStep = get().activeStep + set({ activeStep: currentStep + 1 }) + } + + function setLoadingCreate(loading: 'idle' | 'pending' | 'succeeded' | 'failed') { + set({ loadingCreate: loading }) + } + + function setDataForCreate(data: any) { + set({ dataForCreate: data }) + } + + function setCompany(company: any) { + set({ company }) + } + + return { + activeStep: 0, + loadingCreate: 'idle', + dataForCreate: {}, + company: null, + switchStep, + switchPreviousStep, + switchNextStep, + setLoadingCreate, + setDataForCreate, + setCompany, + } +}) \ No newline at end of file @@ -5,22 +5,26 @@ import { SubmitErrorHandler } from 'react-hook-form/dist/types/form' import { Box, TextField, Typography } from '@mui/material' import { useSession } from 'next-auth/react' +import { nameOptions } from '../../lib/constants-step-contact' + import styles from '#/features/register-business/ui/step-information/step-information.module.scss' + +import { useThemeStore } from '#/entities/theme/model/use-theme-store' +import { useStepperStore } from '#/features/register-business/model/use-stepper-store' import { IEmailForms } from '#/features/register-by-email/model/types' -import { useAppDispatch, useAppSelector } from '#/app/store/store' import { ButtonUI, Error, InputStyleDark, InputStyleLight } from '#/shared' import { emailOptions } from '#/shared' import { phoneOptions } from '#/shared/lib/constants/hook-form-options' - -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 { dataForCreate, setDataForCreate } = useStepperStore((state) => ({ + dataForCreate: state.dataForCreate, + setDataForCreate: state.setDataForCreate, + })) + + const { phone, email, name, job_title } = dataForCreate + const theme = useThemeStore((state) => state.theme) const { showMessage } = useShowDataStore() const methods = useForm({ defaultValues: { @@ -30,7 +34,6 @@ export const StepContact = () => { job_title, }, }) - const dispatch = useAppDispatch() const checkError: SubmitErrorHandler = (data) => { showMessage(Object.values(data)[0].message || 'Неверные данные') @@ -39,8 +42,7 @@ export const StepContact = () => { const { data: session } = useSession() const onSubmit = (data: any) => { const { name, email, phone, job_title } = data - dispatch(setContact({ name, email, phone, job_title })) - dispatch(createBusinessCompany(session?.access || null)) + setDataForCreate({ ...dataForCreate, name, email, phone, job_title }) } return ( @@ -6,19 +6,24 @@ import { Box, TextField, Typography } from '@mui/material' 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 { useThemeStore } from '#/entities/theme/model/use-theme-store' +import { useStepperStore } from '#/features/register-business/model/use-stepper-store' import { ButtonUI, InputStyleDark, InputStyleLight } from '#/shared' import { Error } from '#/shared' 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 { dataForCreate, setDataForCreate, switchNextStep } = useStepperStore((state) => ({ + dataForCreate: state.dataForCreate, + setDataForCreate: state.setDataForCreate, + switchNextStep: state.switchNextStep + })) - const { frequency, fieldActivity, numberStuff } = useAppSelector((state) => state.stepper.dataForCreate) + const { frequency, fieldActivity, numberStuff } = dataForCreate const { showMessage } = useShowDataStore() @@ -31,15 +36,15 @@ export const StepInformation = () => { }, }) const onSubmit = (data: any) => { - dispatch(setNumberStuff(data.numberStuff)) - dispatch(switchNextStep()) + setDataForCreate({ ...dataForCreate, numberStuff: data.numberStuff }) + switchNextStep() } const checkError: SubmitErrorHandler = (data) => { showMessage(Object.values(data)[0].message || 'Неверные данные') } - const theme = useAppSelector((state) => state.theme.theme) + const theme = useThemeStore((state) => state.theme) return ( @@ -49,7 +54,7 @@ export const StepInformation = () => { dispatch(setFieldActivity(e.target.value as FieldActivity))} + onChange={(e: any) => setDataForCreate({ ...dataForCreate, fieldActivity: e.target.value as FieldActivity })} /> @@ -57,7 +62,7 @@ export const StepInformation = () => { dispatch(setFrequency(e.target.value as Frequency))} + onChange={(e: any) => setDataForCreate({ ...dataForCreate, frequency: e.target.value as Frequency })} /> @@ -7,29 +7,38 @@ import { CompanyData } from '#/features/register-business/api/get-company-date/t import { useCheckTypeCompany } from '#/features/register-business/model/legal-info/use-check-type-company' import styles from '#/features/register-business/ui/step-information/step-information.module.scss' import { IEmailForms } from '#/features/register-by-email/model/types' -import { useAppDispatch, useAppSelector } from '#/app/store/store' +import { useThemeStore } from '#/entities/theme/model/use-theme-store' +import { useStepperStore } from '#/features/register-business/model/use-stepper-store' import { ButtonUI, Error, InputStyleDark, InputStyleLight } from '#/shared' +import { useShowDataStore } from '#/shared/lib/hooks/use-show-data' import { companyNameOptions, innOptions, ogrnOptions, } from '../../lib/constatnts-step-legal-informative' -import { useAutoLoadingInfo } from '../../model/legal-info/use-auto-loading-info' import { useAutoSetInfo } from '../../model/legal-info/use-auto-set-info' -import { setCompanyName, setInn, setOgrn, switchNextStep } from '../../model/stepper-slice' -import { useShowDataStore } from '#/shared/lib/hooks/use-show-data' export const StepLegalInformative = () => { - const theme = useAppSelector((state) => state.theme.theme) - - const dispatch = useAppDispatch() + const theme = useThemeStore((state) => state.theme) + const { + dataForCreate, + company, + setDataForCreate, + setCompany, + switchNextStep + } = useStepperStore((state) => ({ + dataForCreate: state.dataForCreate, + company: state.company, + setDataForCreate: state.setDataForCreate, + setCompany: state.setCompany, + switchNextStep: state.switchNextStep + })) - const { companyName, ogrn, inn } = useAppSelector((state) => state.stepper.dataForCreate) - - const suggestedCompany = useAppSelector((state) => state.stepper.company) + const { companyName, ogrn, inn } = dataForCreate + const suggestedCompany = company const [selected, setSelected] = React.useState( - () => suggestedCompany.filter((el) => el.value === companyName)[0] || null + () => (suggestedCompany as any[])?.filter((el: any) => el.value === companyName)[0] || null ) const methods = useForm({ @@ -45,25 +54,29 @@ export const StepLegalInformative = () => { ) const { showMessage } = useShowDataStore() - const onSubmit = (data: any, e: any) => { + + const handleSubmit = (data: any, e: any) => { e.preventDefault() - dispatch(setCompanyName(data.companyName)) - dispatch(setInn(data.inn)) - dispatch(setOgrn(data.ogrn)) - dispatch(switchNextStep()) + setDataForCreate({ + ...dataForCreate, + companyName: data.companyName, + inn: data.inn, + ogrn: data.ogrn + }) + switchNextStep() } - useAutoLoadingInfo(methods) - - useAutoSetInfo(methods, selected) - const checkError: SubmitErrorHandler = (data, event) => { event!.preventDefault() showMessage(Object.values(data)[0].message || 'Неверные данные') } + + + useAutoSetInfo(methods, selected) + return ( -
+ Наименование вашей организации { clearText={'Очистить'} noOptionsText={'Не найдено'} options={suggestedCompany} - value={selected} + value={selected as any} onChange={(event, value) => setSelected(value)} - getOptionLabel={(label) => (typeof label !== 'string' ? label.value : '')} + getOptionLabel={(label) => (typeof label !== 'string' ? (label as any).value : '')} sx={{ '& .MuiAutocomplete-clearIndicator': { - color: theme === 'light' ? 'black' : 'while', + color: theme === 'light' ? 'black' : 'white', }, }} renderInput={(params) => ( @@ -128,11 +141,11 @@ export const StepLegalInformative = () => { - dispatch(switchNextStep())} text='Пропустить' /> + switchNextStep()} text='Пропустить' /> @@ -5,10 +5,10 @@ import StepperMui from '@mui/material/Stepper' import Image from 'next/image' import { useRouter } from 'next/router' -import { useAppDispatch, useAppSelector } from '#/app/store/store' +import { useStepperStore } from '#/features/register-business/model/use-stepper-store' import { steps } from '../../lib/constants' -import { switchPreviousStep, switchStep } from '../../model/stepper-slice' + import { StepContact } from '../step-contact/step-contact' import { StepInformation } from '../step-information/step-information' import { StepLegalInformative } from '../step-legal-informative/step-legal-informative' @@ -16,16 +16,22 @@ import { StepLegalInformative } from '../step-legal-informative/step-legal-infor 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) - - const loadingCreate = useAppSelector((state) => state.stepper.loadingCreate) - - const dispatch = useAppDispatch() + const { + activeStep, + loadingCreate, + processCreate, + switchStep, + switchPreviousStep + } = useStepperStore((state) => ({ + activeStep: state.activeStep, + loadingCreate: state.loadingCreate, + processCreate: state.loadingCreate, + switchStep: state.switchStep, + switchPreviousStep: state.switchPreviousStep + })) const { showMessage } = useShowDataStore() - const processCreate = useAppSelector((state) => state.stepper.loadingCreate) - const { push } = useRouter() useEffect(() => { @@ -49,7 +55,7 @@ export const Stepper = () => { if (idx < activeStep) { e.preventDefault() - dispatch(switchStep(idx)) + switchStep(idx) return } @@ -71,7 +77,7 @@ export const Stepper = () => { {isShowBackBtn && ( dispatch(switchPreviousStep())} + onClick={() => switchPreviousStep()} className={styles.back} src={'/svg/business/back-register.svg'} width={31} @@ -12,7 +12,7 @@ import { useRouter } from 'next/router' import { PasswordOptions } from '#/features/register-by-email/lib/constants' import { IEmailForms } from '#/features/register-by-email/model/types' -import { useAppSelector } from '#/app/store/store' + import { emailOptions } from '#/shared' import { CheckBoxAgreeWithRules } from '#/shared' import { Error } from '#/shared' @@ -32,8 +32,6 @@ export const RegisterEmailForm: React.FC = ({ successLo const { theme, desktop } = useThemeAndDevice() - const referral = useAppSelector((state) => state.user.referral) - const { showMessage } = useShowDataStore() const { push } = useRouter() @@ -1,7 +1,7 @@ import React from 'react' import { Box, Tooltip, Typography } from '@mui/material' -import { useAppSelector } from '#/app/store/store' +import { useThemeStore } from '#/entities/theme/model/use-theme-store' import { baseColor } from '#/shared/lib/constants/colors' interface ITooltipMy { @@ -13,7 +13,7 @@ interface ITooltiptext { begin: () => void } const TooltipText: React.FC = ({ begin }) => { - const theme = useAppSelector((state) => state.theme.theme) + const theme = useThemeStore((state) => state.theme) return ( @@ -49,7 +49,7 @@ const TooltipText: React.FC = ({ begin }) => { ) } const TooltipMy: React.FC = ({ children, begin }) => { - const theme = useAppSelector((state) => state.theme.theme) + const theme = useThemeStore((state) => state.theme) return ( { @@ -1,50 +0,0 @@ -import { createAsyncThunk, createSlice } from '@reduxjs/toolkit' - -import { CopywriteProxy } from '#/domains/copywrite/proxy/copywrite-proxy' -import { Template } from '#/domains/copywrite/proxy/types/template' -import { Message } from '#/shared/lib/types/model' - -export interface Theme { - templates: Template[] | null - generation: Message[] | null -} - -const initialState: Theme = { - templates: null, - generation: null, -} - -export const loadTemplates = createAsyncThunk( - 'copywrite/loadTemplates', - async (token?: string): Promise => { - return await CopywriteProxy.getTemplates(token) - } -) - -export const loadGeneration = createAsyncThunk( - 'copywrite/loadGeneration', - async (token?: string): Promise => { - return await CopywriteProxy.getGeneration(token) - } -) - -export const copySlice = createSlice({ - name: 'copySlice', - initialState, - reducers: { - loadTemplates: (state, action) => {}, - }, - extraReducers: (builder) => { - builder - .addCase(loadTemplates.fulfilled, (state, action) => { - state.templates = action.payload - }) - .addCase(loadGeneration.fulfilled, (state, action) => { - state.generation = action.payload - }) - }, -}) - -export const {} = copySlice.actions - -export default copySlice.reducer @@ -1,72 +0,0 @@ -import { Dispatch, SetStateAction, useEffect, useMemo, useState } from 'react' -import { useRouter } from 'next/router' -import { useSession } from 'next-auth/react' - -import { emptyTemplate } from '#/domains/copywrite/lib/constants' -import { Template } from '#/domains/copywrite/proxy/types/template' -import { loadGeneration, loadTemplates } from '#/features/use-copy/copy-slice' -import { useAppDispatch, useAppSelector } from '#/app/store/store' -import { Message } from '#/shared/lib/types/model' - -interface UseCopy { - currentTemplate: Template | null - generations: Message[] | null - pickGeneration: Message | null - setPickGeneration: Dispatch> - createEmpty: () => void -} - -export const useCopy = (): UseCopy => { - const [pickGeneration, setPickGeneration] = useState(null) - - const { data } = useSession() - - const { query, push, pathname } = useRouter() - - const templates = useAppSelector((state) => state.copy.templates) - - const generations = useAppSelector((state) => state.copy.generation) - - const dispatch = useAppDispatch() - - const getId = () => { - const id = query['id'] - - if (id) { - return Number(id) - } - } - - const createEmpty = () => { - setPickGeneration({ - content: '', - uid: '123', - created_at: '123', - file: null, - info: null, - from_model: false, - elapsed_time: '12', - is_favourite: false, - is_sent: false, - }) - } - - useEffect(() => { - if (data?.access) { - dispatch(loadTemplates(data.access)) - dispatch(loadGeneration(data.access)) - } - }, [data?.access]) - - const currentTemplate = useMemo(() => { - const id = getId() - const foundTemplate = templates?.find((el) => el.id === id) - if (foundTemplate) { - return foundTemplate - } - - return emptyTemplate - }, [templates]) - - return { currentTemplate, generations, pickGeneration, setPickGeneration, createEmpty } -} @@ -1,166 +0,0 @@ -import { Dispatch, SetStateAction, useEffect, useState } from 'react' -import axios, { AxiosError, AxiosResponse } from 'axios' -import { ContentState, EditorState } from 'draft-js' -import { useSession } from 'next-auth/react' - -import { Template } from '#/domains/copywrite/proxy/types/template' -import { loadGeneration } from '#/features/use-copy/copy-slice' -import { useAppDispatch } from '#/app/store/store' -import { API_URL } from '#/shared/lib/constants' -import { Message, MessageSend } from '#/shared/lib/types/model' -import { useShowDataStore } from '#/shared/lib/hooks/use-show-data' - -type Languages = 'ru' | 'en' | 'it' | 'fr' -type LanguagesText = 'Русский' | 'Английский' | 'Итальянский' | 'Французский' - -export const langs: Record = { - Русский: 'ru', - Английский: 'en', - Итальянский: 'it', - Французский: 'fr', -} - -export const languages = { ...langs, Немецкий: 'de' } -export const target_audiences = ['Вся', '18+', '21+', '30+', '14-20', '35-40'] -export const tovs = ['Нейтральный', 'Спокойный', 'Агрессивный', 'Серьезный', 'Провокационный', 'Остроумный', 'Наставнический', 'Дружелюбный'] - -type Setting = Pick - -type UseTemplate = { - text: EditorState - isLoading: boolean - - lang: string - targetAudiences: string - tov: string - theme: string - content: string - keywords: string[] - resource_urls: string[] - - setLang: Dispatch> - setTargetAudiences: Dispatch> - setTov: Dispatch> - setTheme: Dispatch> - setContent: Dispatch> - setKeywords: Dispatch> - setResourceUrls: Dispatch> - - clearSetting: () => void - createText: () => void - onEditorChange: (a: any) => void -} - -export const useTemplate = (currentTemplate: Template | null): UseTemplate => { - const [text, setText] = useState(EditorState.createEmpty()) - - const onEditorChange = (editorState: any) => { - setText(editorState) - } - - const [isLoading, setIsLoading] = useState(false) - - const dispatch = useAppDispatch() - - const { showMessage } = useShowDataStore() - - const { data: session } = useSession() - - const [lang, setLang] = useState(() => { - const a = Object.entries(languages) - - const b = a.find(([key, value]) => value === currentTemplate?.language) - - if (b) { - return b[0] - } else { - return a[0][0] - } - }) - - const [targetAudiences, setTargetAudiences] = useState(currentTemplate?.target_audience || target_audiences[0]) - - const [tov, setTov] = useState(currentTemplate?.tov || tovs[0]) - - const [theme, setTheme] = useState(currentTemplate?.theme || '') - - const [content, setContent] = useState(currentTemplate?.content || '') - - const [keywords, setKeywords] = useState(currentTemplate?.keywords || []) - - const [resource_urls, setResourceUrls] = useState(currentTemplate?.resources_urls || []) - - const clearSetting = () => { - setLang('Русский') - setTargetAudiences(currentTemplate?.target_audience || target_audiences[0]) - setTov(currentTemplate?.tov || tovs[0]) - setTheme(currentTemplate?.theme || '') - setKeywords(currentTemplate?.keywords || []) - setResourceUrls(currentTemplate?.resources_urls || []) - } - - const createText = async () => { - const dataForSend: MessageSend = { - content: content, - file: null, - info: { - keywords, - language: Object.entries(languages).find(([key, value]) => key === lang)![1], - tov, - resources_urls: resource_urls, - target_audience: targetAudiences, - theme: theme, - }, - } - - if (!session?.access) { - showMessage('У вас неактивный токен, попробуйте перезайти в аккаунт') - return - } - - try { - setIsLoading(true) - const { data } = await axios.post>(API_URL + '/copywrite/', dataForSend, { - withCredentials: true, - headers: { - Authorization: `Bearer ${session?.access}`, - }, - }) - setIsLoading(false) - - const newContentState = ContentState.createFromText(data[0].content) - - setText(EditorState.createWithContent(newContentState)) - dispatch(loadGeneration(session?.access)) - } catch (err: any) { - setIsLoading(false) - return { - error: true, - message: 'Произошла ошибка при выполнении запроса', - details: err as AxiosError, - } - } - } - - return { - tov, - setTov, - lang, - setLang, - clearSetting, - createText, - isLoading, - keywords, - setKeywords, - setResourceUrls, - resource_urls, - setTargetAudiences, - targetAudiences, - setTheme, - text, - onEditorChange, - theme, - setContent, - content, - } -} @@ -0,0 +1,6 @@ +import { BusinessHost } from "#/views/account"; +import { getAdminLayout } from "#/widgets/layouts"; + +BusinessHost.getLayout = getAdminLayout({title: 'Корпоративный аккаунт'}) + +export default BusinessHost \ No newline at end of file @@ -0,0 +1,6 @@ +import { Settings } from '#/views/account' +import { getAdminLayout } from '#/widgets/layouts' + +Settings.getLayout = getAdminLayout({ title: 'Настройки' }) + +export default Settings @@ -0,0 +1,6 @@ +import { Payment } from '#/views/account' +import { getAdminLayout } from '#/widgets/layouts' + +Payment.getLayout = getAdminLayout({ title: 'Управление оплатой' }) + +export default Payment @@ -1,136 +1,72 @@ -import NextAuth, { NextAuthOptions, User } from 'next-auth' +import { getUserServer } from '#/entities/user-account/api' +import { authUser, authUserByYandex, refresh } from '#/features/auth' +import NextAuth, { User } from 'next-auth' +import { JWT } from 'next-auth/jwt' import CredentialsProvider from 'next-auth/providers/credentials' import YandexProvider from 'next-auth/providers/yandex' -import { AuthConstants, ERROR_MAPPING, ERROR_YANDEX_MAPPING } from './constants' -import { AuthorizationProxy } from './proxy' - -import { getAll } from '#/entities/user-account/model/user-type-slice' - -const AuthProxy = new AuthorizationProxy() - -export const authOptions: NextAuthOptions = { - session: { strategy: 'jwt', maxAge: AuthConstants.sessionTime }, +const handler = NextAuth({ secret: process.env.NEXTAUTH_SECRET, providers: [ YandexProvider({ - clientId: AuthConstants.client_id_yandex || '', - clientSecret: AuthConstants.client_secret_yandex || '', - authorization: AuthConstants.redirect_uri_yandex, - checks: ['none'], + clientId: process.env.NEXT_PUBLIC_CLIENT_ID_YANDEX!, + clientSecret: process.env.NEXT_PUBLIC_CLIENT_SECRET_YANDEX!, + allowDangerousEmailAccountLinking: true, + checks: 'state', + httpOptions: { + timeout: 10000, + }, }), CredentialsProvider({ id: 'credentials', type: 'credentials', - credentials: {}, - async authorize(credentials, req) { - const { username, password } = credentials as { username: string, password: string } - - const resp = await AuthProxy.loginByEmail(username, password) - const data = await resp.json() - - if (resp.status >= 400 && resp.status < 500) { - throw new Error( - ERROR_MAPPING[(data as { detail: string }).detail] ?? - JSON.stringify(data as Object) - .replace(/\s+/, '_') - .replace(/[А-Яа-я]/, '') - .toLowerCase() - ) - } - return (await data) as User + credentials: { + username: { label: 'Email', type: 'text', placeholder: 'jsmith@gmail.com' }, + password: { label: 'Password', type: 'password' }, }, - }), - CredentialsProvider({ - id: 'email_token', - type: 'credentials', - credentials: {}, - async authorize(credentials, req) { - const { token } = credentials as { token: string } - - const resp = await AuthProxy.loginByEmailToken(token) + async authorize(credentials) { + const { data, status } = await authUser({ ...credentials! }) - const data = await resp.json() + if (status >= 400 && status <= 500) throw Error('something error') - if (resp.status >= 400 && resp.status < 500) { - throw new Error( - ERROR_MAPPING[(data as { detail: string }).detail] ?? - JSON.stringify(data as Object) - .replace(/\s+/, '_') - .replace(/[А-Яа-я]/, '') - .toLowerCase() - ) - } - return (await data) as User + return { ...data.token, user: { email: data.email } } }, }), ], callbacks: { - async jwt({ token, user, account, trigger }) { - if (account?.provider === 'yandex') { - const result = await AuthProxy.exchangeTokenYandex(account.access_token || '') + async jwt({ token, user, account, trigger, session }) { + if (account && account.provider === 'yandex' && account.access_token) { + const { data, status } = await authUserByYandex(account.access_token) - if (result.status >= 400 && result.status < 500) { - throw new Error('Непредвиденная ошибка') - } + if (status >= 400 && status <= 500) throw Error('something error') - const info = await getAll( - (result.data as { access_token: string }).access_token - ) - - return { ...info.token, tokenExpiry: AuthConstants.getExpiresDate() } - } + const { data: r } = await getUserServer(data.access_token) - if (user) { - try { - token.access = user.token.access - token.refresh = user.token.refresh - token.tokenExpiry = AuthConstants.getExpiresDate() - return token - } catch (error) { - return { ...token, ...user } - } + return { ...r.token, user: { email: r.email } } as any } - const shouldRefreshTime = Math.round(Date.now() - token.tokenExpiry) >= 0 + if (trigger === 'update' && session === 'refresh' && token.refresh) { + const { data, status } = await refresh(token.refresh) - if (shouldRefreshTime) { - return await AuthProxy.refreshToken(token) - } - - return token - }, - signIn: async ({ user, account, ...props }) => { - if (account?.provider === 'yandex') { - const { data: result, status } = await AuthProxy.exchangeTokenYandex( - account.access_token! - ) + if (status === 401) throw Error('something error') - if (status >= 400 && status < 500) { - return Promise.reject( - new Error( - ERROR_YANDEX_MAPPING[ - (result as { error_description: string }).error_description - ] - ) - ) - } + const { data: r } = await getUserServer(data.access_token) - return Promise.resolve(true) + return { ...data, refresh: token.refresh, user: { email: r.email } } } - return Promise.resolve(true) + if (!user) return { ...token } + + return { ...token, ...user } }, async session({ session, token }) { - session.access = token.access - session.refresh = token.refresh - return session + return { ...session, ...token } }, }, pages: { signIn: '/login', error: '/login', }, -} +}) -export default NextAuth(authOptions) +export default handler @@ -40,8 +40,8 @@ export class AuthorizationProxy { API_URL + '/auth/login-social/convert-token', { grant_type: 'convert_token', - client_id: AuthConstants.django_app_client_id_yandex, - client_secret: AuthConstants.django_app_client_secret_yandex, + client_id: process.env.NEXT_PUBLIC_DJANGO_YANDEX_APP_CLIENT_ID, + client_secret: process.env.NEXT_PUBLIC_DJANGO_YANDEX_APP_CLIENT_SECRET, backend: 'yandex-oauth2', token: yandex_token, }, @@ -1,6 +0,0 @@ -import { AudioModelsPage } from '#/views/audio' -import { getDefaultLayout } from '#/widgets/layouts' - -AudioModelsPage.getLayout = getDefaultLayout({ titlePage: 'Аудио' }) - -export default AudioModelsPage @@ -1,5 +1,4 @@ import React, { ReactElement, ReactNode, useCallback, useEffect } from 'react' -import { Provider } from 'react-redux' import { StyledEngineProvider, ThemeProvider } from '@mui/material' import * as Sentry from '@sentry/browser' import axios from 'axios' @@ -15,40 +14,12 @@ import ErrorBoundary from './error-boundary' import '#/app/styles/globals.css' import '#/app/styles/styles-pages/system.scss' -import { store, useAppSelector } from '#/app/store/store' import { Error } from '#/shared' import { pingFangFont } from '#/shared/lib/constants/font/font' import { useBlockTelegram } from '#/shared/lib/hooks/use-block-telegram' import { Providers } from '#/widgets/providers' -axios.defaults.httpsAgent = new https.Agent({ - rejectUnauthorized: false, -}) - -axios.defaults.validateStatus = () => true - -axios.interceptors.response.use(async (response) => { - if (![401, 404, 403].includes(response.status)) return response - - const session = await getSession() - - if (!session) { - window.location.href = '/login' - } - - return response -}) -axios.interceptors.response.use( - (response) => response, - (error) => { - if (!error.response) { - window.location.href = '/network-error' - } - return error - } -) - -const inter = Inter({ subsets: ['latin'] }) +const inter = Raleway({ subsets: ['latin'] }) export type NextPageWithLayout

= NextPage & { getLayout?: (page: ReactElement) => ReactNode @@ -74,16 +45,14 @@ function App({ Component, pageProps: { session, ...pageProps } }: AppPropsWithLa

- - - - - {getLayout()} - - - - - + + + + {getLayout()} + + + +
@@ -1,6 +0,0 @@ -import { AccountPage } from '#/views/account' -import { getDefaultLayout } from '#/widgets/layouts' - -AccountPage.getLayout = getDefaultLayout({ titlePage: 'Аккаунт' }) - -export default AccountPage @@ -1,51 +0,0 @@ -import axios from 'axios' - -import { API_URL } from '#/shared/lib/constants' -import { ShortModel, Model } from '#/entities/model-entity' - -const model_api = { - async getBots(token?: string): Promise { - try { - const { data } = await axios.get( - API_URL + '/ml_models/?category=chat-bots', - { - headers: { - Authorization: `Bearer ${token}`, - }, - } - ) - return data - } catch (err: any) { - return err - } - }, - - async getBotParams(slug: string, token?: string): Promise { - try { - const { data } = await axios.get(API_URL + `/ml_models/${slug}`, { - headers: { - Authorization: `Bearer ${token}`, - }, - }) - return data - } catch (err: any) { - return err - } - }, - - async getImages(token?: string): Promise { - try { - const { data } = await axios.get(API_URL + '/ml_models/?category=images', { - headers: { - Authorization: `Bearer ${token}`, - }, - }) - - return data - } catch (e) { - return [] - } - }, -} - -export default model_api @@ -6,7 +6,7 @@ import { useSession } from 'next-auth/react' import process from 'process' import { getUserBalance } from '#/entities/balance' -import { useAppDispatch } from '#/app/store/store' + import { API_URL } from '#/shared/lib/constants' import { Device } from '#/shared/lib/types/entities' import { IMessageRequest } from '#/shared/lib/types/types-gpt' @@ -72,7 +72,6 @@ export function useModel( const { data } = useSession() const [messages, setMessages] = useState(null) const [loading, setLoading] = useState(false) - const dispatch = useAppDispatch() const [offset, setOffset] = useState(0) useEffect(() => { @@ -193,7 +192,7 @@ export function useModel( setMessages((prev) => [...prev!, ...(result as Message[])]) } - dispatch(getUserBalance(data?.access)) + // TODO: getUserBalance для зутсанд стора } const deleteMessage = (message_uid: string) => { @@ -281,8 +280,6 @@ export function useModelImages( const [offset, setOffset] = useState(0) - const dispatch = useAppDispatch() - const typeRef = useRef(type) const dataRef = useRef(data) const messagesRef = useRef(messages) @@ -390,7 +387,7 @@ export function useModelImages( showMessage(message) return } - dispatch(getUserBalance(data?.access)) + // TODO: Add getUserBalance to Zustand store //@ts-ignore setMessages((prev) => { @@ -462,8 +459,6 @@ export function useMedia(showMessage: (message: string) => void, modelType: stri const [loading, setLoading] = useState(false) - const dispatch = useAppDispatch() - const [input, setInput] = React.useState('') useEffect(() => { @@ -536,7 +531,7 @@ export function useMedia(showMessage: (message: string) => void, modelType: stri setMessages((prev) => [...prev!, ...(result as Message[])]) setInput('') - dispatch(getUserBalance(data?.access)) + // TODO: Add getUserBalance to Zustand store } return { messages, sendMessage, loading, input, setInput } @@ -2,7 +2,7 @@ import axios, { AxiosResponse } from 'axios' import { User } from 'next-auth' import { API_URL } from '#/shared/lib/constants' -import { IOffer } from '#/widgets/payment/model/payment' +import { IOffer } from '#/widgets/payment/ui/payment' interface IUserBalance { current_token_balance: number @@ -22,6 +22,14 @@ interface DataForLogin { password: string } +interface PaymentHistory { + uid: string + amount: number + status: string + created_at: string + payment_method: string +} + export const accountApi = { async getPaymentsPlans(token: string): Promise { try { @@ -146,15 +154,15 @@ export const accountApi = { } }, - async removeSub(token?: string) { + async getPaymentsHistory(token: string): Promise { try { - const { status } = await axios.delete(API_URL + '/payments/plans', { + const { data } = await axios.get(API_URL + '/payments/history', { headers: { Authorization: `Bearer ${token}`, }, }) - return status + return data } catch (err) { return null } @@ -1,267 +0,0 @@ -import axios, { AxiosResponse } from 'axios' -import { Session } from 'next-auth' -import * as process from 'process' - -import { Chat } from '#/shared/api/type-model-chats' -import { Error } from '#/shared/lib/types/entities' -import { IImagesResponse } from '#/shared/lib/types/types-dalle' -import { IMessageRequest, ISendMessageResponse } from '#/shared/lib/types/types-gpt' - -import { Message } from '../lib/types/model' - -const API_URL = process.env.NEXT_PUBLIC_API_HOST - -type Result = { - question: string - link: string - created_at: string - resulting_balance?: number -} - -export type Wrap = { - count: number - next: string | null - page_size: number - previous: string | null -} - -export type WrapResponse = Wrap & { - results: T[] -} - -type INeuronModel = any - -export const api = { - async getImagesSD(token: string | null, url: string): Promise | null> { - try { - const { data } = await axios.get(url, { - headers: { - Authorization: `Bearer ${token}`, - }, - }) - return data - } catch (err) { - return null - } - }, - - async getImagesDalle(token: string | null, url: string): Promise | null> { - try { - const { data } = await axios.get(url, { - headers: { - Authorization: `Bearer ${token}`, - }, - }) - - return data - } catch (err) { - return null - } - }, - - async getMessagesChatGPT(token: string | null, uid: string): Promise { - try { - const { data } = await axios.get(API_URL + `/chats/${uid}/messages/`, { - headers: { - Authorization: `Bearer ${token}`, - }, - }) - return data - } catch (err: any) { - return { - error: true, - message: 'Произошла ошибка при выполнении запроса', - details: err.message, - } - } - }, - - async updRenameChat(uid: string, title: string, token: string | undefined) { - try { - const { data } = await axios.put<{ uid: string; title: string; created_at: string }>( - API_URL + `/chats/${uid}/`, - { title: title }, - { - withCredentials: true, - headers: { - Authorization: `Bearer ${token}`, - }, - } - ) - return { data, isError: false } - } catch (err) { - return { data: null, isError: true, error: err } - } - }, - - async sendMessageChatGPT(token: string, message: any, uid: string): Promise { - try { - const { data } = await axios.post>( - API_URL + `/chats/${uid}/messages/`, - message, - { - withCredentials: true, - headers: { - Authorization: `Bearer ${token}`, - }, - } - ) - - return { data, isError: false } - } catch (err) { - return { data: null, isError: true, error: err } - } - }, - - async getFavoritesModel(token: string | null, session: Session | null): Promise { - try { - const { data } = await axios.get(API_URL + `/ml_models/${session?.user?.name}`, { - headers: { - Authorization: `Bearer ${token}`, - }, - }) - - return data?.filter((model) => model.is_favourite) - } catch (err) { - return [] - } - }, - - async getAllModels(token: string | null): Promise { - try { - const { data } = await axios.get(API_URL + '/ml_models/', { - headers: { - Authorization: token ? `Bearer ${token}` : '', - }, - }) - - return data - } catch (err) { - return [] - } - }, -} - -export const createChat = async (model: string, token?: string) => { - try { - const { data } = await axios.post( - API_URL + `/chats/?model=${model}`, - { - title: 'Новый чат', - model, - }, - { headers: { Authorization: `Bearer ${token}` } } - ) - - return data - } catch (e) { - return null - } -} - -export const getAllChats = async (model: string, token?: string): Promise => { - try { - const { data } = await axios.get(API_URL + `/chats/?model=${model}`, { - headers: { Authorization: `Bearer ${token}` }, - }) - - if (data.length === 0) { - const newChat = await createChat(model, token) - - return [newChat].reverse() - } - - return data.reverse() - } catch (e) { - return [] - } -} - -export const addToFavorites = async (model_id: string, token?: string) => { - try { - const { data, status } = await axios.put( - API_URL + '/ml_models/favourites ', - { - uid: model_id, - }, - { - headers: { - Authorization: `Bearer ${token}`, - }, - } - ) - return status - } catch (err) { - return 400 - } -} - -export const deleteFromFavorites = async (model_id: string, token?: string) => { - try { - const { status, data } = await axios.delete(API_URL + '/ml_models/favorites', { - headers: { - Authorization: `Bearer ${token}`, - }, - data: { - uid: model_id, - }, - }) - return status - } catch (err) { - return 400 - } -} - -export interface PaymentHistory { - uid: string - plan: { - uid: string - title: string - price: string - tokens_per_plan: string - duration: string - } - created_at: string - updated_at: string - amount: string - status: string -} - -export const getPaymentsHistory = async (token?: string) => { - try { - const { data } = await axios.get(API_URL + '/payments/history', { - headers: { - Authorization: `Bearer ${token}`, - }, - }) - return data - } catch (err) { - return null - } -} - -export interface IReferral { - referrals: [ - { - username: string - joined_at: string - profile_picture_link: string - } - ] - registrations_count: number - payments_count: number - accrued_bonuses_amount: number -} - -export const getReferral = async (token?: string) => { - try { - const { data } = await axios.get(API_URL + '/payments/referral-account', { - headers: { - Authorization: `Bearer ${token}`, - }, - }) - return data - } catch (err) { - return null - } -} @@ -4,14 +4,4 @@ export const api = axios.create({ validateStatus: () => true, baseURL: process.env.NEXT_PUBLIC_API_HOST, withCredentials: true, -}) - -api.interceptors.response.use( - (response) => response, - (error) => { - if (!error.response) { - window.location.href = '/network-error' - } - return error - } -) +}) \ No newline at end of file @@ -1,20 +1,52 @@ -import { useSession } from 'next-auth/react' +import { getSession, signOut, useSession } from 'next-auth/react' +import { useRouter } from 'next/navigation' import { api } from './instance' +import axios, { InternalAxiosRequestConfig } from 'axios' +import { useCallback } from 'react' export function makePrivateRequest( callback: (...params: Params) => Promise ) { - const { data } = useSession() + const { data, update } = useSession() + + const { push } = useRouter() + + const onRequest = async (config: any) => { + if (config.headers.Authorization) return config + + const session = await getSession() + + config.headers.Authorization = `Bearer ${session?.access}` + + return config + } return async (...params: Params) => { - api.interceptors.request.use((config) => { - config.headers.Authorization = `Bearer ${data?.access}` - return config - }) + api.interceptors.request.use(onRequest) + + api.interceptors.response.use( + async ({ status, ...r }) => { + if (![401].includes(status)) return { status, ...r } + + const session = await update('refresh') + + console.log(session) + + if (!session) window.location.href = '/login' + + r.config.headers.Authorization = `Bearer ${session?.access}` + + return { status, ...r } + }, + (error) => { + if (!error.respose) return (window.location.href = '/network-error') + } + ) const result = await callback(...params) api.interceptors.request.clear() + api.interceptors.response.clear() return result } @@ -1,5 +0,0 @@ -export type Chat = { - uid: string - title: string - created_at: string -} @@ -1,3 +1,4 @@ +import { FieldValues, Path } from 'react-hook-form' import { RegisterOptions } from 'react-hook-form/dist/types/validator' const EMAIL_REGEXP = @@ -17,6 +18,10 @@ export const emailOptions: RegisterOptions = { }, } +export function getEmailOptions>() { + return emailOptions as RegisterOptions +} + export const phoneOptions: RegisterOptions = { required: { value: true, @@ -1,9 +1,9 @@ -import { useAppSelector } from '#/app/store/store' +import { useThemeStore } from '#/entities/theme/model/use-theme-store' import { getDeviceType } from '../helpers' import { DeviceOs } from '../types/entities' export const useThemeAndDevice = (device: 'desktop' | 'mobile' = getDeviceType(), deviceOs?: DeviceOs) => { - const theme = useAppSelector((state) => state.theme.theme) + const theme = useThemeStore((state) => state.theme) const desktop = device === 'desktop' const ios = deviceOs === 'ios' @@ -29,6 +29,5 @@ declare module 'next-auth/jwt' { interface JWT { access: string refresh: string - tokenExpiry: number } } @@ -1,3 +0,0 @@ -import { RootState } from '#/app/store/store' - -export const balanceSelector = (state: RootState) => Math.floor(state.balance.balance) @@ -1,5 +1,5 @@ 'use client' -import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import React, { HtmlHTMLAttributes, useCallback, useEffect, useMemo, useRef, useState } from 'react' import ArrowDownSvg from '#/assets/svg/arrow-down.svg?react' import styles from './common-select.module.scss' @@ -8,7 +8,7 @@ import { CommonTooltip } from '#/shared/ui/tooltip' import { SelectItem } from '../types/select-item' import { CSSTransition } from 'react-transition-group' -interface CommonSelectProps { +interface CommonSelectProps extends HtmlHTMLAttributes { value: string | null | undefined setValue: (value: string) => void items: SelectItem[] @@ -3,10 +3,10 @@ import { DateTimePicker, LocalizationProvider } from '@mui/x-date-pickers' import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs' import dayjs, { Dayjs } from 'dayjs' -import { useAppSelector } from '#/app/store/store' - import styles from './date.module.scss' +import { useThemeStore } from '#/entities/theme/model/use-theme-store' + export const DateInput = ({ setDate, label, @@ -18,7 +18,7 @@ export const DateInput = ({ date?: boolean prevdate?: boolean }) => { - const theme = useAppSelector((state) => state.theme.theme) + const theme = useThemeStore((state) => state.theme) useEffect(() => { if (date) { @@ -7,7 +7,7 @@ import { TextFieldProps } from '@mui/material/TextField/TextField' import Image from 'next/image' import { Calculation } from '#/features/calculation-tokens-gpt' -import { useAppSelector } from '#/app/store/store' +import { useThemeStore } from '#/entities/theme/model/use-theme-store' import { styleInputWithoutBorderFocus } from '#/shared/ui/input' interface Input { @@ -36,7 +36,7 @@ export const InputImagesModels: FC = ({ imageLoad, unpinImage, }) => { - const theme = useAppSelector((state) => state.theme.theme) + const theme = useThemeStore((state) => state.theme) const isCalculatePrice = useMemo(() => quality && count && desktop, []) @@ -3,16 +3,16 @@ import { Box } from '@mui/material' import Dialog from '@mui/material/Dialog' import Image from 'next/image' -import { useAppSelector } from '#/app/store/store' - import styles from './modal.module.scss' + +import { useThemeStore } from '#/entities/theme/model/use-theme-store' export interface ModalProps { open: boolean onClose: (e?: any) => void children?: ReactNode } export const Modal: FC = ({ open, onClose, children }) => { - const theme = useAppSelector((state) => state.theme.theme) + const theme = useThemeStore((state) => state.theme) return ( = ({ open, onClose, children }) => { open={open} onClose={onClose} > - {''} + {''} {children} ) @@ -2,7 +2,7 @@ import React from 'react' import { Typography } from '@mui/material' import Box from '@mui/material/Box' -import { useAppSelector } from '#/app/store/store' +import { useThemeStore } from '#/entities/theme/model/use-theme-store' import { PrettoSlider, PrettoSliderDark } from '#/widgets/filters-gpt/ui/filters' export const Slider = ({ @@ -22,7 +22,7 @@ export const Slider = ({ step: number onChangeCommitted?: () => void }) => { - const theme = useAppSelector((state) => state.theme.theme) + const theme = useThemeStore((state) => state.theme) return ( @@ -2,7 +2,7 @@ import React from 'react' import { styled } from '@mui/material' import Switch, { SwitchProps } from '@mui/material/Switch' -import { useAppSelector } from '#/app/store/store' +import { useThemeStore } from '#/entities/theme/model/use-theme-store' import { baseColor } from '#/shared/lib/constants/colors' import { pingFangFont } from '#/shared/lib/constants/font/font' @@ -47,7 +47,7 @@ const AntSwitch = styled(Switch)(({ theme }) => ({ }, })) export const SwitchCustom = (props: any) => { - const theme = useAppSelector((state) => state.theme.theme) + const theme = useThemeStore((state) => state.theme) return ( = ({ children, title, placement, className, maxWidth }) => { - const theme = useAppSelector((state) => state.theme.theme) + const theme = useThemeStore((state) => state.theme) return ( = ({ anchorEl, changeVisible, d const open = Boolean(anchorEl) - const theme = useAppSelector((state) => state.theme.theme) + const theme = useThemeStore((state) => state.theme) const desktop = device === 'desktop' - const email = useAppSelector((state) => state.user.email) + const { getUser } = useUserStore() + + const { email } = getUser() return ( = ({ anchorEl, changeVisible, d onClose={() => changeVisible()} PaperProps={{ style: { - transform: desktop ? 'translateX(-9%) translateY(28%)' : 'translateX(0%) translateY(16%)', + transform: desktop + ? 'translateX(-9%) translateY(28%)' + : 'translateX(0%) translateY(16%)', borderRadius: desktop ? 10 : '0px 0px 15px 15px', marginTop: desktop ? 0 : 12, }, @@ -6,7 +6,7 @@ import FormGroup from '@mui/material/FormGroup' import Menu from '@mui/material/Menu' import MenuItem from '@mui/material/MenuItem' -import { useAppSelector } from '#/app/store/store' +import { useThemeStore } from '#/entities/theme/model/use-theme-store' interface IDalleFilterMenu { anchorEl: null | HTMLElement @@ -27,7 +27,7 @@ export const DalleFilterMenu: React.FC = ({ sliderChange, open, }) => { - const theme = useAppSelector((state) => state.theme.theme) + const theme = useThemeStore((state) => state.theme) return ( { - const { payment_plan, status } = useAppSelector((state) => state.user) + const { getUser } = useUserStore() + + const { payment_plan } = getUser() if (Math.floor(+payment_plan?.plan.price) !== 0) { return null @@ -28,7 +30,7 @@ const DemoDanger = () => { Вам доступны 5 токенов, которые можно потратить на любые продукты в AIR. Чтобы продолжить пользоваться платформой вы можете купить токены. - + + + {!is_social && ( + + Изменить пароль + + + Текущий пароль + setCurrentPassword(e.target.value)} + fullWidth + /> + + + + Новый пароль + setNewPassword1(e.target.value)} + fullWidth + /> + + + + Подтвердить пароль + + setNewPassword2(e.target.value)} + fullWidth + /> + + + + + + )} + + Удаление аккаунта + + Удаление аккаунта приведет к потере всех настроек + + setConfirmDeleteModal(true)} + sx={{ marginTop: '20px' }} + text={'Удалить аккаунт'} + style={{ + width: '100%', + backgroundColor: 'rgba(255, 35, 114, 0.10)', + color: '#FF2372', + }} + /> + + + setConfirmDeleteModal(false)}> + + Удаление аккаунта + + Вы действительно хотите удалить ваш аккаунт? + + + + + + ) +} + +export default AccountSettings @@ -1,270 +1,97 @@ -import * as React from 'react' -import { useEffect, useMemo, useRef, useState } from 'react' -import { Avatar, Box, Button, Stack, Tab, Tabs, Typography } from '@mui/material' +import React, { useCallback, useEffect, useMemo,useState } from 'react' +import { Box, Stack, Tab, Tabs, Typography } from '@mui/material' import CircularProgress from '@mui/material/CircularProgress' -import axios from 'axios' -import Image from 'next/image' +import Link from 'next/link' import { useRouter } from 'next/router' -import { signOut, useSession } from 'next-auth/react' -import { getUserBalance } from '#/entities/balance' -import { getAllInfo, unfollowEmail } from '#/entities/user-account/model/user-type-slice' -import { useAppDispatch, useAppSelector } from '#/app/store/store' +import { scopes, ScopeType } from '../config' +import { Payment } from '../payment' +import { Settings } from '../settings' + +import AccountContentFactory from './account-content-factory' + import styles2 from '#/app/styles/accountTabs.module.css' import styles from '#/app/styles/business.module.scss' -import { ButtonUI, Error, Input, Loader, Modal, SwitchCustom } from '#/shared' -import { accountApi } from '#/shared/api/account-endpoints' -import { API_URL } from '#/shared/lib/constants/constants' + +import { useUserStore } from '#/entities/user-account' +import { NextPageWithLayout } from '#/pages/_app' import { getDeviceType } from '#/shared/lib/helpers' -import { ScreenForInactive } from '#/widgets/business' -import BusinessHost from '#/widgets/business-host/business-host' -import { Info } from '#/widgets/business-info' -import { Subscription } from '#/widgets/payment/model/payment' 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 { - email, - is_subscribed_to_emails, - account_type, - first_name, - last_name, - username, - profile_picture_link, - is_social, - status: userInfoLoaded, - referral_code, - } = useAppSelector((state) => state.user) - const device = getDeviceType() + const desktop = device === 'desktop' - const { status, account_type: type } = useAppSelector((state) => state.user) - - const [name, setName] = useState('') - - const [lastName, setLastName] = useState('') - - const [userName, setUserName] = useState('') - - const fileInputRef = useRef(null) - - const [loading, setLoading] = useState(false) - - const { data } = useSession() - - const handleDivClick = () => { - ;(fileInputRef.current! as any).click() - } - - const dispatch = useAppDispatch() - - const { showMessage } = useShowDataStore() - - const handleFileChange = async (event: any) => { - const formData = new FormData() - formData.append('new_picture', event.target.files[0]) + const { getUser, loaded } = useUserStore() + const { account_type: type } = getUser() + const { pathname } = useRouter() - try { - setLoading(true) - await axios.put(API_URL + '/auth/reset-profile-pic', formData, { - headers: { - Authorization: `Bearer ${data?.access}`, - 'Content-Type': 'multipart/form-data', - }, - }) - dispatch(getAllInfo(data?.access)) - showMessage('Изображение успешно загружено!') - setLoading(false) - } catch (e) { - setLoading(false) - showMessage('Ошибка загрузки изображения на сервере!', 'error') + const getCurrentScope = useCallback((): ScopeType => { + const scopeMap: Record = { + '/account/settings': 'setting', + '/account/subscribe': 'subscribe', + '/account/business': 'business', + '/account/referral': 'referral', } - } - - const [currentPassword, setCurrentPassword] = React.useState('') - - const [newPassword1, setNewPassword1] = React.useState('') - - const [newPassword2, setNewPassword2] = React.useState('') - - const [promocode, setPromocode] = React.useState('') - - const [success, setSuccess] = React.useState('') - - const [confirmDeleteModal, setConfirmDeleteModal] = useState(false) - - const { push, query } = useRouter() - - const [scope, setScope] = useState(query['scope'] || 'setting') + return scopeMap[pathname] || 'setting' + }, [pathname]) + const [scope, setScope] = useState(getCurrentScope()) const [downloadModal, setDownloadModal] = useState(false) - const desktop = device === 'desktop' - - const changeScope = async (scope: string) => { - setScope(scope) - await addQueryParams(scope) - } - - async function addQueryParams(scope: string) { - await push({ - pathname: '/account', - query: { scope }, - }) - } - - useEffect(() => { - if (query['scope'] !== undefined && scope !== query['scope']) { - changeScope(query['scope'] as string) - } - }, [query]) - useEffect(() => { - if (Object.keys(query).length === 0) addQueryParams('setting') - }, []) + setScope(getCurrentScope()) + }, [getCurrentScope]) - const changePassword = async () => { - if (!data) return - if (newPassword1 !== newPassword2) { - showMessage('Укажите одинаковые новы пароли!') - return - } - const status = await accountApi.changePassword(data.access, newPassword1, newPassword2, currentPassword) - - if (status === 200) { - showMessage('Пароль успешно изменён!') - setNewPassword1('') - setNewPassword2('') - setCurrentPassword('') - setTimeout(() => setSuccess(''), 6000) - return - } + const filteredScopes = useMemo(() => { + return scopes.filter((scope) => { + const deviceFilter = desktop || scope.showOnMobile - showMessage('К сожалению, произошла ошибка') - } + if (scope.scope === 'referral' && type === 'business') { + return false + } - function body() { - if (type === 'regular') { - return ( - - - - ) - } - if (type === 'business_account') { - return Информация о корп.аккаунте доступна только владельцу и администраторам. - } - if (type === 'business_host') { - return - } - if (type === 'business_admin') { - return - } - if (type === 'business_security') { - return ( - <> - - - - - Раздел 1. Запросы сотрудников по моделям - - - { - setDownloadModal(true) - }} - sx={{ marginTop: '20px' }} - text={'Скачать'} + return deviceFilter + }) + }, [desktop, type]) + + const renderCurrentContent = useMemo(() => { + switch (scope) { + case 'setting': + return + case 'subscribe': + return + case 'business': + return ( + + {!loaded || type === null ? ( + + + + ) : ( + - + )} - - ) - } - } - - const isUserDataChange = useMemo(() => name !== first_name || last_name !== lastName || username !== userName, [name, lastName, userName]) - - const promocodeActivate = async () => { - if (!data) return - if (!promocode.trim()) { - showMessage('Введите корректный промокод') - return - } - let resStatus = 404 - try { - const { status } = await axios.post( - API_URL + '/payments/promocode', - { code: promocode }, - { headers: { Authorization: `Bearer ${data.access}` } } - ) - resStatus = status - } catch (e) {} - - if (resStatus === 200) { - showMessage('Промокод успешно активирован! Токены уже зачислены!') - dispatch(getUserBalance(data?.access)) - return - } - - if (resStatus === 403) { - showMessage('Промокод уже был активирован!') - return - } - - if (resStatus === 404) { - showMessage('Промокод не найден!') - return - } - } - - async function changeUserData() { - if (!data) return - if (!isUserDataChange) { - showMessage('Вы не изменили данные', 'success') - return + ) + case 'referral': + if (type === 'business') { + return ( + + + Реферальная программа недоступна для корпоративных аккаунтов + + + ) + } + return + default: + return null } - - try { - await axios.put( - API_URL + '/auth/user-data', - { - username: userName, - email: email, - first_name: name, - last_name: lastName, - }, - { headers: { Authorization: `Bearer ${data.access}` } } - ) - - showMessage('Данные успешно изменены!') - dispatch(getAllInfo(data.access)) - } catch (e) {} - } - - const deleteAccount = async () => { - if (!data) return - try { - const { status } = await axios.delete(API_URL + '/auth/remove', { - headers: { - Authorization: `Bearer ${data.access}`, - }, - }) - - if (status === 200) await signOut() - } catch (err) {} - } - - useEffect(() => { - setName(first_name) - setLastName(last_name) - setUserName(username) - }, [first_name, last_name, username]) + }, [scope, loaded, type, downloadModal, desktop]) return ( { scrollButtons={false} sx={{ '& .MuiTabs-indicator': { display: 'none' } }} > - {scopes.map((el) => { - if (!desktop && el.scope === 'business') { - return null - } - if (el.scope === 'referral' && account_type !== 'regular') { - return null - } - return ( + {filteredScopes.map((scopeConfig) => ( + changeScope(el.scope)} - key={el.title} - className={scope === el.scope ? styles2.wrap_toggle_button_active : styles2.wrap_toggle_button} - value={el.scope} - label={el.title} + className={ + scope === scopeConfig.scope + ? styles2.wrap_toggle_button_active + : styles2.wrap_toggle_button + } + value={scopeConfig.scope} + label={scopeConfig.title} /> - ) - })} + + ))} - {scope === 'setting' ? ( - <> - - Основные - - - - - - {!loading || !(userInfoLoaded === 'succeeded') ? ( - {'Image - ) : ( - - )} - - - {''} - - - - - - Имя - setName(e.target.value)} - fullWidth - /> - - - Фамилия - setLastName(e.target.value)} - fullWidth - /> - - - - Email - - { - if (!data) return - await dispatch(unfollowEmail(data.access)) - showMessage('Данные изменены!') - }} - /> - - Отписаться от рассылки - - - - - - - Никнейм - setUserName(e.target.value)} - fullWidth - /> - - - {/*referral_code.code.trim() && ( - - Ваша реферальная ссылка: -   - - navigator.clipboard.writeText(referral_code.code.trim())} - className='text' - sx={{ color: '#8280FF !important', cursor: 'pointer' }} - > - {referral_code.code} - - - -) */} - - - - - Активация промокода - setPromocode(e.target.value)} - fullWidth - /> - - - {!is_social && ( - - Изменить пароль - - - Текущий пароль - setCurrentPassword(e.target.value)} - fullWidth - /> - - - - Новый пароль - setNewPassword1(e.target.value)} - fullWidth - /> - - - Подтвердить пароль - setNewPassword2(e.target.value)} - fullWidth - /> - - - - - - )} - - Удаление аккаунта - Удаление аккаунта приведет к потере всех настроек - setConfirmDeleteModal(true)} - sx={{ marginTop: '20px' }} - text={'Удалить аккаунт'} - style={{ - width: '100%', - backgroundColor: 'rgba(255, 35, 114, 0.10)', - color: '#FF2372', - }} - /> - - - setConfirmDeleteModal(false)}> - - Удаление аккаунта - Вы действительно хотите удалить ваш аккаунт? - - - - - ) : scope === 'business' ? ( - - {status == 'pending' || type === null ? ( - - - - ) : ( - {body()} - )} - - ) : scope === 'subscribe' ? ( - - ) : scope === 'referral' && account_type === 'regular' ? ( - - ) : ( - <> - )} + {renderCurrentContent} @@ -0,0 +1,129 @@ +import React, { useEffect, useState } from 'react' +import ExpandMoreIcon from '@mui/icons-material/ExpandMore' +import { Accordion, AccordionDetails, AccordionSummary, Box, Stack, Typography } from '@mui/material' +import { useSession } from 'next-auth/react' + +import styles from '#/app/styles/business.module.scss' +import paymentStyles from '#/widgets/payment/ui/payment.module.scss' + +import { useUserStore } from '#/entities/user-account' +import { NextPageWithLayout } from '#/pages/_app' +import { accountApi } from '#/shared/api/account-endpoints' +import { useShowDataStore } from '#/shared/lib/hooks/use-show-data' +import { IOffer } from '#/widgets/payment/ui/payment' +import Offer from '#/widgets/payment/ui/offer' + +const PaymentPage: NextPageWithLayout = () => { + const [offers, setOffers] = useState(null) + const [selectedOffer, setSelectedOffer] = useState('') + const { data } = useSession() + const { showMessage } = useShowDataStore() + const { getUser } = useUserStore() + + useEffect(() => { + if (data?.access) { + accountApi + .getPaymentsPlans(data?.access) + .then((res) => { + setOffers(res) + if (res && res.length > 0) { + setSelectedOffer(res[0].uid) + } + }) + .catch(() => { + showMessage('Ошибка загрузки тарифов', 'error') + }) + } + }, [data?.access, showMessage]) + + const pay = async (uid: string) => { + if (!data?.access) return + + try { + const urlForPay = await accountApi.payProduct(data.access, uid) + if (urlForPay) { + window.open(urlForPay, '_blank') + } else { + showMessage('Ошибка создания платежа', 'error') + } + } catch (error) { + showMessage('Ошибка создания платежа', 'error') + } + } + + const info = [ + { + title: 'Что такое токены?', + text: 'Токен — это виртуальная валюта AIR, которую пользователи тратят на генерации. Их цена зависит от длины запроса, настроек нейросети и типа контента. Например, текст в ChatGPT будет стоить примерно 1 токен, а картинка в Kandinsky — примерно 4 токена.\n', + }, + { + title: 'Где проверять баланс токенов? Как следить за расходами?\n', + text: 'Траты по всем категориям и текущий баланс можно проверять в разделе «‎Дашборд».', + }, + ] + + return ( + + + + + {offers?.map((offer) => { + return ( + setSelectedOffer(uid)} + key={offer.uid} + /> + ) + })} + + + + + Часто задаваемые вопросы + + {info.map((el) => { + return ( + + p': { + fontSize: '18px !important', + }, + }} + expandIcon={} + aria-controls='panel1a-content' + id='panel1a-header' + > + {el.title} + + + {el.text} + + + ) + })} + + + + ) +} + +export default PaymentPage @@ -0,0 +1,328 @@ +import React, { useEffect, useRef, useState } from 'react' +import { Avatar, Box, Button, Stack, Typography } from '@mui/material' +import Image from 'next/image' +import { signOut, useSession } from 'next-auth/react' + +import styles2 from '#/app/styles/accountTabs.module.css' +import styles from '#/app/styles/business.module.scss' + +import { useUser, useUserStore } from '#/entities/user-account' +import { NextPageWithLayout } from '#/pages/_app' +import { ButtonUI, Input, Loader, Modal, SwitchCustom } from '#/shared' +import { getDeviceType } from '#/shared/lib/helpers' +import { useShowDataStore } from '#/shared/lib/hooks/use-show-data' + +const UserSettings: NextPageWithLayout = () => { + const { getUser, loaded, unfollowEmail, setUser } = useUser() + + const { + email, + is_subscribed_to_emails, + first_name, + last_name, + username, + profile_picture_link, + is_social, + } = getUser() + + const device = getDeviceType() + const desktop = device === 'desktop' + + const [name, setName] = useState('') + const [lastName, setLastName] = useState('') + const [userName, setUserName] = useState('') + const fileInputRef = useRef(null) + const [loading, setLoading] = useState(false) + const { data } = useSession() + + const [currentPassword, setCurrentPassword] = useState('') + const [newPassword1, setNewPassword1] = useState('') + const [newPassword2, setNewPassword2] = useState('') + const [promocode, setPromocode] = useState('') + const [success, setSuccess] = useState('') + const [confirmDeleteModal, setConfirmDeleteModal] = useState(false) + + const { showMessage } = useShowDataStore() + + const handleDivClick = () => { + ;(fileInputRef.current! as any).click() + } + + const handleFileChange = async (event: any) => { + try { + setLoading(true) + await new Promise((resolve) => setTimeout(resolve, 1000)) + + setUser({ ...getUser(), profile_picture_link: URL.createObjectURL(event.target.files[0]) }) + showMessage('Изображение успешно загружено!') + setLoading(false) + } catch (e) { + setLoading(false) + showMessage('Ошибка загрузки изображения!', 'error') + } + } + + const changePassword = async () => { + if (!data) return + if (newPassword1 !== newPassword2) { + showMessage('Укажите одинаковые новые пароли!') + return + } + showMessage('Пароль успешно изменён!') + setNewPassword1('') + setNewPassword2('') + setCurrentPassword('') + setTimeout(() => setSuccess(''), 6000) + } + + const isUserDataChange = React.useMemo( + () => name !== first_name || last_name !== lastName || username !== userName, + [name, lastName, userName] + ) + + const promocodeActivate = async () => { + if (!data) return + if (!promocode.trim()) { + showMessage('Введите корректный промокод') + return + } + + showMessage('Промокод успешно активирован! Токены уже зачислены!') + setPromocode('') + } + + async function changeUserData() { + if (!data) return + if (!isUserDataChange) { + showMessage('Вы не изменили данные', 'success') + return + } + showMessage('Данные успешно изменены!') + setUser({ ...getUser(), username: userName, email, first_name: name, last_name: lastName }) + } + + const deleteAccount = async () => { + if (!data) return + await signOut() + } + + useEffect(() => { + setName(first_name) + setLastName(last_name) + setUserName(username) + }, [first_name, last_name, username]) + + return ( + + + Основные + + + + + + {!loading || !loaded ? ( + {'Image + ) : ( + + )} + + + {''} + + + + + + Имя + setName(e.target.value)} + fullWidth + /> + + + Фамилия + setLastName(e.target.value)} + fullWidth + /> + + + + Email + + { + if (!data) return + await unfollowEmail() + showMessage('Данные изменены!') + }} + /> + + Отписаться от рассылки + + + + + + + Никнейм + setUserName(e.target.value)} + fullWidth + /> + + + + + + + Активация промокода + setPromocode(e.target.value)} + fullWidth + /> + + + {!is_social && ( + + Изменить пароль + + + Текущий пароль + setCurrentPassword(e.target.value)} + fullWidth + /> + + + + Новый пароль + setNewPassword1(e.target.value)} + fullWidth + /> + + + + Подтвердить пароль + + setNewPassword2(e.target.value)} + fullWidth + /> + + + + + + )} + + Удаление аккаунта + + Удаление аккаунта приведет к потере всех настроек + + setConfirmDeleteModal(true)} + sx={{ marginTop: '20px' }} + text={'Удалить аккаунт'} + style={{ + width: '100%', + backgroundColor: 'rgba(255, 35, 114, 0.10)', + color: '#FF2372', + }} + /> + + + setConfirmDeleteModal(false)}> + + Удаление аккаунта + + Вы действительно хотите удалить ваш аккаунт? + + + + + + ) +} + +export default UserSettings @@ -0,0 +1,77 @@ +import React, { useEffect, useMemo } from 'react' +import { Box, CircularProgress } from '@mui/material' + +import styles from '#/app/styles/business.module.scss' + +import { ExpensesBlock, MailingBlock, useBusinessHostData } from '#/features/business-host-data' +import { useBusinessLists } from '#/features/business-host-data/lib/use-business-lists' +import { InfoAdapter } from '#/features/business-host-data/ui/info-adapter' +import { ModelsList } from '#/widgets/business-models' +import { PersonsList } from '#/widgets/business-persons' +import { BusinessGroups } from '#/widgets/business-persons/ui/business-group' +import { LogList } from '#/widgets/business-persons/ui/log-list' +import { SecurityList } from '#/widgets/business-persons/ui/security-list' +import { NextPageWithLayout } from '#/pages/_app' + +export const BusinessHost: NextPageWithLayout = () => { + const { addList } = useBusinessLists() + + const { + mailing, + updateMailingSettings, + downloadExpensesReport, + emails, + setEmails, + fetchData, + updateMailingEmails, + info, + isLoading, + personsList, + setPersonsList, + setSecurityList, + securityList, + } = useBusinessHostData() + + useEffect(() => { + fetchData() + }, []) + + if (isLoading) { + return ( + + + + ) + } + + return ( + + + + + + + + + + + + {/* */} + + + + + + updateMailingEmails(emails)} + /> + + ) +} @@ -1 +1,4 @@ export * from './ui' +export * from './business' +export * from './settings' +export * from './payment' @@ -0,0 +1,26 @@ +import { Box, CircularProgress } from '@mui/material' +import { useSession } from 'next-auth/react' + +import PaymentPage from './ui/payment-page' + +import styles from '#/app/styles/business.module.scss' + +import { NextPageWithLayout } from '#/pages/_app' + +export const Payment: NextPageWithLayout = () => { + const { data, status } = useSession() + + if (status === 'loading') { + return ( + + + + ) + } + + return ( + + + + ) +} @@ -0,0 +1,27 @@ +import React from 'react' +import { Box, CircularProgress } from '@mui/material' + +import UserSettings from './ui/user-settings' + +import styles from '#/app/styles/business.module.scss' + +import { useUserStore } from '#/entities/user-account' +import { NextPageWithLayout } from '#/pages/_app' + +export const Settings: NextPageWithLayout = () => { + const { getUser, loaded } = useUserStore() + + if (!loaded) { + return ( + + + + ) + } + + return ( + + + + ) +} @@ -1,17 +1,17 @@ import React from 'react' import { Stack } from '@mui/material' - -import { useAppSelector } from '#/app/store/store' -import { StatsField } from '#/widgets/stats-field' import { useSession } from 'next-auth/react' -import { getDeviceType } from '#/shared/lib/helpers' + +import { useThemeStore } from '#/entities/theme/model/use-theme-store' import { NextPageWithLayout } from '#/pages/_app' +import { getDeviceType } from '#/shared/lib/helpers' +import { StatsField } from '#/widgets/stats-field' /** * @deprecated */ const Admin: NextPageWithLayout = () => { - const theme = useAppSelector((state) => state.theme.theme) + const theme = useThemeStore((state) => state.theme) const device = getDeviceType() const { data: session } = useSession() @@ -1,14 +1,5 @@ import React, { useEffect, useState } from 'react' -import { - Box, - Stack, - Table, - TableBody, - TableHead, - TableRow, - TextField, - Typography, -} from '@mui/material' +import { Box, Stack, Table, TableBody, TableHead, TableRow, TextField, Typography } from '@mui/material' import Button from '@mui/material/Button' import TableCell from '@mui/material/TableCell' import axios from 'axios' @@ -16,15 +7,16 @@ import Image from 'next/image' import Link from 'next/link' import { useSession } from 'next-auth/react' -import { getAll, ResponseAllInfo } from '#/entities/user-account/model/user-type-slice' +import styles from '#/shared/styles/api-keys.module.scss' + +import { useThemeStore } from '#/entities/theme/model/use-theme-store' +import { ResponseAllInfo, useUserReferralStore } from '#/entities/user-account/model/use-user-referral-store' import { ApiKeyModal } from '#/features/api-key-modal/api-key-modal' -import { useAppSelector } from '#/app/store/store' +import { NextPageWithLayout } from '#/pages/_app' import { TooltipCustom } from '#/shared' import { accountApi } from '#/shared/api/account-endpoints' import { API_URL } from '#/shared/lib/constants' -import styles from '#/shared/styles/api-keys.module.scss' import { InputStyleSmallDark, InputStyleSmallLight } from '#/shared/ui/input' -import { NextPageWithLayout } from '#/pages/_app' const ApiKeys: NextPageWithLayout = () => { const [keys, setKeys] = useState | null>(null) @@ -35,7 +27,41 @@ const ApiKeys: NextPageWithLayout = () => { const [disabled, setDisabled] = React.useState(true) useEffect(() => { - getAll(data?.access).then((res) => setUserInfo(res)) + // Временно используем пустой объект + const res: ResponseAllInfo = { + uid: '', + first_name: '', + last_name: '', + username: '', + created_at: '', + email: '', + is_active: false, + is_staff: false, + show_balance: true, + is_confirmed: false, + is_social: false, + social_auth: [], + is_subscribed_to_emails: false, + profile_picture_link: null, + account_type: 'regular', + referral_code: { code: '' }, + token: { access: '', refresh: '' }, + payment_plan: { + uid: '', + plan: { + uid: '', + price: '', + tokens_per_plan: '', + title: '', + duration: '', + accessed_models: null, + }, + last_payment_at: '', + next_payment_at: '', + current_token_balance: 0, + }, + } + setUserInfo(res) }, [data?.access]) useEffect(() => { @@ -86,9 +112,9 @@ const ApiKeys: NextPageWithLayout = () => { - API-ключ — это инструмент, который идентифицирует пользователя или - программу, запрашивающих доступ к API платформы. С помощью ключа можно - отслеживать, кто и когда пользуется API, рассчитывать оплату. + API-ключ — это инструмент, который идентифицирует пользователя или программу, + запрашивающих доступ к API платформы. С помощью ключа можно отслеживать, кто и + когда пользуется API, рассчитывать оплату. + + {showPersons && ( +
+
+ setSearch(e.target.value)} + /> +
+ + {/* TODO: Переписать без mui */} + + + + + Название группы + Лимит токенов + + + + + {searchBussinessGroups.map((group) => { + return ( + + + {group.title} + + + {group.token_limit + ? group.token_limit.split('.')[0] + : 'Не указано'} + + + { + setCurrentGroup(group) + changeModal.setState(true) + }} + className={styles.tableLimit} + align='right' + > + + Просмотр + + + + { + deleteGroup(group.uid) + }} + src={'/x-mark.svg'} + width={15} + height={15} + alt={'Удалить'} + /> + + + ) + })} + +
+
+
+ )} + + + ) +} @@ -7,60 +7,27 @@ import TableCell from '@mui/material/TableCell' 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 { ResponseGetPersons } from '#/features/invite-person-in-business' -import { ResponseGetIpList } from '#/features/invite-person-in-business/model/types' import { Search } from '#/shared' -import styles from '#/widgets/business-models/ui/models-list/models-list.module.scss' +import styles from '#/widgets/business-models/ui/models-list.module.scss' import ArrowUpOrDown from '#/widgets/top-bar-model/ui/arrow-up-or-down' - -import { getIpList } from '../../api/get-ipList' +import { useIpList } from '../model' export const IpList = () => { - const [ipList, setIpList] = useState() - - const [searchIp, setSearchIp] = useState(ipList?.ips ? ipList.ips : []) - - const { data } = useSession() - - const [search, setSearch] = useState('') + const { fetchIpList, ipList, searchedIps, search, setSearch } = useIpList() const [showPersons, setShowPersons] = useState(true) useEffect(() => { - getIpList(data?.access).then((res) => setIpList(res ? res : null)) - }, [data?.access]) - - useEffect(() => { - setSearchIp((pre) => { - if (!ipList) { - return null - } - - if (search) { - return ipList?.ips.filter((el) => el.includes(search)) - } - return ipList?.ips - }) - }, [search]) - - useEffect(() => { - if (search === '') { - setSearchIp(ipList?.ips) - } - setSearchIp(ipList?.ips) - }, [ipList]) + fetchIpList() + }, []) return ( Список IP-адресов - {ipList && ipList.ips && ipList.ips.length !== 0 ? ( - {ipList?.ips.length} - ) : ( - <> + {ipList.length !== 0 && ( + {ipList.length} )} { - {ipList && searchIp && searchIp.length > 0 ? ( - searchIp.map((ip) => { + {searchedIps.length > 0 ? ( + searchedIps.map((ip) => { return ( - + {ip} @@ -11,55 +11,25 @@ import { Dayjs } from 'dayjs' import { useSession } from 'next-auth/react' import { ResponseGetLogsList } from '#/features/invite-person-in-business/model/types' -import { useAppSelector } from '#/app/store/store' +import { useThemeStore } from '#/entities/theme/model/use-theme-store' import { Search } from '#/shared' import { DateInput } from '#/shared/ui/date-input/date-input' -import styles from '#/widgets/business-models/ui/models-list/models-list.module.scss' +import styles from '#/widgets/business-models/ui/models-list.module.scss' import { getLogsList } from '#/widgets/business-persons/api/get-logsList' import ArrowUpOrDown from '#/widgets/top-bar-model/ui/arrow-up-or-down' +import { useLogList } from '../model' export const LogList = () => { - const [logs, setLogs] = useState() - const [searchLogs, setSearchLogs] = useState(logs ? logs : null) - - const [fromDate, setFromDate] = useState('') - const [toDate, setToDate] = useState('') - const [offset, setOffset] = useState(0) - const [logType, setLogType] = useState<'ip' | 'public-service'>('ip') + const { fetchLogs, setFrom, to, from, setTo, type, setType, search, setSearch, searchedLogs } = + useLogList() const [showPersons, setShowPersons] = useState(true) - const [search, setSearch] = useState('') - const theme = useAppSelector((state) => state.theme.theme) - const { data } = useSession() - - useEffect(() => { - getLogsList(offset, 20, logType, fromDate, toDate, data?.access).then((res) => setLogs(res ? res : null)) - }, [data?.access, logType, fromDate, toDate]) - useEffect(() => { - setSearchLogs((pre) => { - if (!logs) { - return null - } - - if (search) { - return logs?.filter( - (el) => - el.user.toLowerCase().includes(search.toLowerCase()) || - el.message.toLowerCase().includes(search.toLowerCase()) - ) - } else { - return logs - } - }) - }, [search]) + const theme = useThemeStore((state) => state.theme) useEffect(() => { - if (search === '') { - setSearchLogs(logs) - } - setSearchLogs(logs) - }, [logs]) + fetchLogs() + }, [type, to, from]) return ( @@ -78,10 +48,16 @@ export const LogList = () => { - + setFrom(date ? date.toString() : undefined)} + label={'От'} + /> - + setTo(date ? date.toString() : undefined)} + label={'До'} + /> { > @@ -139,16 +115,21 @@ export const LogList = () => { - {searchLogs && searchLogs.length !== 0 ? ( - searchLogs?.map((log, idx) => { + {searchedLogs.length !== 0 ? ( + searchedLogs.map((log, idx) => { return ( - + {log.user} @@ -167,7 +148,9 @@ export const LogList = () => { .split(':')[1] } - {log.message} + + {log.message} + ) }) @@ -183,7 +166,10 @@ export const LogList = () => { className={styles.tableEmail} align='center' > - + Логов не найдено @@ -13,106 +13,69 @@ 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 { Error, formatDate, Search } 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, formatDateStatus, formatEmail } from '../../lib/lib' -import { getModalById, PLATE_CHANGE_PASSWORD, RESEND_INVATION_PASSWORD } from '#/features/modals' +import { translateEmailStatus, formatDateStatus, formatEmail } from '../lib/lib' +import { + getModalById, + INVITE_USER, + LIMIT_MODAL, + 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' +import { usePersonsList } from '../model' export const PersonsList = ({ personsList, setPersonsList, addList, }: { - personsList: ResponseGetPersons[] | null - setPersonsList: React.Dispatch> + personsList: ResponseGetPersons[] + setPersonsList: (list: ResponseGetPersons[]) => void addList: (newUser: ResponseGetPersons, list: 'personal' | 'security') => void }) => { - const [persons, setPersons] = useState([]) - const [searchPersons, setSearchPersons] = useState(persons) - const [inviteModal, setInviteModal] = useState(false) - const [currentPerson, setCurrentPerson] = useState(null) - + const { + searchedList, + showNewPerson, + setShowPersons, + showPersons, + currentPerson, + search, + setSearch, + setCurrentPerson, + } = usePersonsList(personsList, setPersonsList) + + const inviteModal = getModalById(INVITE_USER) const passChangeModal = getModalById(PLATE_CHANGE_PASSWORD) const passResendModal = getModalById(RESEND_INVATION_PASSWORD) - const { data: session } = useSession() - - useEffect(() => { - setPersons(personsList) - }, [personsList]) - - const showNewPerson = (data: ResponseGetPersons) => { - setShowPersons(true) - setPersons((prev) => { - if (prev !== null) { - return [data, ...prev] - } - return [data] - }) - - showMessage('Пользователь успешно приглашен!', 'success') - } - - const [search, setSearch] = useState('') - - const [showPersons, setShowPersons] = useState(true) - - const { showMessage } = useShowDataStore() - const updateLimit = (limit: string, email?: string) => { - setPersons((prev) => - prev!.map((el) => { - if (el.email === email) { - el.token_limit = limit - return el - } - - return el - }) - ) - setCurrentPerson(null) - showMessage(`Лимит пользователя ${email} успешно изменён!`, 'success') - } - - useEffect(() => { - setSearchPersons((pre) => { - if (!persons) { - return null - } - - if (search) { - return persons?.filter((el) => el.email.includes(search)) - } - return persons - }) - }, [search]) - - useEffect(() => { - if (search === '') { - setSearchPersons(persons) - } - setSearchPersons(persons) - }, [persons]) + const limitModal = getModalById(LIMIT_MODAL) return ( Сотрудники - {persons?.length !== 0 && {persons?.length}} - setShowPersons((prev) => !prev)} className={styles.arrow} /> + {personsList.length !== 0 && ( + {personsList.length} + )} + setShowPersons((prev) => !prev)} + className={styles.arrow} + /> - setInviteModal(true)}>Добавить сотрудника + inviteModal.setState(true)}>Добавить сотрудника {showPersons && ( - {persons?.length === 0 ? ( + {personsList.length === 0 ? ( Добавьте первого сотрудника @@ -140,50 +103,77 @@ export const PersonsList = ({ - {searchPersons?.map((person: ResponseGetPersons) => { - const statusPerson = translateEmailStatus(person.acceptance_status) + {searchedList.map((person: ResponseGetPersons) => { + const statusPerson = translateEmailStatus( + person.acceptance_status + ) return ( - + {formatEmail(person.email)} - {RoleSelect[person.account_type]} + + {RoleSelect[person.account_type]} + {statusPerson + ' '} - {statusPerson === 'Приглашен' ? formatDateStatus(person.created_at) : null} + {statusPerson === 'Приглашен' + ? formatDateStatus( + person.created_at + ) + : null} {statusPerson === 'Приглашен' ? ( ) : null} - {Math.floor(+person.token_limit)} + + {Math.floor(+person.token_limit)} +