@@ -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,35 @@ +import React, { createContext, ReactNode,useContext } from 'react' + +import { useBusinessHostStore } from '../model/business-host.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, isLoading, error, securityList, personsList } = useBusinessHostStore() + + 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,22 @@ +import { useEffect } from 'react' +import { useSession } from 'next-auth/react' + +import { useBusinessHostStore } from '../model/business-host.store' + +import { useBusinessHostData } from './use-business-host-data' + +export const useBusinessHostCache = () => { + const { data } = useSession() + const { isLoading, isDataLoaded } = useBusinessHostStore() + const { loadBusinessHostData } = useBusinessHostData() + + useEffect(() => { + if (data?.access && !isDataLoaded && !isLoading) { + loadBusinessHostData() + } + }, [data?.access, isDataLoaded, isLoading]) + + return { + hasLoaded: isDataLoaded, + } +} @@ -0,0 +1,110 @@ +import { useEffect } from 'react' +import { useSession } from 'next-auth/react' + +import { businessHostApi } from '../api/business-host-api' +import { useBusinessHostStore } from '../model/business-host.store' + +export const useBusinessHostData = () => { + const { data } = useSession() + + const { + securityList, + personsList, + info, + mailing, + isLoading, + error, + isDataLoaded, + setSecurityList, + setPersonsList, + setInfo, + setMailing, + setIsLoading, + setError, + setIsDataLoaded, + } = useBusinessHostStore() + + const loadBusinessHostData = async () => { + if (!data?.access) return + + setIsLoading(true) + setError(null) + + try { + const [securityPersons, regularPersons, businessInfo] = await Promise.all([ + businessHostApi.getSecurityPersons(data.access), + businessHostApi.getRegularPersons(data.access), + businessHostApi.getBusinessInfo(data.access), + ]) + + setSecurityList(securityPersons) + setPersonsList(regularPersons) + setInfo(businessInfo) + setIsDataLoaded(true) + + if (businessInfo) { + setMailing(businessInfo.token_cap_enabled) + } + } catch (err) { + setError('Ошибка загрузки данных') + } finally { + setIsLoading(false) + } + } + + const updateMailingSettings = async (newMailing: boolean) => { + if (!data?.access) return + + try { + await businessHostApi.updateMailingSettings(data.access, newMailing) + setMailing(newMailing) + } catch (err) { + setError('Ошибка обновления настроек') + } + } + + const downloadExpensesReport = async (fromDate?: string, toDate?: string) => { + if (!data?.access) return + + try { + const blob = await businessHostApi.downloadExpenses(data.access, fromDate, toDate) + const link = document.createElement('a') + link.href = window.URL.createObjectURL(blob) + link.download = 'Expenses.xlsx' + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + } catch (err) { + setError('Ошибка скачивания отчета') + } + } + + const updateMailingEmails = async (emails: string[]) => { + if (!data?.access) return + + try { + await businessHostApi.updateMailingEmails(data.access, emails) + } catch (err) { + setError('Ошибка обновления списка email') + } + } + + useEffect(() => { + if (!data?.access) { + setIsDataLoaded(false) + } + }, [data?.access, setIsDataLoaded]) + + return { + securityList, + personsList, + info, + mailing, + isLoading, + error, + loadBusinessHostData, + updateMailingSettings, + downloadExpensesReport, + updateMailingEmails, + } +} @@ -0,0 +1,27 @@ +import { useBusinessHostStore } from '../model/business-host.store' + +import { ResponseGetPersons } from '#/features/invite-person-in-business' + +export const useBusinessLists = () => { + const { securityList, personsList, addPersonToList } = useBusinessHostStore() + + 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,52 @@ +import { useBusinessInfoStore } from './business-info.store' +import { useLoadingStore } from './loading.store' +import { usePersonsStore } from './persons.store' +import { useReportsStore } from './reports.store' + +export const useBusinessHostStore = () => { + const personsStore = usePersonsStore() + const businessInfoStore = useBusinessInfoStore() + const reportsStore = useReportsStore() + const loadingStore = useLoadingStore() + + return { + securityList: personsStore.securityList, + personsList: personsStore.personsList, + setSecurityList: personsStore.setSecurityList, + setPersonsList: personsStore.setPersonsList, + addPersonToList: personsStore.addPersonToList, + resetLists: personsStore.resetLists, + + info: businessInfoStore.info, + mailing: businessInfoStore.mailing, + setInfo: businessInfoStore.setInfo, + setMailing: businessInfoStore.setMailing, + resetInfo: businessInfoStore.resetInfo, + + fromDate: reportsStore.fromDate, + toDate: reportsStore.toDate, + setFromDate: reportsStore.setFromDate, + setToDate: reportsStore.setToDate, + resetDates: reportsStore.resetDates, + + isLoading: loadingStore.isLoading, + error: loadingStore.error, + isDataLoaded: loadingStore.isDataLoaded, + setIsLoading: loadingStore.setIsLoading, + setError: loadingStore.setError, + setIsDataLoaded: loadingStore.setIsDataLoaded, + resetLoading: loadingStore.resetLoading, + + resetState: () => { + personsStore.resetLists() + businessInfoStore.resetInfo() + reportsStore.resetDates() + loadingStore.resetLoading() + }, + } +} + +export { usePersonsStore } from './persons.store' +export { useBusinessInfoStore } from './business-info.store' +export { useReportsStore } from './reports.store' +export { useLoadingStore } from './loading.store' @@ -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,50 @@ +import { create } from 'zustand' +import { persist } from 'zustand/middleware' + +import { ResponseGetPersons } from '#/features/invite-person-in-business' + +export interface PersonsStore { + securityList: ResponseGetPersons[] | null + personsList: ResponseGetPersons[] | null + + setSecurityList: (securityList: ResponseGetPersons[] | null) => void + setPersonsList: (personsList: ResponseGetPersons[] | null) => void + addPersonToList: (person: ResponseGetPersons, listType: 'personal' | 'security') => void + resetLists: () => void +} + +export const usePersonsStore = create()( + persist( + (set, get) => ({ + securityList: null, + personsList: null, + + 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: null, personsList: null }), + }), + { + 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/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,125 @@ +import React, { 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/models-list.module.scss' + +import { useAppSelector } from '#/app/store/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 + onUpdateEmails: (emails: string[]) => Promise +} + +export const MailingBlock: React.FC = ({ info, mailing, onUpdateEmails }) => { + const [showPersons, setShowPersons] = useState(true) + const [newMail, setNewMail] = useState('') + const theme = useAppSelector((state) => state.theme.theme) + const [allMails, setAllMails] = useState() + const [isMailing, setIsMailing] = useState(false) + + useEffect(() => { + if (info) { + setAllMails(info?.token_cap_emails) + setIsMailing(info?.token_cap_enabled) + } + }, [info]) + + useEffect(() => { + if (allMails) { + onUpdateEmails(allMails).then(() => { + setNewMail('') + }) + } + }, [allMails, onUpdateEmails]) + + const addMailingUser = async () => { + if (newMail !== '') { + setAllMails((emails) => [...(emails ?? []), newMail]) + } + } + + const removeEmail = (emailToRemove: string) => { + setAllMails(allMails?.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 } + } + /> + + + + + + {allMails?.map((el) => ( +
+

{el}

+ removeEmail(el)} + > + + + +
+ ))} +
+ + )} +
+ ) +} @@ -0,0 +1,7 @@ +export { useBusinessHostData } from './lib/use-business-host-data' +export { useBusinessHostCache } from './lib/use-business-host-cache' +export { businessHostApi } from './api/business-host-api' +export { useBusinessHostStore } from './model/business-host.store' +export { MailingBlock } from './ui/mailing-block' +export { ExpensesBlock } from './ui/expenses-block' +export { InfoAdapter } from './ui/info-adapter' @@ -0,0 +1,14 @@ +import React from 'react' +import { Box } from '@mui/material' + +import styles from '#/app/styles/business.module.scss' + +const AccountBusinessAccount: React.FC = () => { + return ( + + Информация о корп.аккаунте доступна только владельцу и администраторам. + + ) +} + +export default AccountBusinessAccount @@ -0,0 +1,9 @@ +import React from 'react' + +import BusinessHost from '#/widgets/business-host/business-host' + +const AccountBusinessHost: React.FC = () => { + return +} + +export default AccountBusinessHost @@ -0,0 +1,41 @@ +import React from 'react' +import { Box, Typography } from '@mui/material' + +import styles from '#/app/styles/business.module.scss' + +import { DownloadModal } from '#/features/business-security-download' +import { ButtonUI } from '#/shared' +import { Info } from '#/widgets/business-info' + +interface AccountBusinessSecurityProps { + downloadModal: boolean + setDownloadModal: React.Dispatch> +} + +const AccountBusinessSecurity: React.FC = ({ + downloadModal, + setDownloadModal, +}) => { + return ( + <> + + + + + Раздел 1. Запросы сотрудников по моделям + + + { + setDownloadModal(true) + }} + sx={{ marginTop: '20px' }} + text={'Скачать'} + /> + + + + ) +} + +export default AccountBusinessSecurity @@ -0,0 +1,38 @@ +import React from 'react' + +import AccountBusinessAccount from './account-business-account' +import AccountBusinessHost from './account-business-host' +import AccountBusinessSecurity from './account-business-security' +import AccountRegular from './account-regular' + +interface AccountContentFactoryProps { + type: string | null + downloadModal: boolean + setDownloadModal: React.Dispatch> +} + +const AccountContentFactory: React.FC = ({ + type, + downloadModal, + setDownloadModal, +}) => { + if (type === 'regular') { + return + } + + if (type === 'business_account') { + return + } + + if (type === 'business_host' || type === 'business_admin') { + return + } + + if (type === 'business_security') { + return + } + + return null +} + +export default AccountContentFactory @@ -0,0 +1,14 @@ +import React from 'react' +import { Box } from '@mui/material' + +import { ScreenForInactive } from '#/widgets/business' + +const AccountRegular: React.FC = () => { + return ( + + + + ) +} + +export default AccountRegular @@ -0,0 +1,392 @@ +import React, { useEffect, useRef, useState } from 'react' +import { Avatar, Box, Button, Stack, Typography } from '@mui/material' +import axios from 'axios' +import Image from 'next/image' +import { signOut, useSession } from 'next-auth/react' + +import styles2 from '#/app/styles/accountTabs.module.css' + +import { useAppDispatch, useAppSelector } from '#/app/store/store' +import { getAllInfo, unfollowEmail } from '#/entities/user-account/model/user-type-slice' +import { ButtonUI, Input, Loader, Modal, SwitchCustom } from '#/shared' +import { accountApi } from '#/shared/api/account-endpoints' +import { API_URL } from '#/shared/lib/constants/constants' +import { getDeviceType } from '#/shared/lib/helpers' +import { useShowDataStore } from '#/shared/lib/hooks/use-show-data' + +const AccountSettings: React.FC = () => { + const { + email, + is_subscribed_to_emails, + first_name, + last_name, + username, + profile_picture_link, + is_social, + status: userInfoLoaded, + } = useAppSelector((state) => state.user) + + 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 dispatch = useAppDispatch() + const { showMessage } = useShowDataStore() + + const handleDivClick = () => { + ;(fileInputRef.current! as any).click() + } + + const handleFileChange = async (event: any) => { + const formData = new FormData() + formData.append('new_picture', event.target.files[0]) + + 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 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 + } + + showMessage('К сожалению, произошла ошибка') + } + + 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 + } + 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('Промокод успешно активирован! Токены уже зачислены!') + return + } + + if (resStatus === 403) { + showMessage('Промокод уже был активирован!') + return + } + + if (resStatus === 404) { + showMessage('Промокод не найден!') + return + } + } + + async function changeUserData() { + if (!data) return + if (!isUserDataChange) { + showMessage('Вы не изменили данные', 'success') + return + } + + 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]) + + return ( + <> + + Основные + + + + + + {!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 + /> + + + + + + + Активация промокода + 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 AccountSettings @@ -1,271 +1,74 @@ -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, { useEffect, 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 { 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 } from '../config' + +import AccountContentFactory from './account-content-factory' +import AccountSettings from './account-settings' + 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 { useAppSelector } from '#/app/store/store' +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]) - - 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 [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 { account_type: type } = useAppSelector( + (state) => ({ + account_type: state.user.account_type, + }), + (prev, next) => prev.account_type === next.account_type + ) - const [confirmDeleteModal, setConfirmDeleteModal] = useState(false) + const status = useAppSelector( + (state) => state.user.status, + () => true + ) const { push, query } = useRouter() - const [scope, setScope] = useState(query['scope'] || 'setting') - 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) + const queryScope = query['scope'] as string + if (queryScope && queryScope !== scope) { + setScope(queryScope) } - }, [query]) + }, [query['scope']]) useEffect(() => { - if (Object.keys(query).length === 0) addQueryParams('setting') - }, []) - - 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 - } - - showMessage('К сожалению, произошла ошибка') - } - - 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={'Скачать'} - /> - - - - ) - } - } - - 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 - } - - try { - await axios.put( - API_URL + '/auth/user-data', + if (!query['scope']) { + push( { - username: userName, - email: email, - first_name: name, - last_name: lastName, + pathname: '/account', + query: { scope: 'setting' }, }, - { headers: { Authorization: `Bearer ${data.access}` } } + undefined, + { shallow: true } ) + } + }, []) - 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) {} + const handleTabChange = (newScope: string) => { + setScope(newScope) + push( + { + pathname: '/account', + query: { scope: newScope }, + }, + undefined, + { shallow: true } + ) } - useEffect(() => { - setName(first_name) - setLastName(last_name) - setUserName(username) - }, [first_name, last_name, username]) - return ( { if (!desktop && el.scope === 'business') { return null } - if (el.scope === 'referral' && account_type !== 'regular') { + if (el.scope === 'referral' && type !== 'regular') { return null } return ( changeScope(el.scope)} + onClick={() => handleTabChange(el.scope)} key={el.title} - className={scope === el.scope ? styles2.wrap_toggle_button_active : styles2.wrap_toggle_button} + className={ + scope === el.scope + ? styles2.wrap_toggle_button_active + : styles2.wrap_toggle_button + } value={el.scope} label={el.title} /> @@ -320,233 +127,30 @@ const Account: NextPageWithLayout = () => { })} {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 ? ( + {status === 'pending' || type === null ? ( ) : ( - {body()} + )} ) : scope === 'subscribe' ? ( - ) : scope === 'referral' && account_type === 'regular' ? ( + ) : scope === 'referral' && type === 'regular' ? ( - ) : ( - <> - )} + ) : null} @@ -0,0 +1,117 @@ +import React, { 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 { useBusinessHostCache } from '#/features/business-host-data/lib/use-business-host-cache' +import { useBusinessLists } from '#/features/business-host-data/lib/use-business-lists' +import { useBusinessHostStore } from '#/features/business-host-data/model/business-host.store' +import { InfoAdapter } from '#/features/business-host-data/ui/info-adapter' +import { useShowDataStore } from '#/shared/lib/hooks/use-show-data' +import { ModelsList } from '#/widgets/business-models' +import { PersonsList } from '#/widgets/business-persons' +import { BusinessGroups } from '#/widgets/business-persons/ui/persons-list/business-group' +import { IpList } from '#/widgets/business-persons/ui/persons-list/ip-list' +import { LogList } from '#/widgets/business-persons/ui/persons-list/log-list' +import { SecurityList } from '#/widgets/business-persons/ui/persons-list/security-list' + +export const BusinessHostMemoized: React.FC = () => { + const { showMessage } = useShowDataStore() + + useBusinessHostCache() + + const { info, isLoading, error, securityList, personsList } = useBusinessHostStore() + const { addList } = useBusinessLists() + + const { mailing, updateMailingSettings, downloadExpensesReport, updateMailingEmails } = + useBusinessHostData() + + const handleMailingChange = useMemo( + () => (newMailing: boolean | undefined) => { + if (newMailing !== undefined) { + updateMailingSettings(newMailing) + .then(() => { + showMessage('Настройки уведомлений обновлены') + }) + .catch(() => { + showMessage('Ошибка обновления настроек', 'error') + }) + } + }, + [updateMailingSettings, showMessage] + ) + + const handleDownloadExpenses = useMemo( + () => async (fromDate?: string, toDate?: string) => { + try { + await downloadExpensesReport(fromDate, toDate) + showMessage('Отчет успешно скачан') + } catch (err) { + showMessage('Ошибка скачивания отчета', 'error') + } + }, + [downloadExpensesReport, showMessage] + ) + + const handleUpdateEmails = useMemo( + () => async (emails: string[]) => { + try { + await updateMailingEmails(emails) + } catch (err) { + showMessage('Ошибка обновления списка email', 'error') + } + }, + [updateMailingEmails, showMessage] + ) + + const content = useMemo(() => { + if (isLoading) { + return ( + + + + ) + } + + return ( + + + + + + + + + + {}} addList={addList} /> + + + + + + + + {}} addList={addList} /> + + ) + }, [ + isLoading, + info, + mailing, + handleMailingChange, + handleUpdateEmails, + handleDownloadExpenses, + personsList, + securityList, + addList, + ]) + + return content +} @@ -0,0 +1,94 @@ +import React, { useEffect } 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 { useBusinessHostCache } from '#/features/business-host-data/lib/use-business-host-cache' +import { useBusinessLists } from '#/features/business-host-data/lib/use-business-lists' +import { useBusinessHostStore } from '#/features/business-host-data/model/business-host.store' +import { InfoAdapter } from '#/features/business-host-data/ui/info-adapter' +import { useShowDataStore } from '#/shared/lib/hooks/use-show-data' +import { ModelsList } from '#/widgets/business-models' +import { PersonsList } from '#/widgets/business-persons' +import { BusinessGroups } from '#/widgets/business-persons/ui/persons-list/business-group' +import { LogList } from '#/widgets/business-persons/ui/persons-list/log-list' +import { SecurityList } from '#/widgets/business-persons/ui/persons-list/security-list' + +const BusinessHostOptimized: React.FC = () => { + const { showMessage } = useShowDataStore() + + useBusinessHostCache() + + const { info, isLoading, error } = useBusinessHostStore() + const { securityList, personsList } = useBusinessHostStore() + const { addList } = useBusinessLists() + + const { mailing, updateMailingSettings, downloadExpensesReport, updateMailingEmails } = + useBusinessHostData() + + useEffect(() => { + if (error) { + showMessage(error, 'error') + } + }, [error, showMessage]) + + const handleMailingChange = (newMailing: boolean | undefined) => { + if (newMailing !== undefined) { + updateMailingSettings(newMailing) + .then(() => { + showMessage('Настройки уведомлений обновлены') + }) + .catch(() => { + showMessage('Ошибка обновления настроек', 'error') + }) + } + } + + const handleDownloadExpenses = async (fromDate?: string, toDate?: string) => { + try { + await downloadExpensesReport(fromDate, toDate) + showMessage('Отчет успешно скачан') + } catch (err) { + showMessage('Ошибка скачивания отчета', 'error') + } + } + + const handleUpdateEmails = async (emails: string[]) => { + try { + await updateMailingEmails(emails) + } catch (err) { + showMessage('Ошибка обновления списка email', 'error') + } + } + + if (isLoading) { + return ( + + + + ) + } + + return ( + + + + + + + + {}} addList={addList} /> + + {}} addList={addList} /> + + + + + + + + ) +} + +export default BusinessHostOptimized @@ -1,225 +1,9 @@ -import * as React from 'react' -import { useEffect, useState } from 'react' -import { Box, Button, TextField, Typography } from '@mui/material' -import axios from 'axios' -import { Dayjs } from 'dayjs' -import { useSession } from 'next-auth/react' +import React from 'react' -import { getAll, ResponseAllInfo } from '#/entities/user-account/model/user-type-slice' -import { ResponseGetPersons } from '#/features/invite-person-in-business' -import { useAppSelector } from '#/app/store/store' -import styles from '#/app/styles/business.module.scss' -import { Error, InputStyleDark, InputStyleLight } from '#/shared' -import { API_URL } from '#/shared/lib/constants' -import { DateInput } from '#/shared/ui/date-input/date-input' -import { Info } from '#/widgets/business-info' -import { getInfo, InfoBusiness } from '#/widgets/business-info/api/get-info' -import { ModelsList } from '#/widgets/business-models' -import styles2 from '#/widgets/business-models/ui/models-list/models-list.module.scss' -import { PersonsList } from '#/widgets/business-persons' -import { getPersons } from '#/widgets/business-persons/api/get-persons' -import { BusinessGroups } from '#/widgets/business-persons/ui/persons-list/business-group' -import { IpList } from '#/widgets/business-persons/ui/persons-list/ip-list' -import { LogList } from '#/widgets/business-persons/ui/persons-list/log-list' -import { SecurityList } from '#/widgets/business-persons/ui/persons-list/security-list' -import ArrowUpOrDown from '#/widgets/top-bar-model/ui/arrow-up-or-down' -import { useShowDataStore } from '#/shared/lib/hooks/use-show-data' +import BusinessHostOptimized from './ui/business-host-optimized' -type ListType = 'personal' | 'security' - -export default function BusinessHost() { - const [securityList, setSecurityList] = useState([]) - const [personsList, setPersonsList] = useState([]) - const [info, setInfo] = useState() - const [showPersons, setShowPersons] = useState(true) - const [fromDate, setFromDate] = useState('') - const [mailing, setMailing] = useState() - const [toDate, setToDate] = useState('') - const { data } = useSession() - const { message, isOpened } = useShowDataStore() - - useEffect(() => { - getPersons(data?.access, true).then((res) => setSecurityList(res ? res?.reverse() : null)) - getPersons(data?.access).then((res) => setPersonsList(res ? res?.reverse() : null)) - getInfo(data?.access).then((res) => { - setInfo(res) - if (res) setMailing(res?.token_cap_enabled) - }) - }, [data?.access]) - - const addList = (newUser: ResponseGetPersons, list: ListType) => { - if (list === 'personal') { - if (personsList?.length !== 0) { - setPersonsList((prev) => [...prev!, newUser]) - } else { - setPersonsList([newUser]) - } - } - if (list === 'security') { - if (securityList?.length !== 0) { - setSecurityList((prev) => [...prev!, newUser]) - } else { - setSecurityList([newUser]) - } - } - } - - const download = () => { - axios.get(API_URL + `/auth/business-host/download-expenses?type=employees&from_date=${fromDate}&to_date=${toDate}`, { - responseType: 'arraybuffer', - headers: { Authorization: `Bearer ${data?.access}` }, - }).then((res) => { - const file = new Blob([res.data], { - type: 'application/ms-excel;charset=utf-8', - }) - const link = document.createElement('a') - link.href = window.URL.createObjectURL(file) - link.download = 'Expenses.xlsx' - document.body.appendChild(link) - link.click() - document.body.removeChild(link) - }) - } - - useEffect(() => { - axios.put(API_URL + '/auth/business-host', { token_cap_enabled: mailing }, { headers: { Authorization: `Bearer ${data?.access}` } }) - }, [mailing]) - - return ( - - - - {info?.is_ip_whitelist_enabled ? : <>} - - - - - - - Затраты - setShowPersons((prev) => !prev)} className={styles2.arrow} /> - - - {showPersons && ( - - - - - - )} - - - - - ) +const BusinessHost: React.FC = () => { + return } -const MailingBlock = ({ info, mailing }: { info: InfoBusiness | null | undefined; mailing: boolean | undefined }) => { - const [showPersons, setShowPersons] = useState(true) - const [newMail, setNewMail] = useState('') - const theme = useAppSelector((state) => state.theme.theme) - const [allMails, setAllMails] = useState() - const [isMailing, setIsMailing] = useState(false) - const { data } = useSession() - - useEffect(() => { - if (info) { - setAllMails(info?.token_cap_emails) - setIsMailing(info?.token_cap_enabled) - } - }, [info]) - - useEffect(() => { - if (allMails) { - req(allMails).then((res) => { - setNewMail('') - }) - } - }, [allMails]) - - const addMailingUser = async () => { - if (newMail !== '') { - setAllMails((emails) => [...(emails ?? []), newMail]) - } - } - - const req = async (mailingArr: string[]) => { - await axios.put(API_URL + '/auth/business-host', { token_cap_emails: mailingArr }, { headers: { Authorization: `Bearer ${data?.access}` } }) - } - - return ( - <> - {mailing && ( - - - - Уведомления о низком балансе - setShowPersons((prev) => !prev)} - className={styles2.arrow} - /> - - - {showPersons && ( - <> - - - setNewMail(e.target.value)} - fullWidth - sx={theme === 'light' ? { ...InputStyleLight } : { ...InputStyleDark }} - /> - - - - - - {allMails?.map((el) => ( -
-

{el}

- { - setAllMails(allMails.filter((item) => item !== el)) - }} - > - - - -
- ))} -
- - )} -
- )} - - ) -} +export default BusinessHost @@ -12,6 +12,10 @@ import { filter } from 'lodash' import Image from 'next/image' import { useSession } from 'next-auth/react' +import { getIpList } from '../../api/get-ipList' + +import styles from '#/widgets/business-models/ui/models-list/models-list.module.scss' + import { AddBusinessGroup } from '#/features/business-group' import { ChangeBusinessGroup } from '#/features/business-group/ui/change-business-group' import { ResponseGetPersons } from '#/features/invite-person-in-business' @@ -19,17 +23,14 @@ import { ResponseGetBusinessGroups } from '#/features/invite-person-in-business/ import { XMark } from '#/features/remove-person' import { Search } from '#/shared' import { API_URL } from '#/shared/lib/constants' -import styles from '#/widgets/business-models/ui/models-list/models-list.module.scss' import { getBusinessGroups } from '#/widgets/business-persons/api/get-businessGroups' import ArrowUpOrDown from '#/widgets/top-bar-model/ui/arrow-up-or-down' -import { getIpList } from '../../api/get-ipList' - export const BusinessGroups = ({ company_uid }: { company_uid?: string }) => { const [businessGroups, setBusinessGroups] = useState() - const [searchGroup, setSearchGroup] = useState( - businessGroups ? businessGroups : [] - ) + const [searchGroup, setSearchGroup] = useState< + ResponseGetBusinessGroups[] | undefined | null | undefined + >(businessGroups ? businessGroups : []) const [businessAddModal, setBusinessAddModal] = useState(false) const [showPersons, setShowPersons] = useState(true) const [search, setSearch] = useState('') @@ -138,10 +139,15 @@ export const BusinessGroups = ({ company_uid }: { company_uid?: string }) => { - + {group.title} { className={styles.tableLimit} align='right' > - Просмотр + + Просмотр + - + { deleteGroup(group.uid) @@ -185,7 +196,10 @@ export const BusinessGroups = ({ company_uid }: { company_uid?: string }) => { }, }} > - + Не найдено группы с таким именем @@ -1,6 +1,7 @@ -import { useGlobalSettings, UserSettingsContextProvider } from '#/entities/user-account' -import { useSession } from 'next-auth/react' import React, { PropsWithChildren, useEffect } from 'react' +import { useSession } from 'next-auth/react' + +import { useGlobalSettings, UserSettingsContextProvider } from '#/entities/user-account' interface ProvidersProps extends PropsWithChildren {} @@ -10,11 +11,10 @@ export const Providers = ({ children }: ProvidersProps) => { const { data } = useSession() useEffect(() => { - if(!data) return - console.log(settings.length) + if (!data) return if (settings.length > 0) return fetchUserSettings() - }, [data]) + }, [data, settings.length, fetchUserSettings]) return (