add dashboard statistic

This commit is contained in:
smanylov
2026-05-15 15:31:40 +07:00
parent 80692d6446
commit cce7eb6f96
22 changed files with 972 additions and 176 deletions
+2 -1
View File
@@ -31,7 +31,8 @@ export default async function RootLayout({
} }
const queryClient = getQueryClient(); const queryClient = getQueryClient();
await prefetchLayoutQueries(queryClient);
await prefetchLayoutQueries(queryClient);
return ( return (
<HydrationBoundary state={dehydrate(queryClient)}> <HydrationBoundary state={dehydrate(queryClient)}>
+284
View File
@@ -0,0 +1,284 @@
'use server'
import { API_BASE_URL } from '@/app/actions/definitions';
import { getSessionData } from '@/app/actions/session';
export async function fetchTariffStatistic() {
try {
const response = await fetch(`${API_BASE_URL}/api/admin/info`, {
method: 'POST',
body: JSON.stringify({
version: 1,
msg_id: 30020,
message_body: {
active_only: true
}
}),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
});
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 fetchUserFilesTopStatistic() {
try {
const response = await fetch(`${API_BASE_URL}/api/admin/info`, {
method: 'POST',
body: JSON.stringify({
version: 1,
msg_id: 30019,
message_body: {
top: 5
}
}),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
});
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 fetchUserGeneralStatistic() {
try {
const response = await fetch(`${API_BASE_URL}/api/admin/info`, {
method: 'POST',
body: JSON.stringify({
version: 1,
msg_id: 30021,
message_body: {
active_only: true
}
}),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
});
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 fetchSubscribeGeneralStatistic() {
try {
const response = await fetch(`${API_BASE_URL}/api/admin/info`, {
method: 'POST',
body: JSON.stringify({
version: 1,
msg_id: 30024,
message_body: {
active_only: true
}
}),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
});
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 fetchProtectedContentGeneralStatistic() {
try {
const response = await fetch(`${API_BASE_URL}/api/admin/info`, {
method: 'POST',
body: JSON.stringify({
version: 1,
msg_id: 30022,
message_body: {
active_only: true
}
}),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
});
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 fetchViolationGeneralStatistic() {
try {
const response = await fetch(`${API_BASE_URL}/api/admin/info`, {
method: 'POST',
body: JSON.stringify({
version: 1,
msg_id: 30023,
message_body: {
active_only: true
}
}),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
});
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 fetchTokensGeneralStatistic() {
try {
const response = await fetch(`${API_BASE_URL}/api/admin/info`, {
method: 'POST',
body: JSON.stringify({
version: 1,
msg_id: 30025,
message_body: {
active_only: true
}
}),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
});
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 fetchIncomeGeneralStatistic() {
try {
const response = await fetch(`${API_BASE_URL}/api/admin/info`, {
method: 'POST',
body: JSON.stringify({
version: 1,
msg_id: 30026,
message_body: {
active_only: true
}
}),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
});
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;
}
}
-1
View File
@@ -46,7 +46,6 @@ export async function fetchUsesData(page: number, size: number, sortBy?: string,
} }
export async function fetchUsesInfo(userInfo: number) { export async function fetchUsesInfo(userInfo: number) {
console.log('fetchUsesInfo');
try { try {
const response = await fetch(`${API_BASE_URL}/api/admin/get-users`, { const response = await fetch(`${API_BASE_URL}/api/admin/get-users`, {
@@ -0,0 +1,31 @@
import { useQuery } from '@tanstack/react-query';
import { fetchIncomeGeneralStatistic } from '@/app/actions/statisticActions';
interface IncomePerUser {
p5: number;
p50: number;
p95: number;
}
interface IncomeGeneralStatistic {
total_income: number;
total_available: number;
total_hold: number;
income_per_user: IncomePerUser;
}
export const useIncomeGeneralStatistic = () => {
return useQuery({
queryKey: ['incomeGeneralStatistic'],
queryFn: () => {
return fetchIncomeGeneralStatistic()
},
select: (data: IncomeGeneralStatistic | null) => {
if (!data) {
return null
}
return data;
},
retry: false
});
};
@@ -0,0 +1,41 @@
import { useQuery } from '@tanstack/react-query';
import { fetchProtectedContentGeneralStatistic } from '@/app/actions/statisticActions';
interface FilesPerUserPercentiles {
p5: number;
p50: number;
p95: number;
}
interface SizePerUserPercentiles {
p5_bytes: number;
p50_bytes: number;
p95_bytes: number;
p5_mb: number;
p50_mb: number;
p95_mb: number;
}
interface ProtectedContentGeneralStatistic {
total_files: number;
total_size_bytes: number;
total_size_gb: number;
files_per_user_percentiles: FilesPerUserPercentiles;
size_per_user_percentiles: SizePerUserPercentiles;
}
export const useProtectedContentGeneralStatistic = () => {
return useQuery({
queryKey: ['protectedContentGeneralStatistic'],
queryFn: () => {
return fetchProtectedContentGeneralStatistic()
},
select: (data: ProtectedContentGeneralStatistic | null) => {
if (!data) {
return null
}
return data;
},
retry: false
});
};
@@ -0,0 +1,31 @@
import { useQuery } from '@tanstack/react-query';
import { fetchSubscribeGeneralStatistic } from '@/app/actions/statisticActions';
interface SubscriptionStats {
current_active_subscriptions: number;
first_time_subscriptions: number;
renewals: number;
upgrades: number;
downgrades: number;
}
interface SubscribeGeneralStatistic {
monthly: SubscriptionStats;
yearly: SubscriptionStats;
}
export const useSubscribeGeneralStatistic = () => {
return useQuery({
queryKey: ['subscribeGeneralStatistic'],
queryFn: () => {
return fetchSubscribeGeneralStatistic()
},
select: (data: SubscribeGeneralStatistic | null) => {
if (!data) {
return null
}
return data;
},
retry: false
});
};
@@ -0,0 +1,28 @@
import { useQuery } from '@tanstack/react-query';
import { fetchTariffStatistic } from '@/app/actions/statisticActions';
interface TariffItem {
tariff_name: string;
user_count: number;
usage_percent: number;
}
interface TariffStatistic {
tariffs: TariffItem[];
}
export const useMonitoringStats = () => {
return useQuery({
queryKey: ['tariffStatistic'],
queryFn: () => {
return fetchTariffStatistic()
},
select: (data: TariffStatistic | null) => {
if (!data) {
return null
}
return data;
},
retry: false
});
};
@@ -0,0 +1,37 @@
import { useQuery } from '@tanstack/react-query';
import { fetchTokensGeneralStatistic } from '@/app/actions/statisticActions';
interface BoughtPerUser {
p5: number;
p50: number;
p95: number;
}
interface SpentPerUser {
p5: number;
p50: number;
p95: number;
}
interface TokensGeneralStatistic {
total_bought: number;
total_spent: number;
bought_per_user: BoughtPerUser;
spent_per_user: SpentPerUser;
}
export const useTokensGeneralStatistic = () => {
return useQuery({
queryKey: ['tokensGeneralStatistic'],
queryFn: () => {
return fetchTokensGeneralStatistic()
},
select: (data: TokensGeneralStatistic | null) => {
if (!data) {
return null
}
return data;
},
retry: false
});
};
@@ -0,0 +1,29 @@
import { useQuery } from '@tanstack/react-query';
import { fetchUserFilesTopStatistic } from '@/app/actions/statisticActions';
interface UserItem {
full_name: string;
user_id: number;
count: number;
files_size: number;
}
interface UserFilesTopStatistic {
tariffs: UserItem[];
}
export const useUserFilesTopStatistic = () => {
return useQuery({
queryKey: ['userFilesTopStatistic'],
queryFn: () => {
return fetchUserFilesTopStatistic()
},
select: (data: UserFilesTopStatistic | null) => {
if (!data) {
return null
}
return data;
},
retry: false
});
};
@@ -0,0 +1,29 @@
import { useQuery } from '@tanstack/react-query';
import { fetchUserGeneralStatistic } from '@/app/actions/statisticActions';
interface UserGeneralStatistic {
total_users: number;
new_last_30_days: number;
new_previous_30_days: number;
growth_percent: number;
new_current_month: number;
new_previous_month: number;
month_growth_percent: number;
}
export const useUserGeneralStatistic = () => {
return useQuery({
queryKey: ['userGeneralStatistic'],
queryFn: () => {
return fetchUserGeneralStatistic()
},
select: (data: UserGeneralStatistic | null) => {
if (!data) {
return null
}
return data;
},
retry: false
});
};
@@ -0,0 +1,27 @@
import { useQuery } from '@tanstack/react-query';
import { fetchViolationGeneralStatistic } from '@/app/actions/statisticActions';
interface ViolationGeneralStatistic {
total_violations: number;
total_complaints: number;
total_law_cases: number;
complaints_without_case: number;
violations_with_complaint: number;
violations_without_complaint: number;
}
export const useViolationGeneralStatistic = () => {
return useQuery({
queryKey: ['violationGeneralStatistic'],
queryFn: () => {
return fetchViolationGeneralStatistic()
},
select: (data: ViolationGeneralStatistic | null) => {
if (!data) {
return null
}
return data;
},
retry: false
});
};
+1 -1
View File
@@ -1,4 +1,4 @@
export function convertBytes(bytes: number) { export function convertBytes(bytes: number | undefined) {
if (!bytes) { if (!bytes) {
return 0; return 0;
} }
+33 -7
View File
@@ -1,14 +1,40 @@
import { QueryClient } from '@tanstack/react-query'; import { QueryClient } from '@tanstack/react-query';
import { fetchTokensGeneralStatistic, fetchProtectedContentGeneralStatistic, fetchTariffStatistic, fetchUserFilesTopStatistic, fetchUserGeneralStatistic, fetchViolationGeneralStatistic } from '@/app/actions/statisticActions';
import { useSubscribeGeneralStatistic } from '@/app/hooks/react-query/dashboard/useSubscribeGeneralStatistic';
export async function prefetchLayoutQueries(queryClient: QueryClient) { export async function prefetchLayoutQueries(queryClient: QueryClient) {
await Promise.all([ await Promise.all([
queryClient.prefetchQuery({ queryClient.prefetchQuery({
queryKey: ['userData'], queryKey: ['incomeGeneralStatistic'],
queryFn: () => { queryFn: () => fetchTokensGeneralStatistic()
return { }),
test: 'testFetchUserData' queryClient.prefetchQuery({
} queryKey: ['protectedContentGeneralStatistic'],
} queryFn: () => fetchProtectedContentGeneralStatistic()
}) }),
queryClient.prefetchQuery({
queryKey: ['subscribeGeneralStatistic'],
queryFn: () => useSubscribeGeneralStatistic()
}),
queryClient.prefetchQuery({
queryKey: ['tariffStatistic'],
queryFn: () => fetchTariffStatistic()
}),
queryClient.prefetchQuery({
queryKey: ['tokensGeneralStatistic'],
queryFn: () => fetchTokensGeneralStatistic()
}),
queryClient.prefetchQuery({
queryKey: ['userFilesTopStatistic'],
queryFn: () => fetchUserFilesTopStatistic()
}),
queryClient.prefetchQuery({
queryKey: ['userGeneralStatistic'],
queryFn: () => fetchUserGeneralStatistic()
}),
queryClient.prefetchQuery({
queryKey: ['violationGeneralStatistic'],
queryFn: () => fetchViolationGeneralStatistic()
}),
]); ]);
} }
+17 -10
View File
@@ -478,6 +478,18 @@
gap: 20px; gap: 20px;
margin-bottom: 24px; margin-bottom: 24px;
&.user-content {
grid-template-columns: minmax(240px, 1fr) minmax(240px, 1fr) 2fr;
}
@media (max-width: 1540px) {
grid-template-columns: repeat(2, minmax(240px, 1fr));
&.user-content {
grid-template-columns: repeat(2, minmax(240px, 1fr));
}
}
.stat-card { .stat-card {
background: var(--surface); background: var(--surface);
padding: 20px; padding: 20px;
@@ -511,24 +523,18 @@
} }
.stat-card-value { .stat-card-value {
width: 40px; /* width: 40px; */
width: auto;
height: 40px; height: 40px;
border-radius: 10px; border-radius: 10px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
font-size: 20px; font-size: 16px;
padding: 0 10px;
background: #eff6ff; background: #eff6ff;
} }
/* .stat-card-value {
font-size: 32px;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 4px;
} */
.stat-card-label { .stat-card-label {
font-size: 12px; font-size: 12px;
color: var(--text-secondary); color: var(--text-secondary);
@@ -1310,6 +1316,7 @@
overflow: hidden; overflow: hidden;
.status { .status {
&-active, &-active,
&-verified { &-verified {
color: #166534; color: #166534;
@@ -1,18 +1,46 @@
'use client'
import { useProtectedContentGeneralStatistic } from '@/app/hooks/react-query/dashboard/useProtectedContentGeneralStatistic'
import { convertBytes } from '@/app/lib/convertBytes';
import { useEffect } from 'react';
export default function GeneralInfoContent() { export default function GeneralInfoContent() {
const { data, isLoading, isError } = useProtectedContentGeneralStatistic();
if (isLoading) {
return (
<div>
<h4 className="stats-title">Защищенный контент (месяц/год)</h4>
<div className="stats-grid">
<div className="stat-card">Загрузка...</div>
</div>
</div>
);
}
if (isError) {
return (
<div>
<h4 className="stats-title">Защищенный контент (месяц/год)</h4>
<div className="stats-grid">
<div className="stat-card">Ошибка при загрузке данных</div>
</div>
</div>
);
}
return ( return (
<div> <div>
<h4 <h4 className="stats-title">
className="stats-title"
>
Защищенный контент (месяц/год) Защищенный контент (месяц/год)
</h4> </h4>
<div className="stats-grid"> <div className="stats-grid user-content">
<div className="stat-card"> <div className="stat-card">
<div className="stat-card-group"> <div className="stat-card-group">
<span className="stat-card-title">Всего файлов этом месяце</span> {/* <span className="stat-card-title">Всего файлов этом месяце</span> */}
<div className="stat-card-value">0</div> <span className="stat-card-title">Всего файлов</span>
<div className="stat-card-value">{data?.total_files || 0}</div>
</div> </div>
<div className="stat-card-group"> <div className="stat-card-group">
<span className="stat-card-title">В прошлом месяце</span> <span className="stat-card-title">В прошлом месяце</span>
@@ -22,8 +50,9 @@ export default function GeneralInfoContent() {
<div className="stat-card"> <div className="stat-card">
<div className="stat-card-group"> <div className="stat-card-group">
<span className="stat-card-title">Всего занято места в этом месяце</span> {/* <span className="stat-card-title">Всего занято места в этом месяце</span> */}
<div className="stat-card-value">0</div> <span className="stat-card-title">Всего занято места</span>
<div className="stat-card-value">{convertBytes(data?.total_size_bytes)}</div>
</div> </div>
<div className="stat-card-group"> <div className="stat-card-group">
<span className="stat-card-title">В прошлом месяце</span> <span className="stat-card-title">В прошлом месяце</span>
@@ -34,11 +63,19 @@ export default function GeneralInfoContent() {
<div className="stat-card"> <div className="stat-card">
<div className="stat-card-group"> <div className="stat-card-group">
<span className="stat-card-title">Файлов на пользователя</span> <span className="stat-card-title">Файлов на пользователя</span>
<div className="stat-card-value">0</div> <div className="stat-card-value">
{data?.files_per_user_percentiles?.p5 || 0}/
{data?.files_per_user_percentiles?.p50 || 0}/
{data?.files_per_user_percentiles?.p95 || 0}
</div>
</div> </div>
<div className="stat-card-group"> <div className="stat-card-group">
<span className="stat-card-title">Места на диске на пользователя</span> <span className="stat-card-title">Места на диске на пользователя</span>
<div className="stat-card-value">0</div> <div className="stat-card-value">
{convertBytes(data?.size_per_user_percentiles?.p5_bytes)} /
{convertBytes(data?.size_per_user_percentiles?.p50_bytes)} /
{convertBytes(data?.size_per_user_percentiles?.p95_bytes)}
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -1,44 +1,67 @@
'use client'
import { useIncomeGeneralStatistic } from '@/app/hooks/react-query/dashboard/useIncomeGeneralStatistic';
import { useEffect } from 'react';
export default function GeneralInfoIncome() { export default function GeneralInfoIncome() {
const { data, isLoading, isError } = useIncomeGeneralStatistic();
if (isLoading) {
return (
<div>
<h4 className="stats-title">Доход (день/месяц)</h4>
<div className="stats-grid">
<div className="stat-card">Загрузка...</div>
</div>
</div>
);
}
if (isError) {
return (
<div>
<h4 className="stats-title">Доход (день/месяц)</h4>
<div className="stats-grid">
<div className="stat-card">Ошибка при загрузке данных</div>
</div>
</div>
);
}
return ( return (
<div> <div>
<h4 <h4 className="stats-title">
className="stats-title"
>
Доход (день/месяц) Доход (день/месяц)
</h4> </h4>
<div className="stats-grid"> <div className="stats-grid">
<div className="stat-card"> <div className="stat-card">
<div className="stat-card-group"> <div className="stat-card-group">
<span className="stat-card-title">Всего заработано в этом месяце</span> <span className="stat-card-title">Всего заработано</span>
<div className="stat-card-value">0</div> <div className="stat-card-value">{data?.total_income || 0} </div>
</div> </div>
<div className="stat-card-group"> <div className="stat-card-group">
<span className="stat-card-title">В прошлом месяце</span> <span className="stat-card-title">Доступно</span>
<div className="stat-card-value">0</div> <div className="stat-card-value">{data?.total_available || 0} </div>
</div>
<div className="stat-card-group">
<span className="stat-card-title">Заморожено</span>
<div className="stat-card-value">{data?.total_hold || 0} </div>
</div> </div>
</div> </div>
<div className="stat-card"> <div className="stat-card">
<div className="stat-card-group"> <div className="stat-card-group">
<span className="stat-card-title">Всего заработано на любого пользователя в этом месяце</span> <span className="stat-card-title">Всего заработано на пользователя</span>
<div className="stat-card-value">0</div> <div className="stat-card-value">
{data?.income_per_user?.p5 || 0}/
{data?.income_per_user?.p50 || 0}/
{data?.income_per_user?.p95 || 0}
</div>
</div> </div>
<div className="stat-card-group"> <div className="stat-card-group">
<span className="stat-card-title">В прошлом месяце</span> <span className="stat-card-title">В прошлом месяце</span>
<div className="stat-card-value">0</div> <div className="stat-card-value">0/0/0</div>
</div>
</div>
<div className="stat-card">
<div className="stat-card-group">
<span className="stat-card-title">Всего заработано на платного пользователя в этом месяце</span>
<div className="stat-card-value">0</div>
</div>
<div className="stat-card-group">
<span className="stat-card-title">В прошлом месяце</span>
<div className="stat-card-value">0</div>
</div> </div>
</div> </div>
</div> </div>
@@ -1,66 +1,91 @@
'use client'
import { useSubscribeGeneralStatistic } from '@/app/hooks/react-query/dashboard/useSubscribeGeneralStatistic';
export default function GeneralInfoSubscriptions() { export default function GeneralInfoSubscriptions() {
const { data, isLoading, isError } = useSubscribeGeneralStatistic();
if (isLoading) {
return (
<div>
<h4 className="stats-title">Активные подписчики (месяц/год)</h4>
<div className="stats-grid">
<div className="stat-card">Загрузка...</div>
</div>
</div>
);
}
if (isError) {
return (
<div>
<h4 className="stats-title">Активные подписчики (месяц/год)</h4>
<div className="stats-grid">
<div className="stat-card">Ошибка при загрузке данных</div>
</div>
</div>
);
}
return ( return (
<div> <div>
<h4 <h4 className="stats-title">Активные подписчики (месяц/год)</h4>
className="stats-title"
>
Активные подписчики (месяц/год)
</h4>
<div className="stats-grid"> <div className="stats-grid">
<div className="stat-card"> <div className="stat-card">
<div className="stat-card-group"> <div className="stat-card-group">
<span className="stat-card-title">Текущие платные подписки</span> <span className="stat-card-title">Текущие платные подписки</span>
<div className="stat-card-value">0</div> <div className="stat-card-value">
{(data?.monthly?.current_active_subscriptions || 0) + (data?.yearly?.current_active_subscriptions || 0)}
</div>
</div> </div>
</div> </div>
<div className="stat-card"> <div className="stat-card">
<div className="stat-card-group"> <div className="stat-card-group">
<span className="stat-card-title">оформили впервые в этом месяце</span> <span className="stat-card-title">оформили впервые в этом месяце</span>
<div className="stat-card-value">0</div> <div className="stat-card-value">{data?.monthly?.first_time_subscriptions || 0}</div>
</div> </div>
<div className="stat-card-group"> <div className="stat-card-group">
<span className="stat-card-title">в прошлом месяце</span> <span className="stat-card-title">в прошлом месяце</span>
<div className="stat-card-value">0</div> <div className="stat-card-value">{data?.yearly?.first_time_subscriptions || 0}</div>
</div> </div>
</div> </div>
<div className="stat-card"> <div className="stat-card">
<div className="stat-card-group"> <div className="stat-card-group">
<span className="stat-card-title">продлили подпискув этом месяце</span> <span className="stat-card-title">продлили подписку в этом месяце</span>
<div className="stat-card-value">0</div> <div className="stat-card-value">{data?.monthly?.renewals || 0}</div>
</div> </div>
<div className="stat-card-group"> <div className="stat-card-group">
<span className="stat-card-title">в прошлом месяце</span> <span className="stat-card-title">в прошлом месяце</span>
<div className="stat-card-value">0</div> <div className="stat-card-value">{data?.yearly?.renewals || 0}</div>
</div> </div>
</div> </div>
<div className="stat-card"> <div className="stat-card">
<div className="stat-card-group"> <div className="stat-card-group">
<span className="stat-card-title">сделали апгрейдв этом месяце</span> <span className="stat-card-title">сделали апгрейд в этом месяце</span>
<div className="stat-card-value">0</div> <div className="stat-card-value">{data?.monthly?.upgrades || 0}</div>
</div> </div>
<div className="stat-card-group"> <div className="stat-card-group">
<span className="stat-card-title">в прошлом месяце</span> <span className="stat-card-title">в прошлом месяце</span>
<div className="stat-card-value">0</div> <div className="stat-card-value">{data?.yearly?.upgrades || 0}</div>
</div> </div>
</div> </div>
<div className="stat-card"> {/* <div className="stat-card">
<div className="stat-card-group"> <div className="stat-card-group">
<span className="stat-card-title">сделали даунгрейдв этом месяце</span> <span className="stat-card-title">сделали даунгрейд в этом месяце</span>
<div className="stat-card-value">0</div> <div className="stat-card-value">{data?.monthly?.downgrades || 0}</div>
</div> </div>
<div className="stat-card-group"> <div className="stat-card-group">
<span className="stat-card-title">в прошлом месяце</span> <span className="stat-card-title">в прошлом месяце</span>
<div className="stat-card-value">0</div> <div className="stat-card-value">{data?.yearly?.downgrades || 0}</div>
</div> </div>
</div> </div> */}
</div> </div>
</div> </div>
) );
} }
@@ -1,18 +1,45 @@
'use client'
import { useTokensGeneralStatistic } from '@/app/hooks/react-query/dashboard/useTokensGeneralStatistic';
import { useEffect } from 'react';
export default function GeneralInfoTokens() { export default function GeneralInfoTokens() {
const { data, isLoading, isError } = useTokensGeneralStatistic();
if (isLoading) {
return (
<div>
<h4 className="stats-title">Токены (день/месяц)</h4>
<div className="stats-grid">
<div className="stat-card">Загрузка...</div>
</div>
</div>
);
}
if (isError) {
return (
<div>
<h4 className="stats-title">Токены (день/месяц)</h4>
<div className="stats-grid">
<div className="stat-card">Ошибка при загрузке данных</div>
</div>
</div>
);
}
return ( return (
<div> <div>
<h4 <h4 className="stats-title">
className="stats-title"
>
Токены (день/месяц) Токены (день/месяц)
</h4> </h4>
<div className="stats-grid"> <div className="stats-grid">
<div className="stat-card"> <div className="stat-card">
<div className="stat-card-group"> <div className="stat-card-group">
<span className="stat-card-title">Всего купленов этом месяце</span> {/* <span className="stat-card-title">Всего куплено в этом месяце</span> */}
<div className="stat-card-value">0</div> <span className="stat-card-title">Всего куплено</span>
<div className="stat-card-value">{data?.total_bought || 0}</div>
</div> </div>
<div className="stat-card-group"> <div className="stat-card-group">
<span className="stat-card-title">В прошлом месяце</span> <span className="stat-card-title">В прошлом месяце</span>
@@ -22,8 +49,9 @@ export default function GeneralInfoTokens() {
<div className="stat-card"> <div className="stat-card">
<div className="stat-card-group"> <div className="stat-card-group">
<span className="stat-card-title">Всего потраченов этом месяце</span> {/* <span className="stat-card-title">Всего потрачено в этом месяце</span> */}
<div className="stat-card-value">0</div> <span className="stat-card-title">Всего потрачено</span>
<div className="stat-card-value">{data?.total_spent || 0}</div>
</div> </div>
<div className="stat-card-group"> <div className="stat-card-group">
<span className="stat-card-title">В прошлом месяце</span> <span className="stat-card-title">В прошлом месяце</span>
@@ -34,22 +62,30 @@ export default function GeneralInfoTokens() {
<div className="stat-card"> <div className="stat-card">
<div className="stat-card-group"> <div className="stat-card-group">
<span className="stat-card-title">Куплено на пользователя в этом месяце</span> <span className="stat-card-title">Куплено на пользователя в этом месяце</span>
<div className="stat-card-value">0</div> <div className="stat-card-value">
{data?.bought_per_user?.p5 || 0}/
{data?.bought_per_user?.p50 || 0}/
{data?.bought_per_user?.p95 || 0}
</div>
</div> </div>
<div className="stat-card-group"> <div className="stat-card-group">
<span className="stat-card-title">В прошлом месяце</span> <span className="stat-card-title">В прошлом месяце</span>
<div className="stat-card-value">0</div> <div className="stat-card-value">0/0/0</div>
</div> </div>
</div> </div>
<div className="stat-card"> <div className="stat-card">
<div className="stat-card-group"> <div className="stat-card-group">
<span className="stat-card-title">Потрачено на пользователя в этом месяце</span> <span className="stat-card-title">Потрачено на пользователя в этом месяце</span>
<div className="stat-card-value">0</div> <div className="stat-card-value">
{data?.spent_per_user?.p5 || 0}/
{data?.spent_per_user?.p50 || 0}/
{data?.spent_per_user?.p95 || 0}
</div>
</div> </div>
<div className="stat-card-group"> <div className="stat-card-group">
<span className="stat-card-title">В прошлом месяце</span> <span className="stat-card-title">В прошлом месяце</span>
<div className="stat-card-value">0</div> <div className="stat-card-value">0/0/0</div>
</div> </div>
</div> </div>
</div> </div>
@@ -1,51 +1,87 @@
'use client'
import { useUserGeneralStatistic } from '@/app/hooks/react-query/dashboard/useUserGeneralStatistic';
export default function GeneralInfoUsers() { export default function GeneralInfoUsers() {
const { data, isLoading, isError } = useUserGeneralStatistic();
if (isLoading) {
return (
<div>
<h4 className="stats-title">Кол-во пользователей и динамика</h4>
<div className="stats-grid">
<div className="stat-card">Загрузка...</div>
</div>
</div>
);
}
if (isError) {
return (
<div>
<h4 className="stats-title">Кол-во пользователей и динамика</h4>
<div className="stats-grid">
<div className="stat-card">Ошибка при загрузке данных</div>
</div>
</div>
);
}
const renderDelta = (percent: number) => {
const isPositive = percent > 0;
const isNegative = percent < 0;
const sign = isPositive ? '+' : '';
const color = isPositive ? '#10b981' : isNegative ? '#ef4444' : '#6b7280';
return (
<div className="stat-card-value" style={{ color }}>
{sign}{percent}%
</div>
);
};
return ( return (
<div> <div>
<h4 <h4 className="stats-title">Кол-во пользователей и динамика</h4>
className="stats-title"
>
Кол-во пользователей и динамика
</h4>
<div className="stats-grid"> <div className="stats-grid">
<div className="stat-card"> <div className="stat-card">
<div className="stat-card-group"> <div className="stat-card-group">
<span className="stat-card-title">Всего пользователей</span> <span className="stat-card-title">Всего пользователей</span>
<div className="stat-card-value">0</div> <div className="stat-card-value">{data?.total_users || 0}</div>
</div> </div>
</div> </div>
<div className="stat-card"> <div className="stat-card">
<div className="stat-card-group"> <div className="stat-card-group">
<span className="stat-card-title">Новых за 30 дней</span> <span className="stat-card-title">Новых за 30 дней</span>
<div className="stat-card-value">0</div> <div className="stat-card-value">{data?.new_last_30_days || 0}</div>
</div> </div>
<div className="stat-card-group"> <div className="stat-card-group">
<span className="stat-card-title">Новых за предыдущие 30 дней</span> <span className="stat-card-title">Новых за предыдущие 30 дней</span>
<div className="stat-card-value">0</div> <div className="stat-card-value">{data?.new_previous_30_days || 0}</div>
</div> </div>
<div className="stat-card-group"> <div className="stat-card-group">
<span className="stat-card-title">Дельта</span> <span className="stat-card-title">Дельта</span>
<div className="stat-card-value">0</div> {renderDelta(data?.growth_percent || 0)}
</div> </div>
</div> </div>
<div className="stat-card"> <div className="stat-card">
<div className="stat-card-group"> <div className="stat-card-group">
<span className="stat-card-title">Новых за текущий месяц</span> <span className="stat-card-title">Новых за текущий месяц</span>
<div className="stat-card-value">0</div> <div className="stat-card-value">{data?.new_current_month || 0}</div>
</div> </div>
<div className="stat-card-group"> <div className="stat-card-group">
<span className="stat-card-title">Новых за прошлый месяц</span> <span className="stat-card-title">Новых за прошлый месяц</span>
<div className="stat-card-value">0</div> <div className="stat-card-value">{data?.new_previous_month || 0}</div>
</div> </div>
<div className="stat-card-group"> <div className="stat-card-group">
<span className="stat-card-title">Дельта</span> <span className="stat-card-title">Дельта</span>
<div className="stat-card-value">0</div> {renderDelta(data?.month_growth_percent || 0)}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
) );
} }
@@ -1,18 +1,43 @@
'use client'
import { useViolationGeneralStatistic } from '@/app/hooks/react-query/dashboard/useViolationGeneralStatistic';
export default function GeneralInfoViolations() { export default function GeneralInfoViolations() {
const { data, isLoading, isError } = useViolationGeneralStatistic();
if (isLoading) {
return (
<div>
<h4 className="stats-title">Нарушения (день/месяц)</h4>
<div className="stats-grid">
<div className="stat-card">Загрузка...</div>
</div>
</div>
);
}
if (isError) {
return (
<div>
<h4 className="stats-title">Нарушения (день/месяц)</h4>
<div className="stats-grid">
<div className="stat-card">Ошибка при загрузке данных</div>
</div>
</div>
);
}
return ( return (
<div> <div>
<h4 <h4 className="stats-title">
className="stats-title"
>
Нарушения (день/месяц) Нарушения (день/месяц)
</h4> </h4>
<div className="stats-grid"> <div className="stats-grid">
<div className="stat-card"> <div className="stat-card">
<div className="stat-card-group"> <div className="stat-card-group">
<span className="stat-card-title">Всего нарушений в этом месяце</span> <span className="stat-card-title">Всего совпадений</span>
<div className="stat-card-value">0</div> <div className="stat-card-value">{data?.total_violations || 0}</div>
</div> </div>
<div className="stat-card-group"> <div className="stat-card-group">
<span className="stat-card-title">В прошлом месяце</span> <span className="stat-card-title">В прошлом месяце</span>
@@ -22,9 +47,20 @@ export default function GeneralInfoViolations() {
<div className="stat-card"> <div className="stat-card">
<div className="stat-card-group"> <div className="stat-card-group">
<span className="stat-card-title">Подано жалоб в этом месяце</span> <span className="stat-card-title">Всего подано жалоб</span>
<div className="stat-card-value">{data?.total_complaints || 0}</div>
</div>
<div className="stat-card-group">
<span className="stat-card-title">В прошлом месяце</span>
<div className="stat-card-value">0</div> <div className="stat-card-value">0</div>
</div> </div>
</div>
<div className="stat-card">
<div className="stat-card-group">
<span className="stat-card-title">Всего подано претензий</span>
<div className="stat-card-value">{data?.total_law_cases || 0}</div>
</div>
<div className="stat-card-group"> <div className="stat-card-group">
<span className="stat-card-title">В прошлом месяце</span> <span className="stat-card-title">В прошлом месяце</span>
<div className="stat-card-value">0</div> <div className="stat-card-value">0</div>
@@ -1,4 +1,38 @@
'use client'
import { useMonitoringStats } from '@/app/hooks/react-query/dashboard/useTariffStatistic';
export default function SubscriptionsStatistic() { export default function SubscriptionsStatistic() {
const { data, isLoading, isError } = useMonitoringStats();
if (isLoading) {
return (
<div className="admin-content">
<div className="content-header">
<h3>Статистика подписок</h3>
</div>
<div className="content-body">
<div>Загрузка...</div>
</div>
</div>
);
}
if (isError) {
return (
<div className="admin-content">
<div className="content-header">
<h3>Статистика подписок</h3>
</div>
<div className="content-body">
<div>Ошибка при загрузке данных</div>
</div>
</div>
);
}
const tariffs = data?.tariffs || [];
return ( return (
<div className="admin-content"> <div className="admin-content">
<div className="content-header"> <div className="content-header">
@@ -14,36 +48,28 @@ export default function SubscriptionsStatistic() {
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr> {tariffs.length > 0 ? (
<td> tariffs.map((tariff, index) => (
<span className="badge badge-primary"> <tr key={index}>
СТАРТ <td>
</span> <span className={`badge badge-primary`}>
</td> {tariff.tariff_name}
<td>0</td> </span>
<td>0%</td> </td>
</tr> <td>{tariff.user_count}</td>
<tr> <td>{tariff.usage_percent.toFixed(2)}%</td>
<td> </tr>
<span className="badge badge-primary"> ))
ДЕМО ) : (
</span> <tr>
</td> <td colSpan={3} style={{ textAlign: 'center' }}>
<td>0</td> Нет данных
<td>0%</td> </td>
</tr> </tr>
<tr> )}
<td>
<span className="badge badge-primary">
ПРОФИ
</span>
</td>
<td>0</td>
<td>0%</td>
</tr>
</tbody> </tbody>
</table> </table>
</div> </div>
</div> </div>
) );
} }
+54 -47
View File
@@ -1,4 +1,37 @@
'use client'
import { useUserFilesTopStatistic } from '@/app/hooks/react-query/dashboard/useUserFilesTopStatistic';
import { convertBytes } from '@/app/lib/convertBytes';
export default function UsersTop() { export default function UsersTop() {
const { data, isLoading, isError } = useUserFilesTopStatistic();
if (isLoading) {
return (
<div className="admin-content users-top">
<div className="content-header">
<h3>Топ пользователей</h3>
</div>
<div className="content-body">
<div>Загрузка...</div>
</div>
</div>
);
}
if (isError) {
return (
<div className="admin-content users-top">
<div className="content-header">
<h3>Топ пользователей</h3>
</div>
<div className="content-body">
<div>Ошибка при загрузке данных</div>
</div>
</div>
);
}
return ( return (
<div className="admin-content users-top"> <div className="admin-content users-top">
<div className="content-header"> <div className="content-header">
@@ -14,55 +47,29 @@ export default function UsersTop() {
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{Array.isArray(data) && data.length > 0 ? (
<tr> data.map((user, index) => (
<td> <tr key={user.user_id || index}>
<div className="users-top-user">Сидоров Станислав Андреевич</div> <td>
<div className="users-top-mail">Info@pro-ctrl.ru</div> <div className="users-top-user">{user.full_name}</div>
</td> <div className="users-top-mail">
<td>0</td> ID: {user.user_id}
<td>0 MB</td> </div>
</tr> </td>
<td>{user.count}</td>
<tr> <td>{convertBytes(user.files_size)}</td>
<td> </tr>
<div className="users-top-user">Валерка</div> ))
<div className="users-top-mail">va@no-copy.ru</div> ) : (
</td> <tr>
<td>0</td> <td colSpan={3} style={{ textAlign: 'center' }}>
<td>0 MB</td> Нет данных
</tr> </td>
</tr>
<tr> )}
<td>
<div className="users-top-user">Игорь Викторович</div>
<div className="users-top-mail">igor@pro-ctrl.ru</div>
</td>
<td>0</td>
<td>0 MB</td>
</tr>
<tr>
<td>
<div className="users-top-user">Синицын Игорь Владимирович</div>
<div className="users-top-mail">admin@no-copy.ru</div>
</td>
<td>0</td>
<td>0 MB</td>
</tr>
<tr>
<td>
<div className="users-top-user">test test</div>
<div className="users-top-mail">test@no-copy.ru</div>
</td>
<td>0</td>
<td>0 MB</td>
</tr>
</tbody> </tbody>
</table> </table>
</div> </div>
</div> </div>
) );
} }