diff --git a/src/app/app.module.ts b/src/app/app.module.ts index af205e7..26434bf 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -83,6 +83,9 @@ import {AuthDialogRequest} from "./pages/internal-components/dialogs/AuthDialogR import {SimpleActionDialog} from "./pages/internal-components/dialogs/simple-action-dialog.component"; import {MatTooltipModule} from "@angular/material/tooltip"; import {ServerPlayerViewer} from "./pages/servers-page/server-player-viewer"; +import {MatTabsModule} from "@angular/material/tabs"; +import {MatRadioModule} from "@angular/material/radio"; +import {UsertimeGraphComponent} from "./pages/internal-components/usertime.graph.component"; registerLocaleData(localeRu, "ru") @@ -134,7 +137,9 @@ registerLocaleData(localeRu, "ru") AdminMainPageComponent, FilesPageComponent, FilesUploader, - ServerPlayerViewer + ServerPlayerViewer, + //graph + UsertimeGraphComponent ], imports: [ BrowserModule, @@ -167,7 +172,9 @@ registerLocaleData(localeRu, "ru") MatDialogModule, MatStepperModule, MatCheckboxModule, - MatTooltipModule + MatTooltipModule, + MatTabsModule, + MatRadioModule ], providers: [ {provide: LOCALE_ID, useValue: 'ru' }, diff --git a/src/app/pages/internal-components/usertime.graph.component.ts b/src/app/pages/internal-components/usertime.graph.component.ts new file mode 100644 index 0000000..b3929e1 --- /dev/null +++ b/src/app/pages/internal-components/usertime.graph.component.ts @@ -0,0 +1,226 @@ +import {AfterViewInit, Component, Input, OnInit} from "@angular/core"; +import {SearchFilter} from "../../entities/search/SearchFilter"; +import {GraphService, PerPeriodStatistic} from "../../services/graph.service"; +import {Chart} from "chart.js/auto"; +import {MatSnackBar} from "@angular/material/snack-bar"; +import {ServerService} from "../../services/server.service"; +import {BaseUtils} from "../../utils/BaseUtils"; + +@Component({ + selector: "app-usertime-graph", + template: ` +
+ + За месяц + За год + За 10 лет + +
+
+ {{ usertimeChart.chart }} +
` +}) +export class UsertimeGraphComponent implements OnInit { + + usertimeChart: { + chart: Chart|null + period: 'day' | 'month' | 'year', + loading: boolean + } = { + chart: null, + period: "day", + loading: false + }; + + @Input("steam64") + steam64: string|null = null; + serverList: {name: string, server_id: string }[] = []; + timeDelimiter: number = 1; + + tooltipFooter = (items:any) => { + return "Наиграно: " + BaseUtils.formatSeconds(items.pop().parsed.y * this.timeDelimiter); + }; + + constructor(protected serverService: ServerService, + private graphService: GraphService, + private snack: MatSnackBar) { + } + + ngOnInit(): void { + const fill = (res:any) => { + const keys = res.data ? Object.keys(res.data) : []; + for (const key of keys) { + // @ts-ignore + this.serverList.push({name: res.data[key].name, server_id: key}); + } + } + + this.serverService.getServers().subscribe( + (res) => { + fill(res); + } + ) + } + + getServerName(server_id: string|null) { + try { + // @ts-ignore + return this.serverList.filter(s => s.server_id == server_id).pop().name; + } catch (e) { + return "Неизвестно"; + } + } + + usertimeTabChanged(event: any) { + if (event.index == 1) { + this.updateUsertimeGraph() + } + } + + updateUsertimeGraph() { + if (this.steam64 || true) { + if (this.usertimeChart.loading) return; + this.usertimeChart.loading = true; + + const filter: SearchFilter = new SearchFilter(); + if (this.steam64) + filter.addAccountToSearch(this.steam64); + let endDate = new Date(); + endDate.setUTCHours(23, 59, 59); + filter.addEndTimeToSearch(endDate.getTime()); + switch (this.usertimeChart.period) { + case "day": { + endDate.setDate(endDate.getDate() - 30); + break; + } + case "month": { + endDate.setDate(endDate.getDate() - 365); + break; + } + case "year": { + endDate.setDate(endDate.getDate() - (365*10)); + break; + } + } + filter.addBeginTimeToSearch(endDate); + let chartConfig = {type: 'bar', data: {}, options: { + plugins: { + title: { + display: true, + text: "Наиграно секунд по серверам" + }, + tooltip:{ + callbacks: { + footer: this.tooltipFooter + } + } + }, + responsive: true, + scales: { + x: { + stacked: true, + }, + y: { + stacked: true + } + } + } + }; + let chartData: {labels: string[], datasets: any[]} = { + labels: [],//даты + datasets: [] + /** + * { + * label: 'Dataset 1', + * data: Utils.numbers(NUMBER_CFG), + * backgroundColor: Utils.CHART_COLORS.red, + * }, + */ + }; + let valueOnServer: {[srv_name: string]: number[]} = {} + let srvList: string[] = []; + + let maxValue = 0; + + this.graphService.getUsertimeOnPeriod(filter).subscribe( + (objs) => { + objs.forEach((obj) => {if (obj.value > maxValue) maxValue = obj.value}) + if (maxValue >= 60) { + this.timeDelimiter = 60; + chartConfig.options.plugins.title.text = "Наиграно минут по серверам"; + } //po minutam + if (maxValue >= 60 * 60) { + this.timeDelimiter = 60 * 60; + chartConfig.options.plugins.title.text = "Наиграно часов по серверам"; + } //po chasam + if (maxValue >= 60 * 60 * 24) { + this.timeDelimiter = 60 * 60 * 24; + chartConfig.options.plugins.title.text = "Наиграно дней по серверам"; + }//po dnyam + + const groupByDate = objs.reduce((acc: {[date: string]: PerPeriodStatistic[]}, obj) => { + const key = obj.date; + if (!acc[key]) acc[key] = []; + acc[key].push(obj) + return acc; + }, {}); + chartData.labels = Object.keys(groupByDate); + //console.log(groupByDate); + + chartData.labels.forEach( + (date) => { + groupByDate[date].forEach((stat) => { + if (srvList.indexOf(stat.srv_id) == -1) + srvList.push(stat.srv_id) + }) + } + ); + //console.log(srvList) + srvList.forEach((srv) => { + valueOnServer[srv] = []; + }); + chartData.labels.forEach( + (date) => { + let filled: string[] = []; + groupByDate[date].forEach( + (obj) => { + valueOnServer[obj.srv_id].push(obj.value / this.timeDelimiter) + filled.push(obj.srv_id); + } + ) + srvList.forEach( + (srv) => { + if (filled.indexOf(srv) == -1) { + valueOnServer[srv].push(0); + } + } + ) + }, + ) + //console.log(valueOnServer) + Object.keys(valueOnServer).forEach( + (key) => { + chartData.datasets.push( + {label: this.getServerName(key) + " ("+key+")", data: valueOnServer[key]} + ) + } + ) + /// + chartConfig.data = chartData; + + if (this.usertimeChart.chart) + this.usertimeChart.chart.destroy(); + // @ts-ignore + this.usertimeChart.chart = new Chart('usertimeChart', chartConfig); + this.usertimeChart.loading = false; + }, (err) => {this.usertimeChart.loading = false; this.snack.open("Ошибка загрузка графика")} + ); + } else { + this.snack.open("Нельзя загрузить график т.к нельзя определить индификатор профиля"); + } + } +} diff --git a/src/app/pages/profile-page/profile-page.component.html b/src/app/pages/profile-page/profile-page.component.html index 5fe206c..55be0a2 100644 --- a/src/app/pages/profile-page/profile-page.component.html +++ b/src/app/pages/profile-page/profile-page.component.html @@ -109,12 +109,19 @@ Наиграно времени - - - -

{{gametime.key}} - {{gametime.value | ValueServerMapDate:false}} секунд

-
-
+ + + + + +

{{gametime.key}} - {{gametime.value | ValueServerMapDate:false}} секунд

+
+
+
+ + + +
diff --git a/src/app/pages/profile-page/profile-page.component.ts b/src/app/pages/profile-page/profile-page.component.ts index 4fe61a2..8421ba3 100644 --- a/src/app/pages/profile-page/profile-page.component.ts +++ b/src/app/pages/profile-page/profile-page.component.ts @@ -7,6 +7,11 @@ import {AuthService} from "../../services/auth.service"; import {ProfileRequestData} from "../../entities/profile/ProfileRequestData"; import {ActionService} from "../../services/action.service"; import {Ban} from "../../entities/ban/Ban"; +import {Chart} from "chart.js/auto"; +import {GraphService, PerPeriodStatistic} from "../../services/graph.service"; +import {SearchFilter} from "../../entities/search/SearchFilter"; +import _default from "chart.js/dist/plugins/plugin.legend"; +import labels = _default.defaults.labels; @Component({ selector: 'app-profile-page', @@ -17,6 +22,7 @@ export class ProfilePageComponent implements OnInit { profile: PlayerProfile|null = null; loading: boolean | null = null; + steam64: string|null = null; constructor(private route: ActivatedRoute, private playerService: PlayerService, @@ -25,9 +31,9 @@ export class ProfilePageComponent implements OnInit { public actionService: ActionService) { } ngOnInit(): void { - const steam64: string|null = this.route.snapshot.paramMap.get("steam64"); - if (steam64 != null) - this.loadPlayer(steam64); + this.steam64 = this.route.snapshot.paramMap.get("steam64"); + if (this.steam64 != null) + this.loadPlayer(this.steam64); else { } diff --git a/src/app/pages/servers-page/servers-page.component.html b/src/app/pages/servers-page/servers-page.component.html index 41f9c27..066b50a 100644 --- a/src/app/pages/servers-page/servers-page.component.html +++ b/src/app/pages/servers-page/servers-page.component.html @@ -111,48 +111,59 @@
-

График онлайна

-
-
- - Статистика по - - - {{s.name}} - - - - - Количество минут - - - Разница в {{s}} минут - - - - - Количество дней - - - За {{s}} дней - - - - - Сервер - - - {{s.name}} - - - -
-
- {{ chart }} -
- - -
- + + +
+

Ты можешь оценить как живет сервер посмотрешь график онлайна или общие наигранное время на сервере

+
+
+ +
+
+ + Статистика по + + + {{s.name}} + + + + + Количество минут + + + Разница в {{s}} минут + + + + + Количество дней + + + За {{s}} дней + + + + + Сервер + + + {{s.name}} + + + +
+
+ {{ chart }} +
+ + +
+
+ + + + +
diff --git a/src/app/pages/servers-page/servers-page.component.ts b/src/app/pages/servers-page/servers-page.component.ts index 547332f..b78bb65 100644 --- a/src/app/pages/servers-page/servers-page.component.ts +++ b/src/app/pages/servers-page/servers-page.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit } from '@angular/core'; +import {Component, OnInit, ViewChild} from '@angular/core'; import {WebsocketServersListenerService} from "../../services/websocket-servers-listener.service"; import {Server} from "../../entities/servers/Server"; import {KeyValue} from "@angular/common"; @@ -13,6 +13,7 @@ import {Chart} from "chart.js/auto"; import {Period} from "../statistic-page/statistic-page.component"; import {GraphService} from "../../services/graph.service"; import {ServerService} from "../../services/server.service"; +import {UsertimeGraphComponent} from "../internal-components/usertime.graph.component"; @Component({ selector: 'app-servers-page', @@ -32,6 +33,9 @@ export class ServersPageComponent implements OnInit { init: boolean = true; + @ViewChild("usertimeGraphComponent") + usertimeGraphComponent!: UsertimeGraphComponent; + periods:Period[] = [ {name: 'По дням', value: 'days'}, {name: 'По минутам', value: 'minutes'} @@ -152,4 +156,23 @@ export class ServersPageComponent implements OnInit { } }); } + + onTabChanged(event:any) { + switch (event.index) { + case 0: { + break; + } + case 1: { + this.getGraph(); + break; + } + case 2: { + this.usertimeGraphComponent.updateUsertimeGraph(); + break; + } + default: { + break; + } + } + } } diff --git a/src/app/services/graph.service.ts b/src/app/services/graph.service.ts index 0fe566c..b2683d7 100644 --- a/src/app/services/graph.service.ts +++ b/src/app/services/graph.service.ts @@ -3,6 +3,13 @@ import {HttpClient} from "@angular/common/http"; import {map, Observable} from "rxjs"; import {StatsOfPeakOfDay} from "../entities/graph/StatsOfPeakOfDay"; import {StatsOfPeakOfPerFiveMinutes} from "../entities/graph/StatsOfPeakOfPerFiveMinutes"; +import {SearchFilter} from "../entities/search/SearchFilter"; + +export interface PerPeriodStatistic { + value: number; + srv_id: string; + date: string; +} @Injectable({ providedIn: 'root' @@ -18,4 +25,9 @@ export class GraphService { public getOnlineStatsOfMinutes(minutes: number, limit: number, server_id: string):Observable { return this.http.get(`api/stats/graph/peak/of/minutes`, {params: {limit, server_id, minutes}}).pipe((map((res) => StatsOfPeakOfPerFiveMinutes.fromData(res)))) } + + public getUsertimeOnPeriod(filter: SearchFilter): Observable { + // @ts-ignore + return this.http.post(`api/profile/usertime/graph`, filter); + } } diff --git a/src/app/utils/BaseUtils.ts b/src/app/utils/BaseUtils.ts index 1934add..f89ee1e 100644 --- a/src/app/utils/BaseUtils.ts +++ b/src/app/utils/BaseUtils.ts @@ -2,4 +2,49 @@ export class BaseUtils { openUrlInNewWindow(url: string) { window.open(url, "_blank"); } + + /** AI SLOOOP + * Преобразует секунды в формат "дни ЧЧ:ММ:СС" (если дни > 0) + * или просто "ЧЧ:ММ:СС" (если дней нет). + * @param {number} totalSeconds - количество секунд (целое неотрицательное число) + * @returns {string} отформатированная строка + */ + static formatSeconds(totalSeconds: number) { + if (totalSeconds < 0) return "0 00:00:00"; + + const days = Math.floor(totalSeconds / 86400); + let remainder = totalSeconds % 86400; + + const hours = Math.floor(remainder / 3600); + remainder %= 3600; + + const minutes = Math.floor(remainder / 60); + const seconds = remainder % 60; + + // Форматируем часы, минуты, секунды с ведущими нулями + const timePart = `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; + + if (days === 0) { + return timePart.split(".")[0];; + } + + // Склонение слова "день" + let dayWord; + const lastDigit = days % 10; + const lastTwoDigits = days % 100; + + if (lastTwoDigits >= 11 && lastTwoDigits <= 14) { + dayWord = "дней"; + } else { + switch (lastDigit) { + case 1: dayWord = "день"; break; + case 2: + case 3: + case 4: dayWord = "дня"; break; + default: dayWord = "дней"; + } + } + + return `${days} ${dayWord} ${timePart}`.split(".")[0]; + } }