Rollen neu geschnitten, Seitenzahl korrigiert, frühere Konten

Der Forums-Abgleich las von 25 Archivseiten nur zwei. WoltLab verlinkt in der
Blätterleiste nur die Nachbarseiten und führt die Gesamtzahl in einem eigenen
Element; ausgewertet wurden aber die Links. Alles jenseits von Seite zwei war
damit unsichtbar - etwa ein Antrag auf Seite vier, der deshalb nie ankam.

Die Rollen verlaufen jetzt zwischen erfassen und gewähren statt zwischen
Alltag und Verwaltung: Editoren erfassen Käufe und führen Datei-Import,
GExtension- und Forums-Abgleich samt Entscheidung über Anträge durch. Kulanz
zu vergeben bleibt Administratoren, ebenso das Auflösen unklarer Pakete, weil
dabei Kulanz entstehen kann.

Dazu lassen sich frühere Steam- und Forumskonten am Spieler hinterlegen.
Abgleich und Import prüfen sie mit, sodass ein Kontowechsel die Zuordnung
nicht abreißen lässt. Steam-Kennungen werden dabei auf dieselbe Form gebracht
wie die Hauptangabe, und eine Kennung kann nicht zwei Spielern gehören.
This commit is contained in:
2026-09-18 20:07:56 +02:00
parent 3344592c6e
commit 6c50d32d2b
24 changed files with 639 additions and 62 deletions
+1 -1
View File
@@ -101,7 +101,7 @@ export default function Dashboard() {
<div className="page-head">
<h1>Spieler</h1>
<div className="head-actions">
{can('ADMIN') && (
{can('EDITOR') && (
<button type="button" className="btn" disabled={syncing} onClick={() => void runSync()}>
{syncing ? 'Gleiche ab …' : 'GExtension abgleichen'}
</button>
+6 -6
View File
@@ -16,7 +16,7 @@ import type {
export default function Forum() {
const { can } = useSession();
const admin = can('ADMIN');
const darfEntscheiden = can('EDITOR');
const [threads, setThreads] = useState<ForumThread[]>([]);
const [notices, setNotices] = useState<ForumThread[]>([]);
@@ -88,7 +88,7 @@ export default function Forum() {
die hier mit passendem Forum-Namen erfasst sind.
</p>
</div>
{admin && (
{darfEntscheiden && (
<button type="button" className="btn btn-primary" disabled={busy} onClick={() => void scan()}>
{busy ? 'Lese Forum …' : 'Forum abgleichen'}
</button>
@@ -124,7 +124,7 @@ export default function Forum() {
<a className="btn btn-small btn-ghost" href={thread.url} target="_blank" rel="noreferrer">
Thread öffnen
</a>
{admin && (
{darfEntscheiden && (
<button type="button" className="btn btn-small" onClick={() => void dismiss(thread)}>
Erledigt
</button>
@@ -141,7 +141,7 @@ export default function Forum() {
{threads.length === 0 ? (
<p className="muted">
Keine offenen Anträge. {admin && 'Über „Forum abgleichen" nach neuen suchen.'}
Keine offenen Anträge. {darfEntscheiden && 'Über „Forum abgleichen" nach neuen suchen.'}
</p>
) : (
<ul className="cc-list">
@@ -170,12 +170,12 @@ export default function Forum() {
</p>
</div>
<div className="cc-actions">
{admin && !thread.player && (
{darfEntscheiden && !thread.player && (
<button type="button" className="btn btn-small" onClick={() => setAssigning(thread)}>
Spieler zuordnen
</button>
)}
{admin && (
{darfEntscheiden && (
<button
type="button"
className="btn btn-small btn-primary"
+20 -9
View File
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useState } from 'react';
import { Link, useNavigate, useParams } from 'react-router-dom';
import BalanceCards from '../components/BalanceCards';
import UnmatchedBox from '../components/UnmatchedBox';
import AliasBox from '../components/AliasBox';
import Modal from '../components/Modal';
import Notice from '../components/Notice';
import { ApiError, api } from '../lib/api';
@@ -192,14 +193,17 @@ export default function PlayerDetail() {
>
+ Redesign-Paket
</button>
<button
type="button"
className="btn"
disabled={busy}
onClick={() => setDialog({ type: 'addCredit', kind: 'kulanz' })}
>
+ Kulanz
</button>
{/* Kulanz vergibt das Team, sie steht in keinem Kauf - daher nur Admins. */}
{can('ADMIN') && (
<button
type="button"
className="btn"
disabled={busy}
onClick={() => setDialog({ type: 'addCredit', kind: 'kulanz' })}
>
+ Kulanz
</button>
)}
<button
type="button"
className="btn btn-primary"
@@ -208,7 +212,7 @@ export default function PlayerDetail() {
>
Neuer CC
</button>
{can('ADMIN') && detail.player.steamId && (
{can('EDITOR') && detail.player.steamId && (
<button
type="button"
className="btn"
@@ -340,6 +344,13 @@ export default function PlayerDetail() {
)}
</section>
<AliasBox
playerId={playerId}
aliases={detail.aliases ?? []}
canEdit={editor}
onChange={() => void load()}
/>
<section className="block">
<h2>Guthaben-Historie</h2>
<div className="table-wrap">