add user information modal window

This commit is contained in:
smanylov
2026-05-13 14:25:47 +07:00
parent f1954f5f48
commit 9d40df3273
7 changed files with 475 additions and 19 deletions
+37
View File
@@ -28,6 +28,43 @@ export async function fetchUsesData(page: number, size: number, sortBy?: string,
}
});
if (response.ok) {
const parsed = await response.json();
if (parsed.message_code === 0) {
return parsed.message_body.message_body;
} else {
return null;
}
} else {
throw new Error(`${response.status}`);
}
} catch (error) {
return null;
}
}
export async function fetchUsesInfo(userInfo: number) {
console.log('fetchUsesInfo');
try {
const response = await fetch(`${API_BASE_URL}/api/admin/get-users`, {
method: 'POST',
body: JSON.stringify({
version: 1,
msg_id: 30014,
message_body: {
action: 'getIngoById',
user_id: userInfo
}
}),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
});
if (response.ok) {
const parsed = await response.json();
+162 -11
View File
@@ -31,9 +31,9 @@
padding: 0.5rem 1rem;
font-size: 0.875rem;
font-weight: 500;
color: v.$p-color;
color: var(--primary-color);
background-color: v.$white;
border: 2px solid v.$p-color;
border: 2px solid var(--primary-color);
border-radius: 0.375rem;
box-shadow: 0 1px 2px 0 v.$shadow-1;
transition: background-color 0.2s;
@@ -56,7 +56,7 @@
margin-top: 0.5rem;
transform-origin: top right;
background-color: v.$white;
border: 2px solid v.$p-color;
border: 2px solid var(--primary-color);
border-radius: 0.375rem;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1),
@@ -74,7 +74,7 @@
font-size: 0.875rem;
text-align: left;
transition: background-color 0.2s ease;
color: v.$p-color;
color: var(--primary-color);
&:hover {
background-color: #dbeafe;
@@ -128,7 +128,7 @@
position: fixed;
height: 100vh;
overflow-y: auto;
z-index: 1000;
z-index: 100;
.admin-nav {
padding: 16px 0;
@@ -807,7 +807,7 @@
border: 2px solid v.$border-color-1;
border-radius: 10px;
box-shadow: 0 1px 2px #0000000d;
color: v.$p-color;
color: var(--primary-color);
.icon {
width: 18px;
@@ -834,7 +834,7 @@
border: 2px solid v.$border-color-1;
padding: 0.25rem 0.75rem;
border-radius: 10px;
color: v.$p-color;
color: var(--primary-color);
&:disabled {
opacity: 0.5;
@@ -842,7 +842,7 @@
}
&.current {
background-color: v.$p-color;
background-color: var(--primary-color);
color: v.$white;
}
@@ -1230,7 +1230,7 @@
border: 2px solid v.$border-color-1;
border-radius: 10px;
box-shadow: 0 1px 2px #0000000d;
color: v.$p-color;
color: var(--primary-color);
cursor: pointer;
.icon {
@@ -1256,7 +1256,7 @@
border: 2px solid v.$border-color-1;
padding: 0.25rem 0.75rem;
border-radius: 10px;
color: v.$p-color;
color: var(--primary-color);
cursor: pointer;
&:disabled {
@@ -1265,7 +1265,7 @@
}
&.current {
background-color: v.$p-color;
background-color: var(--primary-color);
color: v.$white;
}
@@ -1276,4 +1276,155 @@
}
}
}
}
.modal-wrapper {
position: fixed;
z-index: 101;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: v.$bg-darkening;
.modal-window {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
padding: 20px;
background: v.$white;
color: v.$text-p;
border-radius: 20px;
box-shadow: 0px 4px 20px v.$shadow-1;
&:has(.modal-window-view-user) {
padding: 0;
background: transparent;
}
}
}
.modal-window-view-user {
border-radius: 20px;
overflow: hidden;
&-content {
padding: 10px;
max-height: calc(90vh - 80px);
overflow-y: auto;
background: v.$white;
&-wrapper {
height: 100%;
max-height: 80vh;
&.image {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
}
.image-section {
display: flex;
align-items: center;
justify-content: center;
min-height: 400px;
background: #fafafa;
border-radius: 12px;
position: relative;
}
.image-container {
position: relative;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
img {
max-width: 100%;
max-height: 100%;
object-fit: contain;
border-radius: 8px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
}
}
.user-info-wrapper {
display: flex;
gap: 15px;
margin-bottom: 15px;
@media (max-width: 875px) {
flex-direction: column;
}
}
.user-info-card {
background: #ffffff;
border: 1px solid v.$border-color-1;
border-radius: 12px;
padding: 0;
overflow: hidden;
min-width: 400px;
width: 100%;
@media (max-width: 470px) {
min-width: auto;
}
.info-header {
background: var(--primary-color);
color: white;
padding: 16px 20px;
border-bottom: 1px solid v.$border-color-1;
h4 {
margin: 0;
font-size: 16px;
font-weight: 600;
}
}
.info-item {
padding: 6px 10px;
border-bottom: 1px solid #f1f5f9;
width: 100%;
&.image {
text-align: center;
max-width: 200px;
display: flex;
margin: 0 auto;
img {
width: 100%;
}
}
}
.info-label {
font-size: 14px;
color: #64748b;
font-weight: 500;
flex-shrink: 0;
}
.info-value {
font-weight: 600;
color: #1e293b;
text-align: left;
word-break: break-word;
/* max-width: 150px; */
}
.status-badges {
padding: 12px 20px;
border-bottom: 1px solid #f1f5f9;
}
}
}
}
+16 -5
View File
@@ -20,6 +20,8 @@ import { toast } from 'sonner';
import { useUsersData } from '@/app/hooks/react-query/useUsersData';
import { TransformedUser } from '@/app/hooks/react-query/useUsersData';
import { useDebouncedCallback } from 'use-debounce';
import { fetchUsesInfo } from '@/app/actions/usersActions';
import { UserInfoModalWindow } from '@/app/ui/users/user-info-modal-window';
// Форматирование даты из timestamp
const formatDate = (timestamp: number) => {
@@ -395,7 +397,7 @@ export default function TanstakUsersTable() {
<div className="actions">
<div className="actions-group">
<button
onClick={() => handleView(row.original)}
onClick={() => handleView(row.original.id)}
className="bg-blue-500 hover:bg-blue-600"
>
<IconEye />
@@ -410,9 +412,16 @@ export default function TanstakUsersTable() {
[]
);
// Обработчики действий
const handleView = (file: TransformedUser) => {
console.log(`Просмотр файла: ${file.id}`);
const handleView = async (useId: number) => {
let userInfo = await fetchUsesInfo(useId);
console.log('userInfo');
setOpenWindowChildren(() => {
return (
<UserInfoModalWindow userInfo={userInfo} setWindowClose={setOpenWindow} setWindowChildren={setOpenWindowChildren} />
)
})
setOpenWindow(true);
};
// Фильтрация по типу файла и дате
@@ -478,7 +487,9 @@ export default function TanstakUsersTable() {
return (
<div className="tanstak-table-wrapper">
<ModalWindow children={openWindowChildren} state={openWindow} callBack={setOpenWindow}></ModalWindow>
<ModalWindow state={openWindow} callBack={setOpenWindow}>
{openWindowChildren}
</ModalWindow>
{/* Фильтры */}
<div className="tanstak-table-filtres">
<div className="table-filtres-wrapper">
+227
View File
@@ -0,0 +1,227 @@
import { formatDate, formatDateTime } from '@/app/lib/formatDate';
import { useTranslations } from 'next-intl';
interface User {
fullName: string;
company: string | null;
email: string;
active: boolean;
phone: string;
genderType: 'MALE' | 'FEMALE';
birthday: string | null;
createdAt: string;
subscriptionType: 'DEMO' | string;
tariffs: null;
tariffInfo: TariffInfo;
permission: null;
verifiedStatus: 'VERIFIED' | 'PENDING' | 'UNVERIFIED';
}
interface TariffInfo {
id: string;
status: 'EXPIRED' | 'ACTIVE' | 'PENDING';
startTariff: string;
endTariff: string;
tariffId: number;
tariffName: string;
tokens: number;
maxFileOnDisk: number;
currentFileOnDisk: number;
currentFileCounts: number;
maxFileCounts: number;
}
export function UserInfoModalWindow({
userInfo,
setWindowClose,
setWindowChildren
}: {
userInfo: User, // Исправлено: ожидаем User, а не TariffInfo
setWindowClose: any,
setWindowChildren: any
}) {
const t = useTranslations('Global');
const formatBytes = (bytes: number) => {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
};
const getStatusBadge = (status: string) => {
const statusConfig = {
'ACTIVE': { class: 'status-active', text: 'Active' },
'EXPIRED': { class: 'status-expired', text: 'Expired' },
'PENDING': { class: 'status-pending', text: 'Pending' },
'VERIFIED': { class: 'status-verified', text: 'Verified' },
'UNVERIFIED': { class: 'status-unverified', text: 'Unverified' }
};
const config = statusConfig[status as keyof typeof statusConfig];
return config ? <span className={`status-badge ${config.class}`}>{config.text}</span> : status;
};
const getGenderText = (gender: string) => {
return gender === 'MALE' ? 'Male' : 'Female';
};
console.log(userInfo)
return (
<div className="modal-window-view-user">
<div className="modal-window-view-user-content">
<div className="user-info-wrapper">
{/* Карточка информации о пользователе */}
<div className="user-info-card">
<div className="info-header">
<h4>{t('userInformation') || 'User Information'}</h4>
</div>
<div className="info-item">
<div className="info-label">
{t('fullName') || 'Full Name'}:
</div>
<div className="info-value">
{userInfo.fullName}
</div>
</div>
<div className="info-item">
<div className="info-label">
{t('email')}:
</div>
<div className="info-value">
{userInfo.email}
</div>
</div>
<div className="info-item">
<div className="info-label">
{t('phone') || 'Phone'}:
</div>
<div className="info-value">
{userInfo.phone}
</div>
</div>
<div className="info-item">
<div className="info-label">
{t('company')}:
</div>
<div className="info-value">
{userInfo.company || '-'}
</div>
</div>
<div className="info-item">
<div className="info-label">
{t('gender') || 'Gender'}:
</div>
<div className="info-value">
{getGenderText(userInfo.genderType)}
</div>
</div>
<div className="info-item">
<div className="info-label">
{t('birthday') || 'Birthday'}:
</div>
<div className="info-value">
{userInfo.birthday ? formatDate(userInfo.birthday) : '-'}
</div>
</div>
<div className="info-item">
<div className="info-label">
{t('createdAt') || 'Registered'}:
</div>
<div className="info-value">
{formatDateTime(userInfo.createdAt)}
</div>
</div>
<div className="info-item">
<div className="info-label">
{t('verifiedStatus') || 'Verification Status'}:
</div>
<div className="info-value">
{getStatusBadge(userInfo.verifiedStatus)}
</div>
</div>
<div className="info-item">
<div className="info-label">
{t('active')}:
</div>
<div className="info-value">
{userInfo.active ? 'active' : 'not active'}
</div>
</div>
</div>
{/* Карточка информации о тарифе */}
<div className="user-info-card">
<div className="info-header">
<h4>{t('tariffInformation') || 'Tariff Information'}</h4>
</div>
<div className="info-item">
<div className="info-label">
{t('tariffName') || 'Tariff'}:
</div>
<div className="info-value">
{userInfo.tariffInfo.tariffName}
</div>
</div>
<div className="info-item">
<div className="info-label">
{t('tariffStatus') || 'Status'}:
</div>
<div className="info-value">
{getStatusBadge(userInfo.tariffInfo.status)}
</div>
</div>
<div className="info-item">
<div className="info-label">
{t('tokens') || 'Tokens'}:
</div>
<div className="info-value">
{userInfo.tariffInfo.tokens || 0}
</div>
</div>
<div className="info-item">
<div className="info-label">
{t('startDate') || 'Start Date'}:
</div>
<div className="info-value">
{formatDate(userInfo.tariffInfo.startTariff)}
</div>
</div>
<div className="info-item">
<div className="info-label">
{t('endDate') || 'End Date'}:
</div>
<div className="info-value">
{formatDate(userInfo.tariffInfo.endTariff)}
</div>
</div>
</div>
</div>
{/* Карточка использования диска */}
<div className="user-info-card">
<div className="info-header">
<h4>{t('storageInformation') || 'Storage Information'}</h4>
</div>
<div className="info-item">
<div className="info-label">
{t('diskUsage') || 'Disk Usage'}:
</div>
<div className="info-value">
{formatBytes(userInfo.tariffInfo.currentFileOnDisk)} / {formatBytes(userInfo.tariffInfo.maxFileOnDisk)}
</div>
</div>
<div className="info-item">
<div className="info-label">
{t('fileCount') || 'Files'}:
</div>
<div className="info-value">
{userInfo.tariffInfo.currentFileCounts} / {userInfo.tariffInfo.maxFileCounts}
</div>
</div>
</div>
</div>
</div>
);
}