350 lines
10 KiB
Svelte
350 lines
10 KiB
Svelte
<script lang="ts">
|
|
import { fly } from 'svelte/transition';
|
|
import type { PageData } from './$types';
|
|
import type { Report } from '$lib/server/database';
|
|
import { browser } from '$app/environment';
|
|
import { env } from '$env/dynamic/public';
|
|
import Select from '$lib/components/Input/Select.svelte';
|
|
import Input from '$lib/components/Input/Input.svelte';
|
|
import Textarea from '$lib/components/Input/Textarea.svelte';
|
|
import { reportCount } from '$lib/stores';
|
|
import HeaderBar from './HeaderBar.svelte';
|
|
import { MagnifyingGlass, Plus, Share } from 'svelte-heros-v2';
|
|
import NewReportModal from './NewReportModal.svelte';
|
|
import { onDestroy, onMount } from 'svelte';
|
|
import { goto } from '$app/navigation';
|
|
import Search from '$lib/components/Input/Search.svelte';
|
|
import { usernameSuggestions } from '$lib/utils';
|
|
|
|
export let data: PageData;
|
|
|
|
let currentPageReports: (typeof Report.prototype.dataValues)[] = [];
|
|
let currentPageReportsRequest: Promise<any> = Promise.resolve();
|
|
let reportsPerPage = 50;
|
|
let reportPage = 0;
|
|
let reportFilter = { draft: false, status: null, reporter: null, reported: null };
|
|
let activeReport: typeof Report.prototype.dataValues | null = null;
|
|
|
|
async function fetchPageReports(
|
|
page: number,
|
|
filter: typeof reportFilter | { hash: string }
|
|
): Promise<typeof currentPageReports> {
|
|
if (!browser) return [];
|
|
|
|
const response = await fetch(`${env.PUBLIC_BASE_PATH}/admin/reports`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({ ...filter, limit: reportsPerPage, from: reportPage * page })
|
|
});
|
|
|
|
if (activeReport) {
|
|
activeReport = null;
|
|
await goto(window.location.href.split('#')[0], { replaceState: true });
|
|
}
|
|
|
|
return await response.json();
|
|
}
|
|
|
|
$: currentPageReportsRequest = fetchPageReports(reportPage, reportFilter).then((r) => {
|
|
currentPageReports = r;
|
|
});
|
|
|
|
async function openHashReport() {
|
|
if (!window.location.hash) return;
|
|
|
|
const requestedHash = window.location.hash.substring(1);
|
|
let report = currentPageReports.find((r) => r.url_hash === requestedHash);
|
|
if (!report) {
|
|
const hashReport = (await fetchPageReports(0, { hash: requestedHash }))[0];
|
|
if (hashReport) {
|
|
currentPageReports = [hashReport, ...currentPageReports];
|
|
report = hashReport;
|
|
} else {
|
|
await goto(window.location.href.split('#')[0], { replaceState: true });
|
|
return;
|
|
}
|
|
}
|
|
|
|
activeReport = report;
|
|
activeReport.originalStatus = report;
|
|
}
|
|
onMount(async () => {
|
|
await currentPageReportsRequest;
|
|
await openHashReport();
|
|
|
|
if (browser) window.addEventListener('hashchange', openHashReport);
|
|
});
|
|
onDestroy(() => {
|
|
if (browser) window.removeEventListener('hashchange', openHashReport);
|
|
});
|
|
|
|
async function updateActiveReport() {
|
|
await fetch(`${env.PUBLIC_BASE_PATH}/admin/reports`, {
|
|
method: 'PATCH',
|
|
body: JSON.stringify({
|
|
id: activeReport.id,
|
|
auditor: data.self?.id || -1,
|
|
notice: activeReport.notice || '',
|
|
statement: activeReport.statement || '',
|
|
status: activeReport.status,
|
|
reported: activeReport.reported?.uuid || null
|
|
})
|
|
});
|
|
}
|
|
|
|
let saveActiveReportChangesModal: HTMLDialogElement;
|
|
let newReportModal: HTMLDialogElement;
|
|
</script>
|
|
|
|
<div class="h-screen flex flex-row">
|
|
<div class="w-full flex flex-col overflow-scroll">
|
|
<HeaderBar bind:reportFilter />
|
|
<hr class="divider my-1 mx-8 border-none" />
|
|
<table class="table table-fixed h-fit">
|
|
<colgroup>
|
|
<col style="width: 20%" />
|
|
<col style="width: 15%" />
|
|
<col style="width: 15%" />
|
|
<col style="width: 20%" />
|
|
<col style="width: 15%" />
|
|
<col style="width: 15%" />
|
|
</colgroup>
|
|
<thead>
|
|
<tr>
|
|
<th>Grund</th>
|
|
<th>Ersteller</th>
|
|
<th>Reporteter User</th>
|
|
<th>Datum</th>
|
|
<th>Bearbeitungsstatus</th>
|
|
<th>Reportstatus</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{#each currentPageReports as report}
|
|
<tr
|
|
class="hover [&>*]:text-sm cursor-pointer"
|
|
class:bg-base-200={activeReport?.url_hash === report.url_hash}
|
|
on:click={() => {
|
|
goto(`${window.location.href.split('#')[0]}#${report.url_hash}`, {
|
|
replaceState: true
|
|
});
|
|
activeReport = report;
|
|
activeReport.originalStatus = report.status;
|
|
}}
|
|
>
|
|
<td title={report.subject}><div class="overflow-scroll">{report.subject}</div></td>
|
|
<td class="flex">
|
|
{report.reporter.username}
|
|
<button
|
|
class="pl-1"
|
|
title="Nach Ersteller filtern"
|
|
on:click|stopPropagation={() => (reportFilter.reporter = report.reporter.username)}
|
|
>
|
|
<MagnifyingGlass size="14" />
|
|
</button>
|
|
</td>
|
|
<td>
|
|
{report.reported?.username || ''}
|
|
{#if report.reported?.id}
|
|
<button
|
|
class="pl-1"
|
|
title="Nach Reportetem Spieler filtern"
|
|
on:click|stopPropagation={() =>
|
|
(reportFilter.reported = report.reported.username)}
|
|
>
|
|
<MagnifyingGlass size="14" />
|
|
</button>
|
|
{/if}
|
|
</td>
|
|
<td
|
|
>{new Intl.DateTimeFormat('de-DE', {
|
|
year: 'numeric',
|
|
month: '2-digit',
|
|
day: '2-digit',
|
|
hour: '2-digit',
|
|
minute: '2-digit'
|
|
}).format(new Date(report.createdAt))} Uhr</td
|
|
>
|
|
<td>
|
|
{report.status === 'none'
|
|
? 'Unbearbeitet'
|
|
: report.status === 'review'
|
|
? 'In Bearbeitung'
|
|
: report.status === 'reviewed'
|
|
? 'Bearbeitet'
|
|
: ''}
|
|
</td>
|
|
<td>{report.draft ? 'Entwurf' : 'Erstellt'}</td>
|
|
</tr>
|
|
{/each}
|
|
<tr>
|
|
<td colspan="100">
|
|
<div class="flex justify-center items-center">
|
|
<button class="btn btn-sm" on:click={() => newReportModal.show()}>
|
|
<Plus />
|
|
<span>Neuer Report</span>
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{#if activeReport}
|
|
<div
|
|
class="relative flex flex-col w-2/5 h-screen bg-base-200/50 px-4 py-6 overflow-scroll"
|
|
transition:fly={{ x: 200, duration: 200 }}
|
|
>
|
|
<div class="absolute right-2 top-2 flex justify-center">
|
|
<form class="dropdown dropdown-end">
|
|
<!-- svelte-ignore a11y-no-noninteractive-tabindex a11y-label-has-associated-control -->
|
|
<label tabindex="0" class="btn btn-sm btn-circle btn-ghost text-center">
|
|
<Share size="1rem" />
|
|
</label>
|
|
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
|
|
<ul
|
|
tabindex="0"
|
|
class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-max"
|
|
>
|
|
<li>
|
|
<button
|
|
on:click={() => {
|
|
navigator.clipboard.writeText(
|
|
`${window.location.protocol}//${window.location.host}${env.PUBLIC_BASE_PATH}/admin/reports#${activeReport.url_hash}`
|
|
);
|
|
}}
|
|
>
|
|
Internen Link kopieren
|
|
</button>
|
|
<button
|
|
on:click={() =>
|
|
navigator.clipboard.writeText(
|
|
`${window.location.protocol}//${window.location.host}${env.PUBLIC_BASE_PATH}/report/${activeReport.url_hash}`
|
|
)}>Öffentlichen Link kopieren</button
|
|
>
|
|
</li>
|
|
</ul>
|
|
</form>
|
|
<button
|
|
class="btn btn-sm btn-circle btn-ghost"
|
|
on:click={() => {
|
|
activeReport = null;
|
|
goto(window.location.href.split('#')[0], { replaceState: true });
|
|
}}>✕</button
|
|
>
|
|
</div>
|
|
<h3 class="font-roboto font-semibold text-2xl mb-2">Report</h3>
|
|
<div class="w-full">
|
|
<Input readonly={true} size="sm" value={activeReport.reporter.username} pickyWidth={false}>
|
|
<span slot="label">Reporter</span>
|
|
</Input>
|
|
<Search
|
|
size="sm"
|
|
suggestionRequired={true}
|
|
emptyAllowed={true}
|
|
searchSuggestionFunc={usernameSuggestions}
|
|
invalidMessage="Es können nur registrierte Spieler reportet werden"
|
|
label="Reporteter User"
|
|
inputValue={activeReport.reported?.username || ''}
|
|
on:submit={(e) =>
|
|
(activeReport.reported = {
|
|
...activeReport.reported,
|
|
username: e.detail.input,
|
|
uuid: e.detail.value
|
|
})}
|
|
/>
|
|
<Textarea readonly={true} rows={1} label="Report Grund" value={activeReport.subject} />
|
|
<Textarea readonly={true} rows={4} label="Report Details" value={activeReport.body} />
|
|
</div>
|
|
<div class="divider mx-4" />
|
|
<div>
|
|
<div
|
|
class="w-full"
|
|
title={activeReport.status === 'none'
|
|
? 'Zum Bearbeiten den Bearbeitungsstatus ändern'
|
|
: ''}
|
|
>
|
|
<Textarea
|
|
label="Interne Notizen"
|
|
readonly={activeReport.status === 'none'}
|
|
rows={1}
|
|
bind:value={activeReport.notice}
|
|
/>
|
|
</div>
|
|
<div
|
|
class="w-full"
|
|
title={activeReport.status === 'none'
|
|
? 'Zum Bearbeiten den Bearbeitungsstatus ändern'
|
|
: ''}
|
|
>
|
|
<Textarea
|
|
label="(Öffentliche) Report Antwort"
|
|
readonly={activeReport.status === 'none'}
|
|
rows={3}
|
|
bind:value={activeReport.statement}
|
|
/>
|
|
</div>
|
|
<Select label="Bearbeitungsstatus" size="sm" bind:value={activeReport.status}>
|
|
<option
|
|
value="none"
|
|
disabled={activeReport.auditor != null || activeReport.notice || activeReport.statement}
|
|
>Unbearbeitet</option
|
|
>
|
|
<option value="review">In Bearbeitung</option>
|
|
<option value="reviewed">Bearbeitet</option>
|
|
</Select>
|
|
</div>
|
|
<div class="self-end mt-auto pt-6 w-full flex justify-center">
|
|
<Input
|
|
type="submit"
|
|
value="Speichern"
|
|
on:click={() => saveActiveReportChangesModal.show()}
|
|
/>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
|
|
<dialog class="modal" bind:this={saveActiveReportChangesModal}>
|
|
<form method="dialog" class="modal-box">
|
|
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">✕</button>
|
|
<h3 class="font-roboto text-xl">Änderungen Speichern?</h3>
|
|
<div class="flex flex-row space-x-2 mt-6">
|
|
<Input
|
|
type="submit"
|
|
value="Speichern"
|
|
on:click={async () => {
|
|
await updateActiveReport();
|
|
if (activeReport.reported?.username && activeReport.reported?.id === undefined) {
|
|
activeReport.reported.id = -1;
|
|
} else {
|
|
activeReport.reported = undefined;
|
|
}
|
|
currentPageReports = [...currentPageReports];
|
|
if (activeReport.originalStatus !== 'reviewed' && activeReport.status === 'reviewed') {
|
|
$reportCount -= 1;
|
|
} else if (
|
|
activeReport.originalStatus === 'reviewed' &&
|
|
activeReport.status !== 'reviewed'
|
|
) {
|
|
$reportCount += 1;
|
|
}
|
|
}}
|
|
/>
|
|
<Input type="submit" value="Abbrechen" />
|
|
</div>
|
|
</form>
|
|
<form method="dialog" class="modal-backdrop bg-[rgba(0,0,0,.3)]">
|
|
<button>close</button>
|
|
</form>
|
|
</dialog>
|
|
|
|
<dialog class="modal" bind:this={newReportModal}>
|
|
<NewReportModal
|
|
on:submit={(e) => {
|
|
if (!e.detail.draft) $reportCount += 1;
|
|
currentPageReports = [e.detail, ...currentPageReports];
|
|
activeReport = currentPageReports[0];
|
|
}}
|
|
/>
|
|
</dialog>
|