Compare commits
4 Commits
2be79c180a
...
710771f5f7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
710771f5f7 | ||
|
|
edaebe9e4a | ||
|
|
e43cbbf9e4 | ||
|
|
8b0d2f9203 |
|
|
@ -51,6 +51,8 @@ public class SecurityConfig {
|
||||||
.requestMatchers("/api/form/**").authenticated()
|
.requestMatchers("/api/form/**").authenticated()
|
||||||
.requestMatchers("/api/login/getUsername").authenticated()
|
.requestMatchers("/api/login/getUsername").authenticated()
|
||||||
.requestMatchers("/api/mail/**").authenticated()
|
.requestMatchers("/api/mail/**").authenticated()
|
||||||
|
.requestMatchers("/api/site/vote").authenticated()
|
||||||
|
.requestMatchers("/api/site/get-staff-playtime/**").hasAuthority(PermissionClaimDto.HEAD_MOD.getValue())
|
||||||
.requestMatchers("/api/head_mod/**").hasAuthority(PermissionClaimDto.HEAD_MOD.getValue())
|
.requestMatchers("/api/head_mod/**").hasAuthority(PermissionClaimDto.HEAD_MOD.getValue())
|
||||||
.requestMatchers("/api/particles/**").hasAuthority(PermissionClaimDto.HEAD_MOD.getValue())
|
.requestMatchers("/api/particles/**").hasAuthority(PermissionClaimDto.HEAD_MOD.getValue())
|
||||||
.requestMatchers("/api/files/save/**").hasAuthority(PermissionClaimDto.HEAD_MOD.getValue())
|
.requestMatchers("/api/files/save/**").hasAuthority(PermissionClaimDto.HEAD_MOD.getValue())
|
||||||
|
|
|
||||||
|
|
@ -2,15 +2,21 @@ package com.alttd.altitudeweb.controllers.site;
|
||||||
|
|
||||||
import com.alttd.altitudeweb.api.SiteApi;
|
import com.alttd.altitudeweb.api.SiteApi;
|
||||||
import com.alttd.altitudeweb.controllers.data_from_auth.AuthenticatedUuid;
|
import com.alttd.altitudeweb.controllers.data_from_auth.AuthenticatedUuid;
|
||||||
|
import com.alttd.altitudeweb.model.StaffPlaytimeDto;
|
||||||
|
import com.alttd.altitudeweb.model.StaffPlaytimeListDto;
|
||||||
import com.alttd.altitudeweb.model.VoteDataDto;
|
import com.alttd.altitudeweb.model.VoteDataDto;
|
||||||
import com.alttd.altitudeweb.model.VoteStatsDto;
|
import com.alttd.altitudeweb.model.VoteStatsDto;
|
||||||
import com.alttd.altitudeweb.services.limits.RateLimit;
|
import com.alttd.altitudeweb.services.limits.RateLimit;
|
||||||
|
import com.alttd.altitudeweb.services.site.StaffPtService;
|
||||||
import com.alttd.altitudeweb.services.site.VoteService;
|
import com.alttd.altitudeweb.services.site.VoteService;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.OffsetDateTime;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
@ -23,6 +29,18 @@ public class SiteController implements SiteApi {
|
||||||
|
|
||||||
private final VoteService voteService;
|
private final VoteService voteService;
|
||||||
private final AuthenticatedUuid authenticatedUuid;
|
private final AuthenticatedUuid authenticatedUuid;
|
||||||
|
private final StaffPtService staffPtService;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ResponseEntity<StaffPlaytimeListDto> getStaffPlaytime(OffsetDateTime from, OffsetDateTime to) {
|
||||||
|
Optional<List<StaffPlaytimeDto>> staffPlaytimeDto = staffPtService.getStaffPlaytime(from.toInstant(), to.toInstant());
|
||||||
|
if (staffPlaytimeDto.isEmpty()) {
|
||||||
|
return ResponseEntity.noContent().build();
|
||||||
|
}
|
||||||
|
StaffPlaytimeListDto staffPlaytimeListDto = new StaffPlaytimeListDto();
|
||||||
|
staffPlaytimeListDto.addAll(staffPlaytimeDto.get());
|
||||||
|
return ResponseEntity.ok(staffPlaytimeListDto);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ResponseEntity<VoteDataDto> getVoteStats() {
|
public ResponseEntity<VoteDataDto> getVoteStats() {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
package com.alttd.altitudeweb.mappers;
|
||||||
|
|
||||||
|
import com.alttd.altitudeweb.database.luckperms.Player;
|
||||||
|
import com.alttd.altitudeweb.database.proxyplaytime.StaffPt;
|
||||||
|
import com.alttd.altitudeweb.model.StaffPlaytimeDto;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.OffsetDateTime;
|
||||||
|
import java.time.ZoneOffset;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public final class StaffPtToStaffPlaytimeMapper {
|
||||||
|
private record PlaytimeInfo(long totalPlaytime, long lastPlayed) {}
|
||||||
|
|
||||||
|
public List<StaffPlaytimeDto> map(List<StaffPt> sessions, List<Player> staffMembers, long from, long to) {
|
||||||
|
Map<UUID, PlaytimeInfo> playtimeData = getUuidPlaytimeInfoMap(sessions, from, to);
|
||||||
|
|
||||||
|
List<StaffPlaytimeDto> results = new ArrayList<>(playtimeData.size());
|
||||||
|
for (Map.Entry<UUID, PlaytimeInfo> entry : playtimeData.entrySet()) {
|
||||||
|
long lastPlayedMillis = entry.getValue().lastPlayed() == Long.MIN_VALUE ? 0L : entry.getValue().lastPlayed();
|
||||||
|
StaffPlaytimeDto dto = new StaffPlaytimeDto();
|
||||||
|
dto.setStaffMember(staffMembers.stream()
|
||||||
|
.filter(player -> player.uuid().equals(entry.getKey()))
|
||||||
|
.map(Player::username)
|
||||||
|
.findFirst()
|
||||||
|
.orElse(entry.getKey().toString())
|
||||||
|
);
|
||||||
|
dto.setLastPlayed(OffsetDateTime.ofInstant(Instant.ofEpochMilli(lastPlayedMillis), ZoneOffset.UTC));
|
||||||
|
dto.setPlaytime((int) TimeUnit.MILLISECONDS.toMinutes(entry.getValue().totalPlaytime()));
|
||||||
|
results.add(dto);
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<UUID, PlaytimeInfo> getUuidPlaytimeInfoMap(List<StaffPt> sessions, long from, long to) {
|
||||||
|
Map<UUID, PlaytimeInfo> playtimeData = new HashMap<>();
|
||||||
|
for (StaffPt session : sessions) {
|
||||||
|
long overlapStart = Math.max(session.sessionStart(), from);
|
||||||
|
long overlapEnd = Math.min(session.sessionEnd(), to);
|
||||||
|
if (overlapEnd <= overlapStart) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
PlaytimeInfo info = playtimeData.getOrDefault(session.uuid(), new PlaytimeInfo(0L, Long.MIN_VALUE));
|
||||||
|
long totalPlaytime = info.totalPlaytime() + (overlapEnd - overlapStart);
|
||||||
|
long lastPlayed = Math.max(info.lastPlayed(), overlapEnd);
|
||||||
|
playtimeData.put(session.uuid(), new PlaytimeInfo(totalPlaytime, lastPlayed));
|
||||||
|
}
|
||||||
|
return playtimeData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
package com.alttd.altitudeweb.services.site;
|
||||||
|
|
||||||
|
import com.alttd.altitudeweb.database.Databases;
|
||||||
|
import com.alttd.altitudeweb.database.luckperms.Player;
|
||||||
|
import com.alttd.altitudeweb.database.luckperms.TeamMemberMapper;
|
||||||
|
import com.alttd.altitudeweb.database.proxyplaytime.StaffPlaytimeMapper;
|
||||||
|
import com.alttd.altitudeweb.database.proxyplaytime.StaffPt;
|
||||||
|
import com.alttd.altitudeweb.mappers.StaffPtToStaffPlaytimeMapper;
|
||||||
|
import com.alttd.altitudeweb.model.StaffPlaytimeDto;
|
||||||
|
import com.alttd.altitudeweb.setup.Connection;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class StaffPtService {
|
||||||
|
private final static String STAFF_GROUPS = "'group.admin', 'group.developer', 'group.headmod', 'group.manager', 'group.moderator', 'group.owner', 'group.trainee'";
|
||||||
|
private final StaffPtToStaffPlaytimeMapper staffPtToStaffPlaytimeMapper;
|
||||||
|
|
||||||
|
public Optional<List<StaffPlaytimeDto>> getStaffPlaytime(Instant from, Instant to) {
|
||||||
|
CompletableFuture<List<Player>> staffMembersFuture = new CompletableFuture<>();
|
||||||
|
CompletableFuture<List<StaffPt>> staffPlaytimeFuture = new CompletableFuture<>();
|
||||||
|
Connection.getConnection(Databases.LUCK_PERMS)
|
||||||
|
.runQuery(sqlSession -> {
|
||||||
|
log.debug("Loading staff members");
|
||||||
|
try {
|
||||||
|
List<Player> staffMemberList = sqlSession.getMapper(TeamMemberMapper.class)
|
||||||
|
.getTeamMembersOfGroupList(STAFF_GROUPS);
|
||||||
|
staffMembersFuture.complete(staffMemberList);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to load staff members", e);
|
||||||
|
staffMembersFuture.completeExceptionally(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
List<Player> staffMembers = staffMembersFuture.join().stream()
|
||||||
|
.collect(Collectors.collectingAndThen(
|
||||||
|
Collectors.toMap(Player::uuid, player -> player, (player1, player2) -> player1),
|
||||||
|
m -> new ArrayList<>(m.values())));
|
||||||
|
Connection.getConnection(Databases.PROXY_PLAYTIME)
|
||||||
|
.runQuery(sqlSession -> {
|
||||||
|
String staffUUIDs = staffMembers.stream()
|
||||||
|
.map(Player::uuid)
|
||||||
|
.map(uuid -> "'" + uuid + "'")
|
||||||
|
.collect(Collectors.joining(","));
|
||||||
|
log.debug("Loading staff playtime for group");
|
||||||
|
try {
|
||||||
|
List<StaffPt> sessionsDuring = sqlSession.getMapper(StaffPlaytimeMapper.class)
|
||||||
|
.getSessionsDuring(from.toEpochMilli(), to.toEpochMilli(), staffUUIDs);
|
||||||
|
staffPlaytimeFuture.complete(sessionsDuring);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to load staff playtime", e);
|
||||||
|
staffPlaytimeFuture.completeExceptionally(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
List<StaffPt> join = staffPlaytimeFuture.join();
|
||||||
|
|
||||||
|
return Optional.of(staffPtToStaffPlaytimeMapper.map(join, staffMembers, from.toEpochMilli(), to.toEpochMilli()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -8,6 +8,7 @@ public enum Databases {
|
||||||
LUCK_PERMS("luckperms"),
|
LUCK_PERMS("luckperms"),
|
||||||
LITE_BANS("litebans"),
|
LITE_BANS("litebans"),
|
||||||
DISCORD("discordLink"),
|
DISCORD("discordLink"),
|
||||||
|
PROXY_PLAYTIME("proxyplaytime"),
|
||||||
VOTING_PLUGIN("votingplugin");
|
VOTING_PLUGIN("votingplugin");
|
||||||
|
|
||||||
private final String internalName;
|
private final String internalName;
|
||||||
|
|
|
||||||
|
|
@ -19,4 +19,19 @@ public interface TeamMemberMapper {
|
||||||
AND world = 'global'
|
AND world = 'global'
|
||||||
""")
|
""")
|
||||||
List<Player> getTeamMembers(@Param("groupPermission") String groupPermission);
|
List<Player> getTeamMembers(@Param("groupPermission") String groupPermission);
|
||||||
|
|
||||||
|
@ConstructorArgs({
|
||||||
|
@Arg(column = "username", javaType = String.class),
|
||||||
|
@Arg(column = "uuid", javaType = UUID.class, typeHandler = UUIDTypeHandler.class)
|
||||||
|
})
|
||||||
|
@Select("""
|
||||||
|
SELECT players.username, players.uuid
|
||||||
|
FROM luckperms_user_permissions AS permissions
|
||||||
|
INNER JOIN luckperms_players AS players ON players.uuid = permissions.uuid
|
||||||
|
WHERE permission IN (${groupPermissions})
|
||||||
|
AND server = 'global'
|
||||||
|
AND world = 'global'
|
||||||
|
""")
|
||||||
|
List<Player> getTeamMembersOfGroupList(@Param("groupPermissions") String groupPermissions);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
package com.alttd.altitudeweb.database.proxyplaytime;
|
||||||
|
|
||||||
|
import com.alttd.altitudeweb.type_handler.UUIDTypeHandler;
|
||||||
|
import org.apache.ibatis.annotations.Arg;
|
||||||
|
import org.apache.ibatis.annotations.ConstructorArgs;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
import org.apache.ibatis.annotations.Select;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public interface StaffPlaytimeMapper {
|
||||||
|
@ConstructorArgs({
|
||||||
|
@Arg(column = "uuid", javaType = UUID.class, typeHandler = UUIDTypeHandler.class),
|
||||||
|
@Arg(column = "serverName", javaType = String.class),
|
||||||
|
@Arg(column = "sessionStart", javaType = long.class),
|
||||||
|
@Arg(column = "sessionEnd", javaType = long.class)
|
||||||
|
})
|
||||||
|
@Select("""
|
||||||
|
SELECT uuid,
|
||||||
|
server_name AS serverName,
|
||||||
|
session_start AS sessionStart,
|
||||||
|
session_end AS sessionEnd
|
||||||
|
FROM sessions
|
||||||
|
WHERE session_end > #{from}
|
||||||
|
AND session_start < #{to}
|
||||||
|
AND uuid IN (${staffUUIDs})
|
||||||
|
ORDER BY uuid, session_start
|
||||||
|
""")
|
||||||
|
List<StaffPt> getSessionsDuring(@Param("from") long from,
|
||||||
|
@Param("to") long to,
|
||||||
|
@Param("staffUUIDs") String staffUUIDs);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
package com.alttd.altitudeweb.database.proxyplaytime;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public record StaffPt(UUID uuid, String serverName, long sessionStart, long sessionEnd) {
|
||||||
|
}
|
||||||
|
|
@ -37,6 +37,7 @@ public class Connection {
|
||||||
InitializeWebDb.init();
|
InitializeWebDb.init();
|
||||||
InitializeLiteBans.init();
|
InitializeLiteBans.init();
|
||||||
InitializeLuckPerms.init();
|
InitializeLuckPerms.init();
|
||||||
|
InitializeProxyPlaytime.init();
|
||||||
InitializeDiscord.init();
|
InitializeDiscord.init();
|
||||||
InitializeVotingPlugin.init();
|
InitializeVotingPlugin.init();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
package com.alttd.altitudeweb.setup;
|
||||||
|
|
||||||
|
import com.alttd.altitudeweb.database.Databases;
|
||||||
|
import com.alttd.altitudeweb.database.proxyplaytime.StaffPlaytimeMapper;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
public class InitializeProxyPlaytime {
|
||||||
|
|
||||||
|
protected static void init() {
|
||||||
|
log.info("Initializing ProxyPlaytime");
|
||||||
|
Connection.getConnection(Databases.PROXY_PLAYTIME, (configuration) -> {
|
||||||
|
configuration.addMapper(StaffPlaytimeMapper.class);
|
||||||
|
}).join();
|
||||||
|
log.debug("Initialized ProxyPlaytime");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -14,6 +14,14 @@ export const routes: Routes = [
|
||||||
requiredAuthorizations: ['SCOPE_head_mod']
|
requiredAuthorizations: ['SCOPE_head_mod']
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'staff-pt',
|
||||||
|
loadComponent: () => import('./pages/head-mod/staff-pt/staff-pt.component').then(m => m.StaffPtComponent),
|
||||||
|
canActivate: [AuthGuard],
|
||||||
|
data: {
|
||||||
|
requiredAuthorizations: ['SCOPE_head_mod']
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'map',
|
path: 'map',
|
||||||
loadComponent: () => import('./pages/features/map/map.component').then(m => m.MapComponent)
|
loadComponent: () => import('./pages/features/map/map.component').then(m => m.MapComponent)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
<div>
|
||||||
|
<app-header [current_page]="'staff-pt'" height="200px" background_image="/public/img/backgrounds/staff.png"
|
||||||
|
[overlay_gradient]="0.5">
|
||||||
|
|
||||||
|
</app-header>
|
||||||
|
<section class="darkmodeSection full-height">
|
||||||
|
<div class="staff-pt-container">
|
||||||
|
<div class="week-header">
|
||||||
|
<button mat-icon-button (click)="prevWeek()" matTooltip="Previous week" aria-label="Previous week">
|
||||||
|
<mat-icon>chevron_left</mat-icon>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="week-title">{{ weekLabel() }}</div>
|
||||||
|
|
||||||
|
<button mat-icon-button (click)="nextWeek()" [disabled]="!canGoNextWeek()"
|
||||||
|
matTooltip="Next week" aria-label="Next week">
|
||||||
|
<mat-icon>chevron_right</mat-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table mat-table [dataSource]="staffPt()" class="mat-elevation-z2 full-width">
|
||||||
|
<ng-container matColumnDef="staff_member">
|
||||||
|
<th mat-header-cell *matHeaderCellDef> Staff Member</th>
|
||||||
|
<td mat-cell *matCellDef="let row"> {{ row.staff_member }}</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="playtime">
|
||||||
|
<th mat-header-cell *matHeaderCellDef> Playtime (h:mm)</th>
|
||||||
|
<td mat-cell *matCellDef="let row"> {{ minutesToHm(row.playtime) }}</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="last_played">
|
||||||
|
<th mat-header-cell *matHeaderCellDef> Last Played</th>
|
||||||
|
<td mat-cell *matCellDef="let row"> {{ row.last_played | date:'medium' }}</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||||
|
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||||
|
|
||||||
|
@if (!staffPt()?.length) {
|
||||||
|
<tr class="no-data">
|
||||||
|
<td colspan="3">No data for this week.</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
.staff-pt-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.week-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.week-title {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.full-width {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-data td {
|
||||||
|
text-align: center;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
105
frontend/src/app/pages/head-mod/staff-pt/staff-pt.component.ts
Normal file
105
frontend/src/app/pages/head-mod/staff-pt/staff-pt.component.ts
Normal file
|
|
@ -0,0 +1,105 @@
|
||||||
|
import {Component, computed, inject, OnInit, signal} from '@angular/core';
|
||||||
|
import {CommonModule, DatePipe} from '@angular/common';
|
||||||
|
import {MatTableModule} from '@angular/material/table';
|
||||||
|
import {MatButtonModule} from '@angular/material/button';
|
||||||
|
import {MatIconModule} from '@angular/material/icon';
|
||||||
|
import {MatTooltipModule} from '@angular/material/tooltip';
|
||||||
|
import {SiteService, StaffPlaytime} from '@api';
|
||||||
|
import {HeaderComponent} from '@header/header.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-staff-pt',
|
||||||
|
standalone: true,
|
||||||
|
imports: [CommonModule, MatTableModule, MatButtonModule, MatIconModule, MatTooltipModule, DatePipe, HeaderComponent],
|
||||||
|
templateUrl: './staff-pt.component.html',
|
||||||
|
styleUrl: './staff-pt.component.scss'
|
||||||
|
})
|
||||||
|
export class StaffPtComponent implements OnInit {
|
||||||
|
siteService = inject(SiteService);
|
||||||
|
|
||||||
|
staffPt = signal<StaffPlaytime[]>([]);
|
||||||
|
|
||||||
|
weekStart = signal<Date>(this.getStartOfWeek(new Date('2024-01-01')));
|
||||||
|
weekEnd = computed(() => this.getEndOfWeek(this.weekStart()));
|
||||||
|
|
||||||
|
todayStart = signal<Date>(this.startOfDay(new Date()));
|
||||||
|
|
||||||
|
displayedColumns = ['staff_member', 'playtime', 'last_played'];
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.loadCurrentWeek();
|
||||||
|
}
|
||||||
|
|
||||||
|
private loadCurrentWeek() {
|
||||||
|
this.loadStaffData(this.weekStart(), this.weekEnd());
|
||||||
|
}
|
||||||
|
|
||||||
|
prevWeek() {
|
||||||
|
const prev = new Date(this.weekStart());
|
||||||
|
prev.setDate(prev.getDate() - 7);
|
||||||
|
prev.setHours(0, 0, 0, 0);
|
||||||
|
this.weekStart.set(prev);
|
||||||
|
this.loadCurrentWeek();
|
||||||
|
}
|
||||||
|
|
||||||
|
nextWeek() {
|
||||||
|
if (!this.canGoNextWeek()) return;
|
||||||
|
const next = new Date(this.weekStart());
|
||||||
|
next.setDate(next.getDate() + 7);
|
||||||
|
next.setHours(0, 0, 0, 0);
|
||||||
|
this.weekStart.set(next);
|
||||||
|
this.loadCurrentWeek();
|
||||||
|
}
|
||||||
|
|
||||||
|
canGoNextWeek(): boolean {
|
||||||
|
const nextWeekStart = new Date(this.weekStart());
|
||||||
|
nextWeekStart.setDate(nextWeekStart.getDate() + 7);
|
||||||
|
nextWeekStart.setHours(0, 0, 0, 0);
|
||||||
|
return nextWeekStart.getTime() <= this.todayStart().getTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
weekLabel(): string {
|
||||||
|
const start = this.weekStart();
|
||||||
|
const end = this.weekEnd();
|
||||||
|
|
||||||
|
const startFmt = start.toLocaleDateString(undefined, {month: 'short', day: 'numeric'});
|
||||||
|
const endFmt = end.toLocaleDateString(undefined, {month: 'short', day: 'numeric'});
|
||||||
|
const year = end.getFullYear();
|
||||||
|
return `Week ${startFmt} – ${endFmt}, ${year}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
minutesToHm(mins?: number): string {
|
||||||
|
if (mins == null) return '';
|
||||||
|
const h = Math.floor(mins / 60);
|
||||||
|
const m = mins % 60;
|
||||||
|
return `${h}:${m.toString().padStart(2, '0')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private loadStaffData(from: Date, to: Date) {
|
||||||
|
this.siteService.getStaffPlaytime(from.toISOString(), to.toISOString())
|
||||||
|
.subscribe({
|
||||||
|
next: data => this.staffPt.set(data ?? []),
|
||||||
|
error: err => console.error('Error getting staff playtime:', err)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private getStartOfWeek(date: Date): Date {
|
||||||
|
const d = new Date(date);
|
||||||
|
d.setDate(d.getDate() - d.getDay()); // Sunday start
|
||||||
|
d.setHours(0, 0, 0, 0);
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getEndOfWeek(start: Date): Date {
|
||||||
|
const d = new Date(start);
|
||||||
|
d.setDate(start.getDate() + 6);
|
||||||
|
d.setHours(23, 59, 59, 999);
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
|
||||||
|
private startOfDay(date: Date): Date {
|
||||||
|
const d = new Date(date);
|
||||||
|
d.setHours(0, 0, 0, 0);
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -144,6 +144,7 @@
|
||||||
<ul class="dropdown">
|
<ul class="dropdown">
|
||||||
@if (hasAccess([PermissionClaim.HEAD_MOD])) {
|
@if (hasAccess([PermissionClaim.HEAD_MOD])) {
|
||||||
<li class="nav_li"><a class="nav_link2" [routerLink]="['/particles']">Particles</a></li>
|
<li class="nav_li"><a class="nav_link2" [routerLink]="['/particles']">Particles</a></li>
|
||||||
|
<li class="nav_li"><a class="nav_link2" [routerLink]="['/staff-pt']">StaffPlaytime</a></li>
|
||||||
}
|
}
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
|
|
|
||||||
|
|
@ -159,6 +159,10 @@ time, mark, audio, video {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.full-height {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.centered {
|
.centered {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,11 @@ tasks.register< GenerateTask>("generateJavaApi") {
|
||||||
typeMappings.put("OffsetDateTime", "Instant")
|
typeMappings.put("OffsetDateTime", "Instant")
|
||||||
importMappings.put("java.time.OffsetDateTime", "java.time.Instant")
|
importMappings.put("java.time.OffsetDateTime", "java.time.Instant")
|
||||||
modelNameSuffix.set("Dto")
|
modelNameSuffix.set("Dto")
|
||||||
|
|
||||||
|
// Make generator use Java 8 time types and map date-time -> Instant
|
||||||
|
additionalProperties.set(mapOf("dateLibrary" to "java8"))
|
||||||
|
typeMappings.set(mapOf("date-time" to "Instant"))
|
||||||
|
importMappings.set(mapOf("Instant" to "java.time.Instant"))
|
||||||
generateModelTests.set(false)
|
generateModelTests.set(false)
|
||||||
generateModelDocumentation.set(false)
|
generateModelDocumentation.set(false)
|
||||||
generateApiTests.set(false)
|
generateApiTests.set(false)
|
||||||
|
|
|
||||||
|
|
@ -93,3 +93,5 @@ paths:
|
||||||
$ref: './schemas/forms/mail/mail.yml#/GetEmails'
|
$ref: './schemas/forms/mail/mail.yml#/GetEmails'
|
||||||
/api/site/vote:
|
/api/site/vote:
|
||||||
$ref: './schemas/site/vote.yml#/VoteStats'
|
$ref: './schemas/site/vote.yml#/VoteStats'
|
||||||
|
/api/site/get-staff-playtime/{from}/{to}:
|
||||||
|
$ref: './schemas/site/staff_pt.yml#/GetStaffPlaytime'
|
||||||
|
|
|
||||||
53
open_api/src/main/resources/schemas/site/staff_pt.yml
Normal file
53
open_api/src/main/resources/schemas/site/staff_pt.yml
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
GetStaffPlaytime:
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- site
|
||||||
|
summary: Get staff playtime for a specified duration
|
||||||
|
description: Get staff playtime for all staff members for a specified duration
|
||||||
|
operationId: getStaffPlaytime
|
||||||
|
parameters:
|
||||||
|
- $ref: '#/components/parameters/From'
|
||||||
|
- $ref: '#/components/parameters/To'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Staff playtime retrieved
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/StaffPlaytimeList'
|
||||||
|
components:
|
||||||
|
parameters:
|
||||||
|
From:
|
||||||
|
name: from
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
example: 2025-01-01T00:00:00.000Z
|
||||||
|
To:
|
||||||
|
name: to
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
example: 2025-01-07T23:59:59.999Z
|
||||||
|
schemas:
|
||||||
|
StaffPlaytimeList:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/StaffPlaytime'
|
||||||
|
StaffPlaytime:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
staff_member:
|
||||||
|
type: string
|
||||||
|
description: The name of the staff member
|
||||||
|
playtime:
|
||||||
|
type: integer
|
||||||
|
description: Total playtime for the specified duration in minutes
|
||||||
|
last_played:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
description: Last played timestamp
|
||||||
Loading…
Reference in New Issue
Block a user