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 лет
+
+
+
+
+
`
+})
+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}}
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
Ты можешь оценить как живет сервер посмотрешь график онлайна или общие наигранное время на сервере
+
+
+
+
+
+
+ Статистика по
+
+
+ {{s.name}}
+
+
+
+
+ Количество минут
+
+
+ Разница в {{s}} минут
+
+
+
+
+ Количество дней
+
+
+ За {{s}} дней
+
+
+
+
+ Сервер
+
+
+ {{s.name}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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];
+ }
}