add option to create a report via the admin panel
All checks were successful
delpoy / build-and-deploy (push) Successful in 47s
All checks were successful
delpoy / build-and-deploy (push) Successful in 47s
This commit is contained in:
parent
61ea07d371
commit
3713c7eaba
78
src/lib/components/Input/Search.svelte
Normal file
78
src/lib/components/Input/Search.svelte
Normal file
@ -0,0 +1,78 @@
|
||||
<script lang="ts">
|
||||
export let id: string | null = null;
|
||||
export let value = '';
|
||||
export let suggestionRequired = false;
|
||||
export let searchSuggestionFunc: (
|
||||
input: string
|
||||
) => Promise<{ name: string; value: string }[]> = () => Promise.resolve([]);
|
||||
export let size: 'xs' | 'sm' | 'md' | 'lg' = 'md';
|
||||
export let label: string | null = null;
|
||||
export let required = false;
|
||||
|
||||
let inputValue: string;
|
||||
let searchSuggestions: { name: string; value: string }[] = [];
|
||||
$: if (!suggestionRequired) value = inputValue;
|
||||
</script>
|
||||
|
||||
<div class="relative">
|
||||
<div>
|
||||
{#if label}
|
||||
<label class="label" for={id}>
|
||||
<span class="label-text">
|
||||
{label}
|
||||
{#if required}
|
||||
<span class="text-red-700">*</span>
|
||||
{/if}
|
||||
</span>
|
||||
</label>
|
||||
{/if}
|
||||
<input
|
||||
type="search"
|
||||
class="input input-bordered w-full"
|
||||
class:input-xs={size === 'xs'}
|
||||
class:input-sm={size === 'sm'}
|
||||
class:input-md={size === 'md'}
|
||||
class:input-lg={size === 'md'}
|
||||
{id}
|
||||
{required}
|
||||
bind:value={inputValue}
|
||||
on:input={() => {
|
||||
value = '';
|
||||
searchSuggestionFunc(inputValue).then((v) => {
|
||||
searchSuggestions = v;
|
||||
let searchSuggestionValue = v.find((v) => v.name === inputValue);
|
||||
if (searchSuggestionValue !== undefined) {
|
||||
value = searchSuggestionValue.value;
|
||||
searchSuggestions = [];
|
||||
}
|
||||
});
|
||||
}}
|
||||
pattern={suggestionRequired ? `${value ? inputValue : 'a^'}` : null}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if inputValue && searchSuggestions.length !== 0}
|
||||
<ul class="absolute bg-base-200 w-full z-20 menu menu-sm rounded-box">
|
||||
{#each searchSuggestions as searchSuggestion}
|
||||
<li class="w-full text-left">
|
||||
<button
|
||||
class="block w-full overflow-hidden text-ellipsis whitespace-nowrap"
|
||||
title="{searchSuggestion.name} ({searchSuggestion.value})"
|
||||
on:click|preventDefault={() => {
|
||||
inputValue = searchSuggestion.name;
|
||||
value = searchSuggestion.value;
|
||||
searchSuggestions = [];
|
||||
}}>{searchSuggestion.name}</button
|
||||
>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if inputValue && searchSuggestions.length !== 0}
|
||||
<button
|
||||
class="absolute top-0 left-0 z-10 w-full h-full cursor-default"
|
||||
on:click={() => (searchSuggestions = [])}
|
||||
/>
|
||||
{/if}
|
@ -8,6 +8,9 @@
|
||||
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 { IconOutline } from 'svelte-heros-v2';
|
||||
import NewReportModal from './NewReportModal.svelte';
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
@ -47,29 +50,12 @@
|
||||
}
|
||||
|
||||
let saveActiveReportChangesModal: HTMLDialogElement;
|
||||
let newReportModal: HTMLDialogElement;
|
||||
</script>
|
||||
|
||||
<div class="h-screen flex flex-row">
|
||||
<div class="w-full flex flex-col">
|
||||
<form class="flex flex-row justify-center space-x-4 mx-4 my-2">
|
||||
<Input size="sm" placeholder="Alle" bind:value={reportFilter.reporter}>
|
||||
<span slot="label">Report Ersteller</span>
|
||||
</Input>
|
||||
<Input size="sm" placeholder="Alle" bind:value={reportFilter.reported}>
|
||||
<span slot="label">Reportete Spieler</span>
|
||||
</Input>
|
||||
<Select label="Bearbeitungsstatus" size="sm" bind:value={reportFilter.status}>
|
||||
<option value="none">Unbearbeitet</option>
|
||||
<option value="review">In Bearbeitung</option>
|
||||
<option value={null}>Unbearbeitet & In Bearbeitung</option>
|
||||
<option value="reviewed">Bearbeitet</option>
|
||||
</Select>
|
||||
<Select label="Reportstatus" size="sm" bind:value={reportFilter.draft}>
|
||||
<option value={false}>Erstellt</option>
|
||||
<option value={true}>Entwurf</option>
|
||||
<option value={null}>Erstellt & Entwurf</option>
|
||||
</Select>
|
||||
</form>
|
||||
<HeaderBar bind:reportFilter />
|
||||
<hr class="divider my-1 mx-8 border-none" />
|
||||
<table class="table table-fixed h-fit">
|
||||
<colgroup>
|
||||
@ -124,6 +110,16 @@
|
||||
<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()}>
|
||||
<IconOutline name="plus-outline" />
|
||||
<span>Neuer Report</span>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@ -162,7 +158,7 @@
|
||||
>
|
||||
<Textarea
|
||||
label="Interne Notizen"
|
||||
readonly={activeReport.auditor === null && activeReport.notice === null}
|
||||
readonly={activeReport.status === 'none'}
|
||||
rows={1}
|
||||
bind:value={activeReport.notice}
|
||||
/>
|
||||
@ -175,13 +171,15 @@
|
||||
>
|
||||
<Textarea
|
||||
label="(Öffentliche) Report Antwort"
|
||||
readonly={activeReport.auditor === null && activeReport.notice === null}
|
||||
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}
|
||||
<option
|
||||
value="none"
|
||||
disabled={activeReport.auditor != null || activeReport.notice || activeReport.statement}
|
||||
>Unbearbeitet</option
|
||||
>
|
||||
<option value="review">In Bearbeitung</option>
|
||||
@ -227,3 +225,13 @@
|
||||
<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>
|
||||
|
@ -5,6 +5,7 @@ import { Admin, Report, User } from '$lib/server/database';
|
||||
import type { Attributes } from 'sequelize';
|
||||
import { Op } from 'sequelize';
|
||||
import { env } from '$env/dynamic/private';
|
||||
import crypto from 'crypto';
|
||||
|
||||
export const POST = (async ({ request, cookies }) => {
|
||||
if (getSession(cookies, { permissions: [Permissions.ReportRead] }) == null) {
|
||||
@ -104,3 +105,49 @@ export const PATCH = (async ({ request, cookies }) => {
|
||||
|
||||
return new Response();
|
||||
}) satisfies RequestHandler;
|
||||
|
||||
export const PUT = (async ({ request, cookies, url }) => {
|
||||
if (getSession(cookies, { permissions: [Permissions.ReportWrite] }) == null) {
|
||||
return new Response(null, {
|
||||
status: 401
|
||||
});
|
||||
}
|
||||
|
||||
const data: {
|
||||
reporter: string;
|
||||
reported: string;
|
||||
reason: string;
|
||||
body: string | null;
|
||||
} = await request.json();
|
||||
|
||||
if (
|
||||
data.reporter == null ||
|
||||
data.reported == null ||
|
||||
data.reason == null ||
|
||||
data.body === undefined
|
||||
)
|
||||
return new Response(null, { status: 400 });
|
||||
|
||||
const reporter = await User.findOne({ where: { uuid: data.reporter } });
|
||||
const reported = await User.findOne({ where: { uuid: data.reported } });
|
||||
|
||||
if (reporter == null || reported == null) return new Response(null, { status: 400 });
|
||||
|
||||
const report = await Report.create({
|
||||
subject: data.reason,
|
||||
body: data.body,
|
||||
draft: data.body === null,
|
||||
status: 'none',
|
||||
url_hash: crypto.randomBytes(18).toString('hex'),
|
||||
completed: false,
|
||||
reporter_id: reporter.id,
|
||||
reported_id: reported.id
|
||||
});
|
||||
report.dataValues.reporter = await User.findOne({ where: { id: report.reporter_id } });
|
||||
report.dataValues.reported = await User.findOne({ where: { id: report.reported_id } });
|
||||
report.dataValues.auditor = null;
|
||||
|
||||
return new Response(JSON.stringify(report), {
|
||||
status: 201
|
||||
});
|
||||
}) satisfies RequestHandler;
|
||||
|
33
src/routes/admin/reports/HeaderBar.svelte
Normal file
33
src/routes/admin/reports/HeaderBar.svelte
Normal file
@ -0,0 +1,33 @@
|
||||
<script lang="ts">
|
||||
import Select from '$lib/components/Input/Select.svelte';
|
||||
import Input from '$lib/components/Input/Input.svelte';
|
||||
|
||||
export let reportFilter = {
|
||||
reporter: null,
|
||||
reported: null,
|
||||
status: null,
|
||||
draft: false
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="flex flex-row justify-center items-end">
|
||||
<form class="flex flex-row justify-center space-x-4 mx-4 my-2">
|
||||
<Input size="sm" placeholder="Alle" bind:value={reportFilter.reporter}>
|
||||
<span slot="label">Report Ersteller</span>
|
||||
</Input>
|
||||
<Input size="sm" placeholder="Alle" bind:value={reportFilter.reported}>
|
||||
<span slot="label">Reportete Spieler</span>
|
||||
</Input>
|
||||
<Select label="Bearbeitungsstatus" size="sm" bind:value={reportFilter.status}>
|
||||
<option value="none">Unbearbeitet</option>
|
||||
<option value="review">In Bearbeitung</option>
|
||||
<option value={null}>Unbearbeitet & In Bearbeitung</option>
|
||||
<option value="reviewed">Bearbeitet</option>
|
||||
</Select>
|
||||
<Select label="Reportstatus" size="sm" bind:value={reportFilter.draft}>
|
||||
<option value={false}>Erstellt</option>
|
||||
<option value={true}>Entwurf</option>
|
||||
<option value={null}>Erstellt & Entwurf</option>
|
||||
</Select>
|
||||
</form>
|
||||
</div>
|
137
src/routes/admin/reports/NewReportModal.svelte
Normal file
137
src/routes/admin/reports/NewReportModal.svelte
Normal file
@ -0,0 +1,137 @@
|
||||
<script lang="ts">
|
||||
import Input from '$lib/components/Input/Input.svelte';
|
||||
import { env } from '$env/dynamic/public';
|
||||
import Textarea from '$lib/components/Input/Textarea.svelte';
|
||||
import Search from '$lib/components/Input/Search.svelte';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
let reporter: string;
|
||||
let reported: string;
|
||||
let reason = '';
|
||||
let body = '';
|
||||
|
||||
async function usernameSuggestions(username: string): Promise<{ name: string; value: string }[]> {
|
||||
const response = await fetch(`${env.PUBLIC_BASE_PATH}/admin/users`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
limit: 6,
|
||||
search: username,
|
||||
slim: true
|
||||
})
|
||||
});
|
||||
const json: { username: string; uuid: string }[] = await response.json();
|
||||
return json.map((v) => {
|
||||
return { name: v.username, value: v.uuid };
|
||||
});
|
||||
}
|
||||
|
||||
async function newReport() {
|
||||
const response = await fetch(`${env.PUBLIC_BASE_PATH}/admin/reports`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({
|
||||
reporter: reporter,
|
||||
reported: reported,
|
||||
reason: reason,
|
||||
body: body || null
|
||||
})
|
||||
});
|
||||
if (response.ok) dispatch('submit', await response.json());
|
||||
}
|
||||
|
||||
let globalCloseForm: HTMLFormElement;
|
||||
|
||||
let reportForm: HTMLFormElement;
|
||||
let confirmDialog: HTMLDialogElement;
|
||||
</script>
|
||||
|
||||
<form method="dialog" class="modal-box" bind:this={reportForm}>
|
||||
<button
|
||||
class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"
|
||||
on:click|preventDefault={() => globalCloseForm.submit()}>✕</button
|
||||
>
|
||||
<h3 class="font-roboto text-xl">Neuer Report</h3>
|
||||
<div class="space-y-2 mt-2 px-1 max-h-[70vh] overflow-y-scroll">
|
||||
<div>
|
||||
<Search
|
||||
size="sm"
|
||||
suggestionRequired={true}
|
||||
searchSuggestionFunc={usernameSuggestions}
|
||||
label="Reporter"
|
||||
required={true}
|
||||
bind:value={reporter}
|
||||
/>
|
||||
<Search
|
||||
size="sm"
|
||||
suggestionRequired={true}
|
||||
searchSuggestionFunc={usernameSuggestions}
|
||||
label="Reporteter User"
|
||||
required={true}
|
||||
bind:value={reported}
|
||||
/>
|
||||
</div>
|
||||
<div class="divider mx-4 pt-3" />
|
||||
<Input type="text" bind:value={reason} required={true} pickyWidth={false}>
|
||||
<span slot="label">Report Grund</span>
|
||||
</Input>
|
||||
<div>
|
||||
<Textarea rows={4} label="Details über den Report Grund" bind:value={body} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-row space-x-2 mt-6">
|
||||
<Input
|
||||
type="submit"
|
||||
value="Erstellen"
|
||||
on:click={(e) => {
|
||||
if (reportForm.checkValidity()) {
|
||||
e.detail.preventDefault();
|
||||
confirmDialog.show();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Input
|
||||
type="submit"
|
||||
value="Abbrechen"
|
||||
on:click={(e) => {
|
||||
e.detail.preventDefault();
|
||||
globalCloseForm.submit();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
<form method="dialog" class="modal-backdrop bg-[rgba(0,0,0,.3)]" bind:this={globalCloseForm}>
|
||||
<button>close</button>
|
||||
</form>
|
||||
|
||||
<dialog class="modal" bind:this={confirmDialog}>
|
||||
<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 mb-2">Report Erstellen?</h3>
|
||||
{#if body}
|
||||
<p>
|
||||
Dadurch, dass bereits Details über den Report Grund hinzugefügt wurden, ist es nach dem
|
||||
Erstellen nicht mehr möglich, den Report Inhalt zu ändern
|
||||
</p>
|
||||
{:else}
|
||||
<p>
|
||||
Der Report wird als Entwurf gespeichert und kann nach dem Erstellen über den Report-Link
|
||||
bearbeitet werden
|
||||
</p>
|
||||
{/if}
|
||||
<div class="flex flex-row space-x-2 mt-6">
|
||||
<Input
|
||||
type="submit"
|
||||
value="Erstellen"
|
||||
on:click={async () => {
|
||||
await newReport();
|
||||
globalCloseForm.submit();
|
||||
}}
|
||||
/>
|
||||
<Input type="submit" value="Abbrechen" />
|
||||
</div>
|
||||
</form>
|
||||
<form method="dialog" class="modal-backdrop bg-[rgba(0,0,0,.3)]">
|
||||
<button>close</button>
|
||||
</form>
|
||||
</dialog>
|
@ -1,7 +1,8 @@
|
||||
import { getSession } from '$lib/server/session';
|
||||
import { Permissions } from '$lib/permissions';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
import { Admin, User } from '$lib/server/database';
|
||||
import { User } from '$lib/server/database';
|
||||
import { type Attributes, Op } from 'sequelize';
|
||||
|
||||
export const POST = (async ({ request, cookies }) => {
|
||||
if (getSession(cookies, { permissions: [Permissions.UserRead] }) == null) {
|
||||
@ -10,11 +11,28 @@ export const POST = (async ({ request, cookies }) => {
|
||||
});
|
||||
}
|
||||
|
||||
const data = await request.json();
|
||||
const limit = data['limit'] || 100;
|
||||
const from = data['from'] || 0;
|
||||
const data: {
|
||||
limit: number | null;
|
||||
from: number | null;
|
||||
search: string | null;
|
||||
slim: boolean | null;
|
||||
} = await request.json();
|
||||
|
||||
const users = await User.findAll({ offset: from, limit: limit });
|
||||
const usersFindOptions: Attributes<User> = {};
|
||||
if (data.search) {
|
||||
Object.assign(usersFindOptions, {
|
||||
[Op.or]: {
|
||||
username: { [Op.like]: `%${data.search}%` },
|
||||
uuid: { [Op.like]: `%${data.search}%` }
|
||||
}
|
||||
});
|
||||
}
|
||||
const users = await User.findAll({
|
||||
where: usersFindOptions,
|
||||
attributes: data.slim ? ['username', 'uuid'] : undefined,
|
||||
offset: data.from || 0,
|
||||
limit: data.limit || 100
|
||||
});
|
||||
|
||||
return new Response(JSON.stringify(users));
|
||||
}) satisfies RequestHandler;
|
||||
|
@ -1,11 +1,12 @@
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
import { Report, User } from '$lib/server/database';
|
||||
import * as crypto from 'crypto';
|
||||
import { env } from '$env/dynamic/public';
|
||||
|
||||
export const POST = (async ({ request, url }) => {
|
||||
const data: { reporter: string; reported: string; reason: string } = await request.json();
|
||||
|
||||
if (data.reporter == undefined || data.reported == undefined || data.reason == undefined)
|
||||
if (data.reporter == null || data.reported == null || data.reason == null)
|
||||
return new Response(null, { status: 400 });
|
||||
|
||||
const reporter = await User.findOne({ where: { uuid: data.reporter } });
|
||||
@ -25,7 +26,9 @@ export const POST = (async ({ request, url }) => {
|
||||
});
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({ url: `${url.toString().replace(/\/$/, '')}/${report.url_hash}` }),
|
||||
JSON.stringify({
|
||||
url: `${url.protocol}//${url.host}${env.PUBLIC_BASE_PATH || ''}/report/${report.url_hash}`
|
||||
}),
|
||||
{
|
||||
status: 201
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user