Compare commits
4 Commits
1d76895cbb
...
ee83bab77e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ee83bab77e | ||
|
|
bdad0ff0ae | ||
|
|
2bc5c41435 | ||
|
|
fb01fc7571 |
|
|
@ -1,6 +1,6 @@
|
||||||
package com.alttd.altitudeweb.mappers;
|
package com.alttd.altitudeweb.mappers;
|
||||||
|
|
||||||
import com.alttd.altitudeweb.database.luckperms.Player;
|
import com.alttd.altitudeweb.database.luckperms.PlayerWithGroup;
|
||||||
import com.alttd.altitudeweb.database.proxyplaytime.StaffPt;
|
import com.alttd.altitudeweb.database.proxyplaytime.StaffPt;
|
||||||
import com.alttd.altitudeweb.model.StaffPlaytimeDto;
|
import com.alttd.altitudeweb.model.StaffPlaytimeDto;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
@ -15,10 +15,10 @@ import java.util.concurrent.TimeUnit;
|
||||||
public final class StaffPtToStaffPlaytimeMapper {
|
public final class StaffPtToStaffPlaytimeMapper {
|
||||||
private record PlaytimeInfo(long totalPlaytime, long lastPlayed) {}
|
private record PlaytimeInfo(long totalPlaytime, long lastPlayed) {}
|
||||||
|
|
||||||
public List<StaffPlaytimeDto> map(List<StaffPt> sessions, List<Player> staffMembers, long from, long to) {
|
public List<StaffPlaytimeDto> map(List<StaffPt> sessions, List<PlayerWithGroup> staffMembers, long from, long to, HashMap<String, String> staffGroupsMap) {
|
||||||
Map<UUID, PlaytimeInfo> playtimeData = getUuidPlaytimeInfoMap(sessions, from, to);
|
Map<UUID, PlaytimeInfo> playtimeData = getUuidPlaytimeInfoMap(sessions, from, to);
|
||||||
|
|
||||||
for (Player staffMember : staffMembers) {
|
for (PlayerWithGroup staffMember : staffMembers) {
|
||||||
if (!playtimeData.containsKey(staffMember.uuid())) {
|
if (!playtimeData.containsKey(staffMember.uuid())) {
|
||||||
playtimeData.put(staffMember.uuid(), new PlaytimeInfo(0L, Long.MIN_VALUE));
|
playtimeData.put(staffMember.uuid(), new PlaytimeInfo(0L, Long.MIN_VALUE));
|
||||||
}
|
}
|
||||||
|
|
@ -28,14 +28,22 @@ public final class StaffPtToStaffPlaytimeMapper {
|
||||||
for (Map.Entry<UUID, PlaytimeInfo> entry : playtimeData.entrySet()) {
|
for (Map.Entry<UUID, PlaytimeInfo> entry : playtimeData.entrySet()) {
|
||||||
long lastPlayedMillis = entry.getValue().lastPlayed() == Long.MIN_VALUE ? 0L : entry.getValue().lastPlayed();
|
long lastPlayedMillis = entry.getValue().lastPlayed() == Long.MIN_VALUE ? 0L : entry.getValue().lastPlayed();
|
||||||
StaffPlaytimeDto dto = new StaffPlaytimeDto();
|
StaffPlaytimeDto dto = new StaffPlaytimeDto();
|
||||||
|
Optional<PlayerWithGroup> first = staffMembers.stream()
|
||||||
|
.filter(player -> player.uuid().equals(entry.getKey())).findFirst();
|
||||||
|
dto.setStaffMember(first.isPresent() ? first.get().username() : entry.getKey().toString());
|
||||||
dto.setStaffMember(staffMembers.stream()
|
dto.setStaffMember(staffMembers.stream()
|
||||||
.filter(player -> player.uuid().equals(entry.getKey()))
|
.filter(player -> player.uuid().equals(entry.getKey()))
|
||||||
.map(Player::username)
|
.map(PlayerWithGroup::username)
|
||||||
.findFirst()
|
.findFirst()
|
||||||
.orElse(entry.getKey().toString())
|
.orElse(entry.getKey().toString())
|
||||||
);
|
);
|
||||||
dto.setLastPlayed(OffsetDateTime.ofInstant(Instant.ofEpochMilli(lastPlayedMillis), ZoneOffset.UTC));
|
dto.setLastPlayed(OffsetDateTime.ofInstant(Instant.ofEpochMilli(lastPlayedMillis), ZoneOffset.UTC));
|
||||||
dto.setPlaytime((int) TimeUnit.MILLISECONDS.toMinutes(entry.getValue().totalPlaytime()));
|
dto.setPlaytime((int) TimeUnit.MILLISECONDS.toMinutes(entry.getValue().totalPlaytime()));
|
||||||
|
if (first.isPresent()) {
|
||||||
|
dto.setRole(staffGroupsMap.getOrDefault(first.get().group(), "Unknown"));
|
||||||
|
} else {
|
||||||
|
dto.setRole("Unknown");
|
||||||
|
}
|
||||||
results.add(dto);
|
results.add(dto);
|
||||||
}
|
}
|
||||||
return results;
|
return results;
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package com.alttd.altitudeweb.services.site;
|
||||||
|
|
||||||
import com.alttd.altitudeweb.database.Databases;
|
import com.alttd.altitudeweb.database.Databases;
|
||||||
import com.alttd.altitudeweb.database.luckperms.Player;
|
import com.alttd.altitudeweb.database.luckperms.Player;
|
||||||
|
import com.alttd.altitudeweb.database.luckperms.PlayerWithGroup;
|
||||||
import com.alttd.altitudeweb.database.luckperms.TeamMemberMapper;
|
import com.alttd.altitudeweb.database.luckperms.TeamMemberMapper;
|
||||||
import com.alttd.altitudeweb.database.proxyplaytime.StaffPlaytimeMapper;
|
import com.alttd.altitudeweb.database.proxyplaytime.StaffPlaytimeMapper;
|
||||||
import com.alttd.altitudeweb.database.proxyplaytime.StaffPt;
|
import com.alttd.altitudeweb.database.proxyplaytime.StaffPt;
|
||||||
|
|
@ -14,6 +15,7 @@ import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
@ -23,17 +25,30 @@ import java.util.stream.Collectors;
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class StaffPtService {
|
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 static HashMap<String, String> STAFF_GROUPS_MAP = new HashMap<>();
|
||||||
|
private final static String STAFF_GROUPS;
|
||||||
|
static {
|
||||||
|
STAFF_GROUPS_MAP.put("group.owner", "Owner");
|
||||||
|
STAFF_GROUPS_MAP.put("group.manager", "Manager");
|
||||||
|
STAFF_GROUPS_MAP.put("group.admin", "Admin");
|
||||||
|
STAFF_GROUPS_MAP.put("group.headmod", "Head Mod");
|
||||||
|
STAFF_GROUPS_MAP.put("group.moderator", "Moderator");
|
||||||
|
STAFF_GROUPS_MAP.put("group.trainee", "Trainee");
|
||||||
|
STAFF_GROUPS_MAP.put("group.developer", "Developer");
|
||||||
|
STAFF_GROUPS = STAFF_GROUPS_MAP.keySet().stream()
|
||||||
|
.map(group -> "'" + group + "'")
|
||||||
|
.collect(Collectors.joining(", "));
|
||||||
|
}
|
||||||
private final StaffPtToStaffPlaytimeMapper staffPtToStaffPlaytimeMapper;
|
private final StaffPtToStaffPlaytimeMapper staffPtToStaffPlaytimeMapper;
|
||||||
|
|
||||||
public Optional<List<StaffPlaytimeDto>> getStaffPlaytime(Instant from, Instant to) {
|
public Optional<List<StaffPlaytimeDto>> getStaffPlaytime(Instant from, Instant to) {
|
||||||
CompletableFuture<List<Player>> staffMembersFuture = new CompletableFuture<>();
|
CompletableFuture<List<PlayerWithGroup>> staffMembersFuture = new CompletableFuture<>();
|
||||||
CompletableFuture<List<StaffPt>> staffPlaytimeFuture = new CompletableFuture<>();
|
CompletableFuture<List<StaffPt>> staffPlaytimeFuture = new CompletableFuture<>();
|
||||||
Connection.getConnection(Databases.LUCK_PERMS)
|
Connection.getConnection(Databases.LUCK_PERMS)
|
||||||
.runQuery(sqlSession -> {
|
.runQuery(sqlSession -> {
|
||||||
log.debug("Loading staff members");
|
log.debug("Loading staff members");
|
||||||
try {
|
try {
|
||||||
List<Player> staffMemberList = sqlSession.getMapper(TeamMemberMapper.class)
|
List<PlayerWithGroup> staffMemberList = sqlSession.getMapper(TeamMemberMapper.class)
|
||||||
.getTeamMembersOfGroupList(STAFF_GROUPS);
|
.getTeamMembersOfGroupList(STAFF_GROUPS);
|
||||||
staffMembersFuture.complete(staffMemberList);
|
staffMembersFuture.complete(staffMemberList);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
|
@ -41,14 +56,14 @@ public class StaffPtService {
|
||||||
staffMembersFuture.completeExceptionally(e);
|
staffMembersFuture.completeExceptionally(e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
List<Player> staffMembers = staffMembersFuture.join().stream()
|
List<PlayerWithGroup> staffMembers = staffMembersFuture.join().stream()
|
||||||
.collect(Collectors.collectingAndThen(
|
.collect(Collectors.collectingAndThen(
|
||||||
Collectors.toMap(Player::uuid, player -> player, (player1, player2) -> player1),
|
Collectors.toMap(PlayerWithGroup::uuid, player -> player, (player1, player2) -> player1),
|
||||||
m -> new ArrayList<>(m.values())));
|
m -> new ArrayList<>(m.values())));
|
||||||
Connection.getConnection(Databases.PROXY_PLAYTIME)
|
Connection.getConnection(Databases.PROXY_PLAYTIME)
|
||||||
.runQuery(sqlSession -> {
|
.runQuery(sqlSession -> {
|
||||||
String staffUUIDs = staffMembers.stream()
|
String staffUUIDs = staffMembers.stream()
|
||||||
.map(Player::uuid)
|
.map(PlayerWithGroup::uuid)
|
||||||
.map(uuid -> "'" + uuid + "'")
|
.map(uuid -> "'" + uuid + "'")
|
||||||
.collect(Collectors.joining(","));
|
.collect(Collectors.joining(","));
|
||||||
log.debug("Loading staff playtime for group");
|
log.debug("Loading staff playtime for group");
|
||||||
|
|
@ -62,7 +77,7 @@ public class StaffPtService {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
List<StaffPt> join = staffPlaytimeFuture.join();
|
List<StaffPt> join = staffPlaytimeFuture.join();
|
||||||
|
HashMap<String, String> staffGroupsMap = new HashMap<>(STAFF_GROUPS_MAP);
|
||||||
return Optional.of(staffPtToStaffPlaytimeMapper.map(join, staffMembers, from.toEpochMilli(), to.toEpochMilli()));
|
return Optional.of(staffPtToStaffPlaytimeMapper.map(join, staffMembers, from.toEpochMilli(), to.toEpochMilli(), staffGroupsMap));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
package com.alttd.altitudeweb.database.luckperms;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public record PlayerWithGroup(String username, UUID uuid, String group) {
|
||||||
|
}
|
||||||
|
|
@ -22,16 +22,17 @@ public interface TeamMemberMapper {
|
||||||
|
|
||||||
@ConstructorArgs({
|
@ConstructorArgs({
|
||||||
@Arg(column = "username", javaType = String.class),
|
@Arg(column = "username", javaType = String.class),
|
||||||
@Arg(column = "uuid", javaType = UUID.class, typeHandler = UUIDTypeHandler.class)
|
@Arg(column = "uuid", javaType = UUID.class, typeHandler = UUIDTypeHandler.class),
|
||||||
|
@Arg(column = "group", javaType = String.class)
|
||||||
})
|
})
|
||||||
@Select("""
|
@Select("""
|
||||||
SELECT players.username, players.uuid
|
SELECT players.username, players.uuid, permissions.permission AS 'group'
|
||||||
FROM luckperms_user_permissions AS permissions
|
FROM luckperms_user_permissions AS permissions
|
||||||
INNER JOIN luckperms_players AS players ON players.uuid = permissions.uuid
|
INNER JOIN luckperms_players AS players ON players.uuid = permissions.uuid
|
||||||
WHERE permission IN (${groupPermissions})
|
WHERE permission IN (${groupPermissions})
|
||||||
AND server = 'global'
|
AND server = 'global'
|
||||||
AND world = 'global'
|
AND world = 'global'
|
||||||
""")
|
""")
|
||||||
List<Player> getTeamMembersOfGroupList(@Param("groupPermissions") String groupPermissions);
|
List<PlayerWithGroup> getTeamMembersOfGroupList(@Param("groupPermissions") String groupPermissions);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,19 +18,25 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<table mat-table [dataSource]="staffPt()" class="mat-elevation-z2 full-width">
|
<table mat-table [dataSource]="sortedStaffPt()" class="mat-elevation-z2 full-width" matSort (matSortChange)="sort.set($event)"
|
||||||
|
[matSortActive]="sort().active" [matSortDirection]="sort().direction" [matSortDisableClear]="true">
|
||||||
<ng-container matColumnDef="staff_member">
|
<ng-container matColumnDef="staff_member">
|
||||||
<th mat-header-cell *matHeaderCellDef> Staff Member</th>
|
<th mat-header-cell *matHeaderCellDef mat-sort-header="staff_member">Staff Member</th>
|
||||||
<td mat-cell *matCellDef="let row"> {{ row.staff_member }}</td>
|
<td mat-cell *matCellDef="let row"> {{ row.staff_member }}</td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container matColumnDef="playtime">
|
<ng-container matColumnDef="playtime">
|
||||||
<th mat-header-cell *matHeaderCellDef> Playtime</th>
|
<th mat-header-cell *matHeaderCellDef mat-sort-header="playtime">Playtime</th>
|
||||||
<td mat-cell *matCellDef="let row"
|
<td mat-cell *matCellDef="let row"
|
||||||
[style.color]="row.playtime < 420 ? 'red' : ''"> {{ minutesToHm(row.playtime) }}
|
[style.color]="row.playtime < 420 ? 'red' : ''"> {{ minutesToHm(row.playtime) }}
|
||||||
</td>
|
</td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="role">
|
||||||
|
<th mat-header-cell *matHeaderCellDef mat-sort-header="role">Rank</th>
|
||||||
|
<td mat-cell *matCellDef="let row"> {{ row.role }}</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.no-data td {
|
.no-data td {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,14 @@ import {MatTableModule} from '@angular/material/table';
|
||||||
import {MatButtonModule} from '@angular/material/button';
|
import {MatButtonModule} from '@angular/material/button';
|
||||||
import {MatIconModule} from '@angular/material/icon';
|
import {MatIconModule} from '@angular/material/icon';
|
||||||
import {MatTooltipModule} from '@angular/material/tooltip';
|
import {MatTooltipModule} from '@angular/material/tooltip';
|
||||||
|
import {MatSortModule, Sort} from '@angular/material/sort';
|
||||||
import {SiteService, StaffPlaytime} from '@api';
|
import {SiteService, StaffPlaytime} from '@api';
|
||||||
import {HeaderComponent} from '@header/header.component';
|
import {HeaderComponent} from '@header/header.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-staff-pt',
|
selector: 'app-staff-pt',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule, MatTableModule, MatButtonModule, MatIconModule, MatTooltipModule, HeaderComponent],
|
imports: [CommonModule, MatTableModule, MatButtonModule, MatIconModule, MatTooltipModule, MatSortModule, HeaderComponent],
|
||||||
templateUrl: './staff-pt.component.html',
|
templateUrl: './staff-pt.component.html',
|
||||||
styleUrl: './staff-pt.component.scss'
|
styleUrl: './staff-pt.component.scss'
|
||||||
})
|
})
|
||||||
|
|
@ -18,13 +19,31 @@ export class StaffPtComponent implements OnInit {
|
||||||
siteService = inject(SiteService);
|
siteService = inject(SiteService);
|
||||||
|
|
||||||
staffPt = signal<StaffPlaytime[]>([]);
|
staffPt = signal<StaffPlaytime[]>([]);
|
||||||
|
sort = signal<Sort>({active: 'playtime', direction: 'desc'} as Sort);
|
||||||
|
sortedStaffPt = computed<StaffPlaytime[]>(() => {
|
||||||
|
const data = [...this.staffPt()];
|
||||||
|
const {active, direction} = this.sort();
|
||||||
|
if (!direction || !active) return data;
|
||||||
|
const dir = direction === 'asc' ? 1 : -1;
|
||||||
|
return data.sort((a, b) => {
|
||||||
|
switch (active) {
|
||||||
|
case 'staff_member':
|
||||||
|
return a.staff_member.localeCompare(b.staff_member) * dir;
|
||||||
|
case 'role':
|
||||||
|
return a.role.localeCompare(b.role) * dir;
|
||||||
|
case 'playtime':
|
||||||
|
default:
|
||||||
|
return ((a.playtime ?? 0) - (b.playtime ?? 0)) * dir;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
weekStart = signal<Date>(this.getStartOfWeek(new Date()));
|
weekStart = signal<Date>(this.getStartOfWeek(new Date()));
|
||||||
weekEnd = computed(() => this.getEndOfWeek(this.weekStart()));
|
weekEnd = computed(() => this.getEndOfWeek(this.weekStart()));
|
||||||
|
|
||||||
todayStart = signal<Date>(this.startOfDay(new Date()));
|
todayStart = signal<Date>(this.startOfDay(new Date()));
|
||||||
|
|
||||||
displayedColumns = ['staff_member', 'playtime'];
|
displayedColumns = ['staff_member', 'playtime', 'role'];
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.loadCurrentWeek();
|
this.loadCurrentWeek();
|
||||||
|
|
|
||||||
|
|
@ -483,3 +483,42 @@ main .container {
|
||||||
.margin-auto {
|
.margin-auto {
|
||||||
margin: auto
|
margin: auto
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Angular Material global theme bridge to app CSS variables */
|
||||||
|
.theme-light, .theme-dark {
|
||||||
|
/* Ensure Material table text and headers follow theme font color */
|
||||||
|
.mat-mdc-table,
|
||||||
|
.mat-mdc-header-cell,
|
||||||
|
.mat-mdc-cell,
|
||||||
|
.mat-mdc-header-row,
|
||||||
|
.mat-mdc-row {
|
||||||
|
color: var(--font-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sort arrow color */
|
||||||
|
.mat-sort-header-arrow {
|
||||||
|
color: var(--font-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark theme overrides for Angular Material tables to ensure readable backgrounds */
|
||||||
|
.theme-dark {
|
||||||
|
/* Set a dark surface for the table container */
|
||||||
|
.mat-mdc-table {
|
||||||
|
background-color: var(--color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure rows and cells don't bring back white backgrounds from the prebuilt theme */
|
||||||
|
.mat-mdc-header-row,
|
||||||
|
.mat-mdc-row,
|
||||||
|
.mat-mdc-header-cell,
|
||||||
|
.mat-mdc-cell {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Optional: keep sort header arrow readable (already set in the bridge, but safe here) */
|
||||||
|
.mat-sort-header-arrow {
|
||||||
|
color: var(--font-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,11 @@ components:
|
||||||
$ref: '#/components/schemas/StaffPlaytime'
|
$ref: '#/components/schemas/StaffPlaytime'
|
||||||
StaffPlaytime:
|
StaffPlaytime:
|
||||||
type: object
|
type: object
|
||||||
|
required:
|
||||||
|
- staff_member
|
||||||
|
- playtime
|
||||||
|
- last_played
|
||||||
|
- role
|
||||||
properties:
|
properties:
|
||||||
staff_member:
|
staff_member:
|
||||||
type: string
|
type: string
|
||||||
|
|
@ -51,3 +56,6 @@ components:
|
||||||
type: string
|
type: string
|
||||||
format: date-time
|
format: date-time
|
||||||
description: Last played timestamp
|
description: Last played timestamp
|
||||||
|
role:
|
||||||
|
type: string
|
||||||
|
description: The role of the staff member
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user