3.8. Профиль пользователя
Модуль "Профиль пользователя" в системе HRoom предоставляет HR-менеджерам возможность управлять своими персональными данными, настройками аккаунта, информацией о компании, а также настраивать платежные данные. Этот модуль является важной частью системы, обеспечивающей персонализацию взаимодействия пользователя с платформой и управление административными данными.
Основные компоненты модуля
- Личная информация - управление персональными данными HR-менеджера
- Информация о компании - просмотр и редактирование данных компании
- Платежная информация - управление платежными данными компании
- История платежей - просмотр истории финансовых операций
- Настройки - конфигурация рабочего пространства и системных параметров
- Загрузка аватара - обновление фотографии профиля
Архитектура модуля профиля пользователя
Модуль профиля пользователя организован по следующей структуре:
src/pages/Profile/
├── CompanyInfo.tsx # Компонент информации о компании
├── PaymentHistory.tsx # Компонент истории платежей
├── PaymentInfo.tsx # Компонент платежной информации
├── PersonalInfo.tsx # Компонент личной информации
├── Settings.tsx # Компонент настроек пользователя
└── index.tsx # Главный компонент страницы профиля
src/components/
├── UserProfile.tsx # Компонент для отображения краткой информации о пользователе
└── AvatarUpload.tsx # Компонент для загрузки и обновления аватара
Основной компонент профиля (Profile/index.tsx)
Главный компонент модуля профиля объединяет все подкомпоненты и обеспечивает навигацию между различными разделами профиля. Он реализован как страница с табами (вкладками), где каждая вкладка представляет собой отдельный раздел профиля.
Код основного компонента профиля
import React, { useState } from 'react';
import { Tabs } from '../../components/ui/Tabs';
import PersonalInfo from './PersonalInfo';
import CompanyInfo from './CompanyInfo';
import PaymentInfo from './PaymentInfo';
import PaymentHistory from './PaymentHistory';
import Settings from './Settings';
import Layout from '../../components/Layout';
const Profile: React.FC = () => {
const [activeTab, setActiveTab] = useState('personal');
const tabs = [
{ id: 'personal', label: 'Личная информация' },
{ id: 'company', label: 'Информация о компании' },
{ id: 'payment', label: 'Платежная информация' },
{ id: 'history', label: 'История платежей' },
{ id: 'settings', label: 'Настройки' },
];
return (
<Layout>
<div className="container mx-auto py-6">
<h1 className="text-2xl font-semibold mb-6">Профиль пользователя</h1>
<Tabs
tabs={tabs}
activeTab={activeTab}
onChange={setActiveTab}
className="bg-white dark:bg-gray-800 rounded-lg shadow-md overflow-hidden"
/>
<div className="mt-6 bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
{activeTab === 'personal' && <PersonalInfo />}
{activeTab === 'company' && <CompanyInfo />}
{activeTab === 'payment' && <PaymentInfo />}
{activeTab === 'history' && <PaymentHistory />}
{activeTab === 'settings' && <Settings />}
</div>
</div>
</Layout>
);
};
export default Profile;
Основной компонент профиля выполняет следующие функции:
- Организует интерфейс профиля пользователя в виде вкладок для удобной навигации
- Управляет состоянием активной вкладки
- Отображает соответствующий компонент в зависимости от выбранной вкладки
- Интегрируется с общим Layout приложения для обеспечения единообразного интерфейса
Личная информация (PersonalInfo.tsx)
Раздел "Личная информация" позволяет HR-менеджеру просматривать и редактировать свои персональные данные, включая ФИО, контактную информацию и аватар профиля.
Функциональность компонента PersonalInfo
Компонент личной информации выполняет следующие функции:
- Загрузка текущих данных профиля пользователя из API
- Предоставление интерфейса для редактирования личных данных
- Валидация вводимых пользователем данных
- Сохранение обновленных данных через API
- Интеграция с компонентом загрузки аватара
- Отображение сообщений об успешном сохранении или ошибках
Код компонента личной информации
import React, { useState, useEffect } from 'react';
import AvatarUpload from '../../components/AvatarUpload';
import Button from '../../components/ui/Button';
import Input from '../../components/ui/Input';
const PersonalInfo: React.FC = () => {
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [success, setSuccess] = useState(false);
const [error, setError] = useState<string | null>(null);
const [profileData, setProfileData] = useState({
firstName: '',
lastName: '',
middleName: '',
email: '',
phone: '',
photoUrl: '',
managerId: ''
});
// Загрузка данных профиля
useEffect(() => {
const fetchProfileData = async () => {
setLoading(true);
try {
const response = await fetch('https://api.hroom.ai/webhook/profile/personal/get', {
method: 'POST',
headers: {
'Authorization': `Bearer ${localStorage.getItem('manager_token')}`,
'Content-Type': 'application/json'
}
});
const data = await response.json();
if (data && data[0]) {
setProfileData({
firstName: data[0].firstName || '',
lastName: data[0].lastName || '',
middleName: data[0].middleName || '',
email: data[0].email || '',
phone: data[0].phone || '',
photoUrl: data[0].photoUrl || '',
managerId: data[0].managerId || ''
});
}
} catch (err) {
setError('Ошибка при загрузке данных профиля');
console.error('Error fetching profile data:', err);
} finally {
setLoading(false);
}
};
fetchProfileData();
}, []);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setProfileData(prev => ({
...prev,
[name]: value
}));
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setSaving(true);
setError(null);
setSuccess(false);
try {
const response = await fetch('https://api.hroom.ai/webhook/profile/personal/save', {
method: 'POST',
headers: {
'Authorization': `Bearer ${localStorage.getItem('manager_token')}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
managerId: profileData.managerId,
firstName: profileData.firstName,
lastName: profileData.lastName,
middleName: profileData.middleName,
phone: profileData.phone
})
});
const data = await response.json();
if (data && data[0] && data[0].success) {
setSuccess(true);
} else {
setError('Не удалось сохранить изменения');
}
} catch (err) {
setError('Ошибка при сохранении данных');
console.error('Error saving profile data:', err);
} finally {
setSaving(false);
// Скрываем сообщение об успехе через 3 секунды
if (success) {
setTimeout(() => {
setSuccess(false);
}, 3000);
}
}
};
const handleAvatarUpload = (url: string) => {
setProfileData(prev => ({
...prev,
photoUrl: url
}));
};
if (loading) {
return (
<div className="flex justify-center items-center p-8">
<div className="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-primary"></div>
</div>
);
}
return (
<div>
<h2 className="text-xl font-medium mb-6">Личная информация</h2>
<div className="flex flex-col md:flex-row gap-8">
<div className="w-full md:w-1/3">
<AvatarUpload
currentUrl={profileData.photoUrl}
onUploadSuccess={handleAvatarUpload}
/>
</div>
<div className="w-full md:w-2/3">
<form onSubmit={handleSubmit} className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Input
label="Имя"
name="firstName"
value={profileData.firstName}
onChange={handleChange}
required
/>
<Input
label="Фамилия"
name="lastName"
value={profileData.lastName}
onChange={handleChange}
required
/>
</div>
<Input
label="Отчество"
name="middleName"
value={profileData.middleName}
onChange={handleChange}
/>
<Input
label="Email"
type="email"
name="email"
value={profileData.email}
disabled
helpText="Email нельзя изменить"
/>
<Input
label="Телефон"
type="tel"
name="phone"
value={profileData.phone}
onChange={handleChange}
placeholder="+7 (XXX) XXX-XXXX"
/>
{error && (
<div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
{error}
</div>
)}
{success && (
<div className="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded">
Данные успешно сохранены
</div>
)}
<div className="flex justify-end">
<Button
type="submit"
isLoading={saving}
disabled={saving}
>
Сохранить изменения
</Button>
</div>
</form>
</div>
</div>
</div>
);
};
API для работы с личной информацией
Для работы с личной информацией пользователя используются следующие API-эндпоинты:
/profile/personal/get- получение текущих данных профиля/profile/personal/save- сохранение обновленных данных профиля/profile/file/upload- загрузка аватара пользователя
Все запросы к этим эндпоинтам проходят аутентификацию с использованием manager_token, который хранится в localStorage.
Информация о компании (CompanyInfo.tsx)
Раздел "Информация о компании" позволяет просматривать и редактировать данные компании HR-менеджера, включая название компании, веб-сайт и другие реквизиты.
Функциональность компонента CompanyInfo
Компонент информации о компании выполняет следующие функции:
- Загрузка текущих данных компании из API
- Предоставление интерфейса для редактирования информации о компании
- Валидация вводимых данных
- Сохранение обновленных данных через API
- Отображение статуса подписки и срока действия аккаунта
Код компонента информации о компании
import React, { useState, useEffect } from 'react';
import Button from '../../components/ui/Button';
import Input from '../../components/ui/Input';
const CompanyInfo: React.FC = () => {
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [success, setSuccess] = useState(false);
const [error, setError] = useState<string | null>(null);
const [companyData, setCompanyData] = useState({
companyId: '',
name: '',
website: '',
accountType: '',
expirationDate: '',
});
// Загрузка данных компании
useEffect(() => {
const fetchCompanyData = async () => {
setLoading(true);
try {
const response = await fetch('https://api.hroom.ai/webhook/profile/company/get', {
method: 'POST',
headers: {
'Authorization': `Bearer ${localStorage.getItem('company_token')}`,
'Content-Type': 'application/json'
}
});
const data = await response.json();
if (data && data[0]) {
setCompanyData({
companyId: data[0].companyId || '',
name: data[0].name || '',
website: data[0].website || '',
accountType: data[0].accountType || '',
expirationDate: data[0].expirationDate || '',
});
}
} catch (err) {
setError('Ошибка при загрузке данных компании');
console.error('Error fetching company data:', err);
} finally {
setLoading(false);
}
};
fetchCompanyData();
}, []);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setCompanyData(prev => ({
...prev,
[name]: value
}));
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setSaving(true);
setError(null);
setSuccess(false);
try {
const response = await fetch('https://api.hroom.ai/webhook/profile/company/save', {
method: 'POST',
headers: {
'Authorization': `Bearer ${localStorage.getItem('manager_token')}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
companyId: companyData.companyId,
name: companyData.name,
website: companyData.website
})
});
const data = await response.json();
if (data && data[0] && data[0].success) {
setSuccess(true);
} else {
setError('Не удалось сохранить изменения');
}
} catch (err) {
setError('Ошибка при сохранении данных');
console.error('Error saving company data:', err);
} finally {
setSaving(false);
// Скрываем сообщение об успехе через 3 секунды
if (success) {
setTimeout(() => {
setSuccess(false);
}, 3000);
}
}
};
// Форматирование даты истечения подписки
const formattedExpirationDate = companyData.expirationDate
? new Date(companyData.expirationDate).toLocaleDateString('ru-RU', {
day: 'numeric',
month: 'long',
year: 'numeric'
})
: 'Не указано';
// Определение текста и цвета для статуса аккаунта
const getAccountTypeInfo = () => {
switch (companyData.accountType) {
case 'free':
return { text: 'Бесплатный', color: 'text-yellow-500' };
case 'paid':
return { text: 'Оплаченный', color: 'text-green-500' };
case 'trial':
return { text: 'Пробный период', color: 'text-blue-500' };
default:
return { text: 'Не определен', color: 'text-gray-500' };
}
};
const accountTypeInfo = getAccountTypeInfo();
if (loading) {
return (
<div className="flex justify-center items-center p-8">
<div className="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-primary"></div>
</div>
);
}
return (
<div>
<h2 className="text-xl font-medium mb-6">Информация о компании</h2>
<div className="mb-8 p-4 bg-gray-50 dark:bg-gray-800 rounded-lg">
<h3 className="text-lg font-medium mb-2">Статус аккаунта</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<p className="text-sm text-gray-500 dark:text-gray-400">Тип аккаунта</p>
<p className={`font-medium ${accountTypeInfo.color}`}>{accountTypeInfo.text}</p>
</div>
<div>
<p className="text-sm text-gray-500 dark:text-gray-400">Действует до</p>
<p className="font-medium">{formattedExpirationDate}</p>
</div>
</div>
</div>
<form onSubmit={handleSubmit} className="space-y-4">
<Input
label="Название компании"
name="name"
value={companyData.name}
onChange={handleChange}
required
/>
<Input
label="Веб-сайт"
name="website"
value={companyData.website}
onChange={handleChange}
placeholder="https://example.com"
/>
{error && (
<div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
{error}
</div>
)}
{success && (
<div className="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded">
Данные успешно сохранены
</div>
)}
<div className="flex justify-end">
<Button
type="submit"
isLoading={saving}
disabled={saving}
>
Сохранить изменения
</Button>
</div>
</form>
</div>
);
};
API для работы с информацией о компании
Для работы с информацией о компании используются следующие API-эндпоинты:
/profile/company/get- получение текущих данных компании (требуетсяcompany_token)/profile/company/save- сохранение обновленных данных компании (требуетсяmanager_token)
Платежная информация (PaymentInfo.tsx)
Раздел "Платежная информация" позволяет HR-менеджеру управлять платежными данными компании, такими как юридический адрес, банковские реквизиты и данные для выставления счетов.
Функциональность компонента PaymentInfo
Компонент платежной информации выполняет следующие функции:
- Загрузка текущих платежных данных компании из API
- Предоставление формы для заполнения/редактирования платежной информации
- Валидация вводимых платежных данных
- Сохранение обновленных платежных данных через API
Код компонента платежной информации
import React, { useState, useEffect } from 'react';
import Button from '../../components/ui/Button';
import Input from '../../components/ui/Input';
import Textarea from '../../components/ui/Textarea';
const PaymentInfo: React.FC = () => {
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [success, setSuccess] = useState(false);
const [error, setError] = useState<string | null>(null);
const [billingInfo, setBillingInfo] = useState({
companyId: '',
legalAddress: '',
actualAddress: '',
inn: '',
kpp: '',
ogrn: '',
bankAccount: '',
corrAccount: '',
bik: '',
bankName: ''
});
// Загрузка платежных данных
useEffect(() => {
const fetchBillingInfo = async () => {
setLoading(true);
try {
const response = await fetch('https://api.hroom.ai/webhook/profile/company/billing/get', {
method: 'POST',
headers: {
'Authorization': `Bearer ${localStorage.getItem('company_token')}`,
'Content-Type': 'application/json'
}
});
const data = await response.json();
if (data && data[0]) {
const billing = data[0].billing_info || {};
setBillingInfo({
companyId: data[0].companyId || '',
legalAddress: billing.legalAddress || '',
actualAddress: billing.actualAddress || '',
inn: billing.inn || '',
kpp: billing.kpp || '',
ogrn: billing.ogrn || '',
bankAccount: billing.bankAccount || '',
corrAccount: billing.corrAccount || '',
bik: billing.bik || '',
bankName: billing.bankName || ''
});
}
} catch (err) {
setError('Ошибка при загрузке платежных данных');
console.error('Error fetching billing info:', err);
} finally {
setLoading(false);
}
};
fetchBillingInfo();
}, []);
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { name, value } = e.target;
setBillingInfo(prev => ({
...prev,
[name]: value
}));
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setSaving(true);
setError(null);
setSuccess(false);
try {
const response = await fetch('https://api.hroom.ai/webhook/profile/company/billing/save', {
method: 'POST',
headers: {
'Authorization': `Bearer ${localStorage.getItem('company_token')}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
billingInfo: {
legalAddress: billingInfo.legalAddress,
actualAddress: billingInfo.actualAddress,
inn: billingInfo.inn,
kpp: billingInfo.kpp,
ogrn: billingInfo.ogrn,
bankAccount: billingInfo.bankAccount,
corrAccount: billingInfo.corrAccount,
bik: billingInfo.bik,
bankName: billingInfo.bankName
}
})
});
const data = await response.json();
if (data && data[0] && data[0].success) {
setSuccess(true);
} else {
setError('Не удалось сохранить изменения');
}
} catch (err) {
setError('Ошибка при сохранении данных');
console.error('Error saving billing info:', err);
} finally {
setSaving(false);
// Скрываем сообщение об успехе через 3 секунды
if (success) {
setTimeout(() => {
setSuccess(false);
}, 3000);
}
}
};
if (loading) {
return (
<div className="flex justify-center items-center p-8">
<div className="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-primary"></div>
</div>
);
}
return (
<div>
<h2 className="text-xl font-medium mb-6">Платежная информация</h2>
<form onSubmit={handleSubmit} className="space-y-6">
<div>
<h3 className="text-lg font-medium mb-4">Юридические данные</h3>
<div className="space-y-4">
<Textarea
label="Юридический адрес"
name="legalAddress"
value={billingInfo.legalAddress}
onChange={handleChange}
rows={2}
/>
<Textarea
label="Фактический адрес"
name="actualAddress"
value={billingInfo.actualAddress}
onChange={handleChange}
rows={2}
/>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<Input
label="ИНН"
name="inn"
value={billingInfo.inn}
onChange={handleChange}
/>
<Input
label="КПП"
name="kpp"
value={billingInfo.kpp}
onChange={handleChange}
/>
<Input
label="ОГРН"
name="ogrn"
value={billingInfo.ogrn}
onChange={handleChange}
/>
</div>
</div>
</div>
<div>
<h3 className="text-lg font-medium mb-4">Банковские реквизиты</h3>
<div className="space-y-4">
<Input
label="Наименование банка"
name="bankName"
value={billingInfo.bankName}
onChange={handleChange}
/>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Input
label="Расчетный счет"
name="bankAccount"
value={billingInfo.bankAccount}
onChange={handleChange}
/>
<Input
label="Корреспондентский счет"
name="corrAccount"
value={billingInfo.corrAccount}
onChange={handleChange}
/>
</div>
<Input
label="БИК"
name="bik"
value={billingInfo.bik}
onChange={handleChange}
/>
</div>
</div>
{error && (
<div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
{error}
</div>
)}
{success && (
<div className="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded">
Платежная информация успешно сохранена
</div>
)}
<div className="flex justify-end">
<Button
type="submit"
isLoading={saving}
disabled={saving}
>
Сохранить платежную информацию
</Button>
</div>
</form>
</div>
);
};
API для работы с платежной информацией
Для работы с платежной информацией используются следующие API-эндпоинты:
/profile/company/billing/get- получение текущих платежных данных компании/profile/company/billing/save- сохранение обновленных платежных данных
Оба запроса требуют аутентификации с использованием company_token.
История платежей (PaymentHistory.tsx)
Раздел "История платежей" предоставляет информацию о выставленных счетах, совершенных платежах и истории транзакций по аккаунту компании.
Функциональность компонента PaymentHistory
Компонент истории платежей выполняет следующие функции:
- Загрузка истории платежей из API
- Отображение списка платежей с информацией о дате, сумме, статусе и назначении
- Фильтрация платежей по периоду или статусу
- Просмотр деталей платежа
- Скачивание документов (счетов, актов)
Код компонента истории платежей
import React, { useState, useEffect } from 'react';
import Button from '../../components/ui/Button';
import { Select } from '../../components/ui/Select';
import Pagination from '../../components/ui/Pagination';
// Пример данных для истории платежей
const mockPaymentHistory = [
{
id: '1',
invoiceNumber: 'INV-2024-001',
date: '2024-03-15T10:00:00.000Z',
amount: 12000,
status: 'paid',
description: 'Оплата подписки на 3 месяца',
documents: [
{ type: 'invoice', url: '#', name: 'Счет INV-2024-001.pdf' },
{ type: 'act', url: '#', name: 'Акт от 15.03.2024.pdf' },
]
},
{
id: '2',
invoiceNumber: 'INV-2023-012',
date: '2023-12-20T14:30:00.000Z',
amount: 24000,
status: 'paid',
description: 'Оплата подписки на 6 месяцев',
documents: [
{ type: 'invoice', url: '#', name: 'Счет INV-2023-012.pdf' },
{ type: 'act', url: '#', name: 'Акт от 20.12.2023.pdf' },
]
},
{
id: '3',
invoiceNumber: 'INV-2024-002',
date: '2024-03-25T09:15:00.000Z',
amount: 5000,
status: 'pending',
description: 'Дополнительные услуги',
documents: [
{ type: 'invoice', url: '#', name: 'Счет INV-2024-002.pdf' },
]
}
];
const PaymentHistory: React.FC = () => {
const [loading, setLoading] = useState(true);
const [payments, setPayments] = useState([]);
const [filteredPayments, setFilteredPayments] = useState([]);
const [currentPage, setCurrentPage] = useState(1);
const [statusFilter, setStatusFilter] = useState('all');
const [periodFilter, setPeriodFilter] = useState('all');
const itemsPerPage = 10;
// Загрузка истории платежей
useEffect(() => {
// Имитация загрузки данных из API
const fetchPaymentHistory = async () => {
setLoading(true);
try {
// В реальном приложении здесь был бы запрос к API
// const response = await fetch('https://api.hroom.ai/webhook/payments/history', {
// method: 'GET',
// headers: {
// 'Authorization': `Bearer ${localStorage.getItem('company_token')}`,
// 'Content-Type': 'application/json'
// }
// });
// const data = await response.json();
// Используем mock-данные для демонстрации
setTimeout(() => {
setPayments(mockPaymentHistory);
setFilteredPayments(mockPaymentHistory);
setLoading(false);
}, 1000);
} catch (err) {
console.error('Error fetching payment history:', err);
setLoading(false);
}
};
fetchPaymentHistory();
}, []);
// Применение фильтров
useEffect(() => {
let filtered = [...payments];
// Фильтр по статусу
if (statusFilter !== 'all') {
filtered = filtered.filter(payment => payment.status === statusFilter);
}
// Фильтр по периоду
const now = new Date();
if (periodFilter === '30days') {
const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
filtered = filtered.filter(payment => new Date(payment.date) >= thirtyDaysAgo);
} else if (periodFilter === '90days') {
const ninetyDaysAgo = new Date(now.getTime() - 90 * 24 * 60 * 60 * 1000);
filtered = filtered.filter(payment => new Date(payment.date) >= ninetyDaysAgo);
} else if (periodFilter === 'year') {
const oneYearAgo = new Date(now.getFullYear() - 1, now.getMonth(), now.getDate());
filtered = filtered.filter(payment => new Date(payment.date) >= oneYearAgo);
}
setFilteredPayments(filtered);
setCurrentPage(1); // Сбрасываем на первую страницу при изменении фильтров
}, [statusFilter, periodFilter, payments]);
// Пагинация
const totalPages = Math.ceil(filteredPayments.length / itemsPerPage);
const currentPayments = filteredPayments.slice(
(currentPage - 1) * itemsPerPage,
currentPage * itemsPerPage
);
// Форматирование даты
const formatDate = (dateString) => {
return new Date(dateString).toLocaleDateString('ru-RU', {
day: 'numeric',
month: 'long',
year: 'numeric'
});
};
// Форматирование суммы
const formatAmount = (amount) => {
return new Intl.NumberFormat('ru-RU', {
style: 'currency',
currency: 'RUB',
minimumFractionDigits: 0
}).format(amount);
};
// Получение статуса платежа
const getPaymentStatus = (status) => {
switch (status) {
case 'paid':
return { text: 'Оплачен', color: 'bg-green-100 text-green-800' };
case 'pending':
return { text: 'Ожидает оплаты', color: 'bg-yellow-100 text-yellow-800' };
case 'canceled':
return { text: 'Отменен', color: 'bg-red-100 text-red-800' };
default:
return { text: 'Неизвестно', color: 'bg-gray-100 text-gray-800' };
}
};
if (loading) {
return (
<div className="flex justify-center items-center p-8">
<div className="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-primary"></div>
</div>
);
}
return (
<div>
<h2 className="text-xl font-medium mb-6">История платежей</h2>
<div className="flex flex-col md:flex-row gap-4 mb-6">
<div className="w-full md:w-1/2">
<Select
label="Статус платежа"
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
options={[
{ value: 'all', label: 'Все статусы' },
{ value: 'paid', label: 'Оплачены' },
{ value: 'pending', label: 'Ожидают оплаты' },
{ value: 'canceled', label: 'Отменены' }
]}
/>
</div>
<div className="w-full md:w-1/2">
<Select
label="Период"
value={periodFilter}
onChange={(e) => setPeriodFilter(e.target.value)}
options={[
{ value: 'all', label: 'За все время' },
{ value: '30days', label: 'За последние 30 дней' },
{ value: '90days', label: 'За последние 90 дней' },
{ value: 'year', label: 'За последний год' }
]}
/>
</div>
</div>
{filteredPayments.length === 0 ? (
<div className="text-center py-8 bg-gray-50 dark:bg-gray-800 rounded-lg">
<p className="text-gray-500 dark:text-gray-400">Платежи не найдены</p>
</div>
) : (
<>
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead className="bg-gray-50 dark:bg-gray-800">
<tr>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
№ счета
</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Дата
</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Описание
</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Сумма
</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Статус
</th>
<th scope="col" className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Документы
</th>
</tr>
</thead>
<tbody className="bg-white dark:bg-gray-900 divide-y divide-gray-200 dark:divide-gray-800">
{currentPayments.map((payment) => {
const status = getPaymentStatus(payment.status);
return (
<tr key={payment.id}>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-white">
{payment.invoiceNumber}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
{formatDate(payment.date)}
</td>
<td className="px-6 py-4 text-sm text-gray-500 dark:text-gray-400">
{payment.description}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-white">
{formatAmount(payment.amount)}
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${status.color}`}>
{status.text}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<div className="flex justify-end space-x-2">
{payment.documents.map((doc, index) => (
<a
key={index}
href={doc.url}
className="text-primary hover:text-primary-light"
download={doc.name}
>
{doc.type === 'invoice' ? 'Счет' : 'Акт'}
</a>
))}
</div>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
{totalPages > 1 && (
<div className="mt-4">
<Pagination
currentPage={currentPage}
totalPages={totalPages}
onPageChange={setCurrentPage}
/>
</div>
)}
</>
)}
</div>
);
};
В текущей реализации компонент PaymentHistory использует мокированные данные для демонстрации функциональности, поскольку в доступных API-эндпоинтах не предусмотрена работа с историей платежей. В реальном приложении этот компонент будет подключен к соответствующему API для получения реальных данных о платежах.
Настройки (Settings.tsx)
Раздел "Настройки" позволяет пользователю настраивать различные параметры приложения, включая уведомления, язык интерфейса и другие пользовательские предпочтения.
Функциональность компонента Settings
Компонент настроек выполняет следующие функции:
- Загрузка текущих настроек пользователя
- Настройка параметров уведомлений (email, push-уведомления)
- Выбор языка интерфейса
- Управление предпочтениями отображения
- Настройки конфиденциальности
- Сохранение обновленных настроек
Код компонента настроек
import React, { useState } from 'react';
import Button from '../../components/ui/Button';
import { Select } from '../../components/ui/Select';
import Checkbox from '../../components/ui/Checkbox';
const Settings: React.FC = () => {
const [saving, setSaving] = useState(false);
const [success, setSuccess] = useState(false);
const [error, setError] = useState<string | null>(null);
// Настройки уведомлений
const [notificationSettings, setNotificationSettings] = useState({
emailNotifications: true,
surveyCreated: true,
surveyCompleted: true,
newComments: true,
weeklyReports: true,
marketingEmails: false
});
// Настройки языка и отображения
const [displaySettings, setDisplaySettings] = useState({
language: 'ru',
darkMode: false,
dashboardLayout: 'standard'
});
const handleNotificationChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, checked } = e.target;
setNotificationSettings(prev => ({
...prev,
[name]: checked
}));
};
const handleDisplayChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const { name, value } = e.target;
setDisplaySettings(prev => ({
...prev,
[name]: value
}));
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setSaving(true);
setError(null);
setSuccess(false);
try {
// В реальном приложении здесь был бы запрос к API
// const response = await fetch('https://api.hroom.ai/webhook/settings/save', {
// method: 'POST',
// headers: {
// 'Authorization': `Bearer ${localStorage.getItem('manager_token')}`,
// 'Content-Type': 'application/json'
// },
// body: JSON.stringify({
// settings: {
// notifications: notificationSettings,
// display: displaySettings
// }
// })
// });
// const data = await response.json();
// Имитируем успешное сохранение
await new Promise(resolve => setTimeout(resolve, 1000));
setSuccess(true);
// Если настройка темного режима изменилась, применяем её
const htmlElement = document.documentElement;
if (displaySettings.darkMode) {
htmlElement.setAttribute('data-theme', 'dark');
htmlElement.classList.add('dark');
} else {
htmlElement.setAttribute('data-theme', 'light');
htmlElement.classList.remove('dark');
}
// Сохраняем настройку темы в localStorage
localStorage.setItem('theme', displaySettings.darkMode ? 'dark' : 'light');
} catch (err) {
setError('Ошибка при сохранении настроек');
console.error('Error saving settings:', err);
} finally {
setSaving(false);
// Скрываем сообщение об успехе через 3 секунды
if (success) {
setTimeout(() => {
setSuccess(false);
}, 3000);
}
}
};
// Опции для выпадающих списков
const languageOptions = [
{ value: 'ru', label: 'Русский' },
{ value: 'en', label: 'English' }
];
const layoutOptions = [
{ value: 'standard', label: 'Стандартный' },
{ value: 'compact', label: 'Компактный' },
{ value: 'expanded', label: 'Расширенный' }
];
return (
<div>
<h2 className="text-xl font-medium mb-6">Настройки</h2>
<form onSubmit={handleSubmit} className="space-y-8">
<div>
<h3 className="text-lg font-medium mb-4">Уведомления</h3>
<div className="space-y-4">
<Checkbox
label="Получать уведомления по email"
name="emailNotifications"
checked={notificationSettings.emailNotifications}
onChange={handleNotificationChange}
/>
<div className="ml-6 space-y-3">
<Checkbox
label="Уведомления о создании опроса"
name="surveyCreated"
checked={notificationSettings.surveyCreated}
onChange={handleNotificationChange}
disabled={!notificationSettings.emailNotifications}
/>
<Checkbox
label="Уведомления о завершении опроса"
name="surveyCompleted"
checked={notificationSettings.surveyCompleted}
onChange={handleNotificationChange}
disabled={!notificationSettings.emailNotifications}
/>
<Checkbox
label="Уведомления о новых комментариях"
name="newComments"
checked={notificationSettings.newComments}
onChange={handleNotificationChange}
disabled={!notificationSettings.emailNotifications}
/>
<Checkbox
label="Еженедельные отчеты"
name="weeklyReports"
checked={notificationSettings.weeklyReports}
onChange={handleNotificationChange}
disabled={!notificationSettings.emailNotifications}
/>
</div>
<Checkbox
label="Получать маркетинговые рассылки"
name="marketingEmails"
checked={notificationSettings.marketingEmails}
onChange={handleNotificationChange}
/>
</div>
</div>
<div>
<h3 className="text-lg font-medium mb-4">Язык и отображение</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Select
label="Язык интерфейса"
name="language"
value={displaySettings.language}
onChange={handleDisplayChange}
options={languageOptions}
/>
<Select
label="Макет дашборда"
name="dashboardLayout"
value={displaySettings.dashboardLayout}
onChange={handleDisplayChange}
options={layoutOptions}
/>
</div>
<div className="mt-4">
<Checkbox
label="Использовать темную тему"
name="darkMode"
checked={displaySettings.darkMode}
onChange={(e) => setDisplaySettings(prev => ({
...prev,
darkMode: e.target.checked
}))}
/>
</div>
</div>
{error && (
<div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
{error}
</div>
)}
{success && (
<div className="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded">
Настройки успешно сохранены
</div>
)}
<div className="flex justify-end">
<Button
type="submit"
isLoading={saving}
disabled={saving}
>
Сохранить настройки
</Button>
</div>
</form>
</div>
);
};
Компонент Settings реализует пользовательские настройки, которые сохраняются в localStorage браузера. В реальном приложении эти настройки также могут сохраняться на сервере через соответствующий API-эндпоинт.
Компонент загрузки аватара (AvatarUpload)
Компонент AvatarUpload предоставляет возможность загрузки и обновления аватара пользователя. Он используется в разделе личной информации для визуальной персонализации профиля.
Функциональность компонента AvatarUpload
Компонент загрузки аватара выполняет следующие функции:
- Отображение текущего аватара пользователя
- Загрузка нового изображения с устройства пользователя
- Валидация типа и размера файла изображения
- Отправка изображения на сервер с помощью API
- Обработка успешной загрузки или ошибок
- Обновление отображаемого аватара после загрузки
Код компонента загрузки аватара
import React, { useState, useRef } from 'react';
import Button from './ui/Button';
interface AvatarUploadProps {
currentUrl?: string;
onUploadSuccess: (url: string) => void;
}
const AvatarUpload: React.FC<AvatarUploadProps> = ({
currentUrl,
onUploadSuccess
}) => {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
// Обработчик клика по кнопке загрузки
const handleUploadClick = () => {
if (fileInputRef.current) {
fileInputRef.current.click();
}
};
// Обработчик загрузки файла
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
// Валидация типа файла
const validTypes = ['image/jpeg', 'image/png', 'image/gif'];
if (!validTypes.includes(file.type)) {
setError('Пожалуйста, загрузите изображение в формате JPEG, PNG или GIF');
return;
}
// Валидация размера файла (не более 5 МБ)
if (file.size > 5 * 1024 * 1024) {
setError('Размер файла не должен превышать 5 МБ');
return;
}
setLoading(true);
setError(null);
try {
// Создаем FormData для отправки файла
const formData = new FormData();
formData.append('file', file);
// Отправляем файл на сервер
const response = await fetch('https://api.hroom.ai/webhook/profile/file/upload', {
method: 'POST',
headers: {
'Authorization': `Bearer ${localStorage.getItem('manager_token')}`
},
body: formData
});
const data = await response.json();
if (data && data.success && data.url) {
onUploadSuccess(data.url);
} else {
setError('Не удалось загрузить изображение');
}
} catch (err) {
setError('Ошибка при загрузке изображения');
console.error('Error uploading avatar:', err);
} finally {
setLoading(false);
// Сбрасываем input, чтобы можно было загрузить тот же файл еще раз
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
}
};
// Базовый аватар, если URL не предоставлен
const getInitials = () => {
return 'HR';
};
return (
<div className="flex flex-col items-center">
<div className="mb-4 relative">
{currentUrl ? (
<img
src={currentUrl}
alt="Avatar"
className="w-32 h-32 rounded-full object-cover border-2 border-primary"
/>
) : (
<div className="w-32 h-32 rounded-full bg-primary-light flex items-center justify-center text-white text-2xl font-semibold">
{getInitials()}
</div>
)}
{loading && (
<div className="absolute inset-0 flex items-center justify-center bg-black bg-opacity-50 rounded-full">
<div className="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-white"></div>
</div>
)}
</div>
<input
type="file"
ref={fileInputRef}
onChange={handleFileChange}
accept="image/jpeg,image/png,image/gif"
className="hidden"
/>
<Button
onClick={handleUploadClick}
disabled={loading}
variant="outline"
size="sm"
>
{currentUrl ? 'Изменить фото' : 'Загрузить фото'}
</Button>
{error && (
<p className="mt-2 text-sm text-red-600">{error}</p>
)}
<p className="mt-2 text-xs text-gray-500 dark:text-gray-400 text-center">
Загружайте изображения в формате JPG, PNG или GIF размером не более 5 МБ
</p>
</div>
);
};
export default AvatarUpload;
API для загрузки аватара
Для загрузки аватара используется API-эндпоинт /profile/file/upload, который принимает файл изображения в формате multipart/form-data и возвращает URL загруженного аватара. Запрос требует аутентификации с использованием manager_token.
Интеграция модуля профиля с другими компонентами
Модуль профиля пользователя интегрируется с другими компонентами системы HRoom для обеспечения целостного пользовательского опыта:
Интеграция с компонентом UserProfile
Компонент UserProfile используется в различных частях приложения для отображения краткой информации о текущем пользователе, включая его имя, фото и роль. Данные для этого компонента берутся из тех же API-эндпоинтов, что и для модуля профиля.
Интеграция с системой аутентификации
Модуль профиля тесно интегрирован с системой аутентификации HRoom. Каждый запрос к API, используемый в модуле профиля, требует аутентификации с помощью токена (manager_token или company_token), который хранится в localStorage после успешного входа пользователя в систему.
Интеграция с модулем администрирования
Для пользователей с административными правами модуль профиля предоставляет дополнительные возможности, такие как управление платежной информацией компании и другие административные функции.
Схема API-запросов в модуле профиля
Ниже представлена схема API-запросов, используемых в модуле профиля пользователя:
| Эндпоинт | Метод | Описание | Токен | Тело запроса |
|---|---|---|---|---|
/profile/personal/get |
POST | Получение личных данных | manager_token | - |
/profile/personal/save |
POST | Сохранение личных данных | manager_token | managerId, firstName, lastName, middleName, phone |
/profile/company/get |
POST | Получение данных компании | company_token | - |
/profile/company/save |
POST | Сохранение данных компании | manager_token | companyId, name, website |
/profile/company/billing/get |
POST | Получение платежных данных | company_token | - |
/profile/company/billing/save |
POST | Сохранение платежных данных | company_token | billingInfo: { legalAddress, actualAddress, inn, kpp, ogrn, bankAccount, corrAccount, bik, bankName } |
/profile/file/upload |
POST | Загрузка аватара | manager_token | file (formData) |
Заключение
Модуль профиля пользователя в системе HRoom предоставляет комплексный набор функций для управления персональными данными HR-менеджера, информацией о компании и другими настройками. Он интегрирован с системой аутентификации и другими модулями приложения, обеспечивая единый пользовательский опыт.
Основные преимущества модуля профиля:
- Интуитивный интерфейс с разделением на тематические вкладки
- Гибкая система управления персональными данными и настройками
- Возможность загрузки аватара для персонализации аккаунта
- Управление платежной информацией компании
- Настройка уведомлений и предпочтений интерфейса
Компоненты модуля профиля реализованы с использованием React и TypeScript, что обеспечивает типобезопасность и простоту поддержки кода. Модуль взаимодействует с API с помощью Fetch API, используя токены аутентификации (manager_token и company_token) для защиты запросов.