<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>