This commit is contained in:
87
src/app/admin/reports/BottomBar.svelte
Normal file
87
src/app/admin/reports/BottomBar.svelte
Normal file
@ -0,0 +1,87 @@
|
||||
<script lang="ts">
|
||||
import type { Report, ReportStatus, StrikeReasons } from './types.ts';
|
||||
import Input from '@components/input/Input.svelte';
|
||||
import Textarea from '@components/input/Textarea.svelte';
|
||||
import Select from '@components/input/Select.svelte';
|
||||
import TeamSearch from '@components/admin/search/TeamSearch.svelte';
|
||||
import { editReportStatus, getReportStatus } from '@app/admin/reports/actions.ts';
|
||||
import { confirmPopupState } from '@components/popup/ConfirmPopup.ts';
|
||||
|
||||
// types
|
||||
interface Props {
|
||||
strikeReasons: StrikeReasons;
|
||||
report: Report | null;
|
||||
}
|
||||
|
||||
// inputs
|
||||
let { strikeReasons, report }: Props = $props();
|
||||
|
||||
// states
|
||||
let status = $state<'open' | 'closed' | null>(null);
|
||||
let notice = $state<string | null>(null);
|
||||
let statement = $state<string | null>(null);
|
||||
|
||||
// consts
|
||||
const strikeReasonValues = strikeReasons.reduce(
|
||||
(prev, curr) => Object.assign(prev, { [curr.id]: `${curr.name} (${curr.weight})` }),
|
||||
{}
|
||||
);
|
||||
|
||||
// lifetime
|
||||
$effect(() => {
|
||||
if (!report) return;
|
||||
|
||||
getReportStatus(report).then((reportStatus) => {
|
||||
if (!reportStatus) return;
|
||||
|
||||
status = reportStatus.status;
|
||||
notice = reportStatus.notice;
|
||||
statement = reportStatus.statement;
|
||||
});
|
||||
});
|
||||
|
||||
// callbacks
|
||||
async function onSaveButtonClick() {
|
||||
$confirmPopupState = {
|
||||
title: 'Änderungen speichern?',
|
||||
message: 'Sollen die Änderungen am Report gespeichert werden?',
|
||||
onConfirm: async () =>
|
||||
editReportStatus(report!, {
|
||||
status: status,
|
||||
notice: notice,
|
||||
statement: statement,
|
||||
strikeId: null
|
||||
} as ReportStatus)
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="absolute bottom-2 bg-base-200 rounded-lg w-[calc(100%-1rem)] mx-2 flex px-6 py-4 gap-2"
|
||||
hidden={report === null}
|
||||
>
|
||||
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" onclick={() => (report = null)}>✕</button>
|
||||
<div class="w-[34rem]">
|
||||
<TeamSearch value={report?.reporter.name} label="Report Team" readonly mustMatch />
|
||||
<TeamSearch value={report?.reported?.name} label="Reportetes Team" />
|
||||
<Textarea bind:value={notice} label="Interne Notizen" rows={8} />
|
||||
</div>
|
||||
<div class="divider divider-horizontal"></div>
|
||||
<div class="w-full">
|
||||
<Input value={report?.reason} label="Grund" readonly dynamicWidth />
|
||||
<Textarea value={report?.body} label="Inhalt" readonly dynamicWidth rows={12} />
|
||||
</div>
|
||||
<div class="divider divider-horizontal"></div>
|
||||
<div class="flex flex-col w-[42rem]">
|
||||
<Textarea bind:value={statement} label="Öffentliche Report Antwort" dynamicWidth rows={5} />
|
||||
<Select
|
||||
values={{ open: 'In Bearbeitung', closed: 'Bearbeitet' }}
|
||||
defaultValue="Unbearbeitet"
|
||||
label="Bearbeitungsstatus"
|
||||
dynamicWidth
|
||||
/>
|
||||
<Select bind:value={status} values={strikeReasonValues} defaultValue="" label="Vergehen" dynamicWidth></Select>
|
||||
<div class="divider mt-0 mb-2"></div>
|
||||
<button class="btn mt-auto" onclick={onSaveButtonClick}>Speichern</button>
|
||||
</div>
|
||||
</div>
|
97
src/app/admin/reports/CreatePopup.svelte
Normal file
97
src/app/admin/reports/CreatePopup.svelte
Normal file
@ -0,0 +1,97 @@
|
||||
<script lang="ts">
|
||||
import Input from '@components/input/Input.svelte';
|
||||
import TeamSearch from '@components/admin/search/TeamSearch.svelte';
|
||||
import Textarea from '@components/input/Textarea.svelte';
|
||||
import Checkbox from '@components/input/Checkbox.svelte';
|
||||
import type { Report } from './types.ts';
|
||||
import { confirmPopupState } from '@components/popup/ConfirmPopup.ts';
|
||||
|
||||
// html bindings
|
||||
let modal: HTMLDialogElement;
|
||||
let modalForm: HTMLFormElement;
|
||||
|
||||
// types
|
||||
interface Props {
|
||||
open: boolean;
|
||||
|
||||
onSubmit: (report: Report) => void;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
// input
|
||||
let { open, onSubmit, onClose }: Props = $props();
|
||||
|
||||
// form
|
||||
let reason = $state<string | null>(null);
|
||||
let body = $state<string | null>(null);
|
||||
let editable = $state<boolean>(true);
|
||||
let reporter = $state<Report['reporter'] | null>(null);
|
||||
let reported = $state<Report['reported'] | null>(null);
|
||||
|
||||
let submitEnabled = $derived(!!(reason && reporter));
|
||||
|
||||
// lifecycle
|
||||
$effect(() => {
|
||||
if (open) modal.show();
|
||||
});
|
||||
|
||||
// callbacks
|
||||
async function onSaveButtonClick(e: Event) {
|
||||
e.preventDefault();
|
||||
$confirmPopupState = {
|
||||
title: 'Report erstellen',
|
||||
message: 'Bist du sicher, dass du den Report erstellen möchtest?',
|
||||
onConfirm: () => {
|
||||
modalForm.submit();
|
||||
onSubmit({
|
||||
id: -1,
|
||||
reason: reason!,
|
||||
body: body!,
|
||||
reporter: reporter!,
|
||||
reported: reported!,
|
||||
createdAt: editable ? null : new Date().toISOString(),
|
||||
status: null
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function onCancelButtonClick(e: Event) {
|
||||
e.preventDefault();
|
||||
modalForm.submit();
|
||||
}
|
||||
</script>
|
||||
|
||||
<dialog class="modal" bind:this={modal} onclose={() => setTimeout(() => onClose?.(), 300)}>
|
||||
<form method="dialog" class="modal-box" bind:this={modalForm}>
|
||||
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" onclick={onCancelButtonClick}>✕</button>
|
||||
<div class="space-y-5">
|
||||
<h3 class="text-xt font-geist font-bold">Neuer Report</h3>
|
||||
<div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<TeamSearch label="Report Team" required mustMatch onSubmit={(team) => (reporter = team)} />
|
||||
<TeamSearch label="Reportetes Team" mustMatch onSubmit={(team) => (reported = team)} />
|
||||
</div>
|
||||
<div class="grid grid-cols-1">
|
||||
<Input label="Grund" bind:value={reason} required dynamicWidth />
|
||||
<Textarea label="Inhalt" bind:value={body} rows={5} dynamicWidth />
|
||||
</div>
|
||||
<div class="grid grid-cols-1 mt-2">
|
||||
<Checkbox label="Report kann bearbeitet werden" bind:checked={editable} />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
class="btn btn-success"
|
||||
class:disabled={!submitEnabled}
|
||||
disabled={!submitEnabled}
|
||||
onclick={onSaveButtonClick}>Erstellen</button
|
||||
>
|
||||
<button class="btn btn-error" onclick={onCancelButtonClick}>Abbrechen</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<form method="dialog" class="modal-backdrop bg-[rgba(0,0,0,.3)]">
|
||||
<button class="!cursor-default">close</button>
|
||||
</form>
|
||||
</dialog>
|
50
src/app/admin/reports/Reports.svelte
Normal file
50
src/app/admin/reports/Reports.svelte
Normal file
@ -0,0 +1,50 @@
|
||||
<script lang="ts">
|
||||
import SortableTr from '@components/admin/table/SortableTr.svelte';
|
||||
import { reports } from './state.ts';
|
||||
import type { Report, StrikeReasons } from './types.ts';
|
||||
import SortableTh from '@components/admin/table/SortableTh.svelte';
|
||||
import BottomBar from '@app/admin/reports/BottomBar.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { getStrikeReasons } from '@app/admin/reports/actions.ts';
|
||||
|
||||
// states
|
||||
let strikeReasons = $state<StrikeReasons>([]);
|
||||
let activeReport = $state<Report | null>(null);
|
||||
|
||||
// lifecycle
|
||||
onMount(() => {
|
||||
getStrikeReasons().then((data) => (strikeReasons = data ?? []));
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="h-screen overflow-x-auto">
|
||||
<table class="table table-pin-rows">
|
||||
<thead>
|
||||
<SortableTr data={reports}>
|
||||
<SortableTh style="width: 5%">#</SortableTh>
|
||||
<SortableTh>Grund</SortableTh>
|
||||
<SortableTh>Report Team</SortableTh>
|
||||
<SortableTh>Reportetes Team</SortableTh>
|
||||
<SortableTh>Datum</SortableTh>
|
||||
<SortableTh>Bearbeitungsstatus</SortableTh>
|
||||
<SortableTh style="width: 5%"></SortableTh>
|
||||
</SortableTr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each $reports as report, i (report.id)}
|
||||
<tr class="hover:bg-base-200" onclick={() => (activeReport = report)}>
|
||||
<td>{i + 1}</td>
|
||||
<td>{report.reason}</td>
|
||||
<td>{report.reporter.name}</td>
|
||||
<td>{report.reported?.name}</td>
|
||||
<td>{report.createdAt}</td>
|
||||
<td>{report.status?.status}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{#key activeReport}
|
||||
<BottomBar {strikeReasons} report={activeReport} />
|
||||
{/key}
|
34
src/app/admin/reports/SidebarActions.svelte
Normal file
34
src/app/admin/reports/SidebarActions.svelte
Normal file
@ -0,0 +1,34 @@
|
||||
<script lang="ts">
|
||||
import Icon from '@iconify/svelte';
|
||||
import Input from '@components/input/Input.svelte';
|
||||
import { addReport, fetchReports } from '@app/admin/reports/actions.ts';
|
||||
import CreatePopup from '@app/admin/reports/CreatePopup.svelte';
|
||||
|
||||
// states
|
||||
let reporterUsernameFilter = $state<string | null>(null);
|
||||
let reportedUsernameFilter = $state<string | null>(null);
|
||||
|
||||
let newReportPopupOpen = $state(false);
|
||||
|
||||
// lifecycle
|
||||
$effect(() => {
|
||||
fetchReports(reporterUsernameFilter, reportedUsernameFilter);
|
||||
});
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<fieldset class="fieldset border border-base-content/50 rounded-box p-2">
|
||||
<legend class="fieldset-legend">Filter</legend>
|
||||
<Input bind:value={reporterUsernameFilter} label="Reporter Ersteller" />
|
||||
<Input bind:value={reportedUsernameFilter} label="Reporteter Spieler" />
|
||||
</fieldset>
|
||||
<div class="divider my-1"></div>
|
||||
<button class="btn btn-soft w-full" onclick={() => (newReportPopupOpen = true)}>
|
||||
<Icon icon="heroicons:plus-16-solid" />
|
||||
<span>Neuer Report</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#key newReportPopupOpen}
|
||||
<CreatePopup open={newReportPopupOpen} onSubmit={addReport} onClose={() => (newReportPopupOpen = false)} />
|
||||
{/key}
|
67
src/app/admin/reports/actions.ts
Normal file
67
src/app/admin/reports/actions.ts
Normal file
@ -0,0 +1,67 @@
|
||||
import { actions } from 'astro:actions';
|
||||
import { reports } from './state.ts';
|
||||
import { actionErrorPopup } from '@util/action.ts';
|
||||
import type { Report, ReportStatus } from './types.ts';
|
||||
|
||||
export async function fetchReports(reporterUsername: string | null, reportedUsername: string | null) {
|
||||
const { data, error } = await actions.report.reports({ reporter: reporterUsername, reported: reportedUsername });
|
||||
if (error) {
|
||||
actionErrorPopup(error);
|
||||
return;
|
||||
}
|
||||
|
||||
reports.set(data.reports);
|
||||
}
|
||||
|
||||
export async function addReport(report: Report) {
|
||||
const { data, error } = await actions.report.addReport({
|
||||
reason: report.reason,
|
||||
body: report.body,
|
||||
createdAt: report.createdAt,
|
||||
reporter: report.reporter.id,
|
||||
reported: report.reported?.id ?? null
|
||||
});
|
||||
if (error) {
|
||||
actionErrorPopup(error);
|
||||
return;
|
||||
}
|
||||
|
||||
reports.update((old) => {
|
||||
old.push(Object.assign(report, { id: data.id, status: null }));
|
||||
return old;
|
||||
});
|
||||
}
|
||||
|
||||
export async function getReportStatus(report: Report) {
|
||||
const { data, error } = await actions.report.reportStatus({ reportId: report.id });
|
||||
if (error) {
|
||||
actionErrorPopup(error);
|
||||
return;
|
||||
}
|
||||
|
||||
return data.reportStatus;
|
||||
}
|
||||
|
||||
export async function editReportStatus(report: Report, reportStatus: ReportStatus) {
|
||||
const { error } = await actions.report.editReportStatus({
|
||||
reportId: report.id,
|
||||
status: reportStatus.status,
|
||||
notice: reportStatus.notice,
|
||||
statement: reportStatus.statement,
|
||||
strikeId: reportStatus.strikeId
|
||||
});
|
||||
|
||||
if (error) {
|
||||
actionErrorPopup(error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getStrikeReasons() {
|
||||
const { data, error } = await actions.report.strikeReasons();
|
||||
if (error) {
|
||||
actionErrorPopup(error);
|
||||
return;
|
||||
}
|
||||
|
||||
return data.strikeReasons;
|
||||
}
|
4
src/app/admin/reports/state.ts
Normal file
4
src/app/admin/reports/state.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import { writable } from 'svelte/store';
|
||||
import type { Reports } from './types.ts';
|
||||
|
||||
export const reports = writable<Reports>([]);
|
14
src/app/admin/reports/types.ts
Normal file
14
src/app/admin/reports/types.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import type { ActionReturnType, actions } from 'astro:actions';
|
||||
|
||||
export type Reports = Exclude<ActionReturnType<typeof actions.report.reports>['data'], undefined>['reports'];
|
||||
export type Report = Reports[0];
|
||||
|
||||
export type ReportStatus = Exclude<
|
||||
Exclude<ActionReturnType<typeof actions.report.reportStatus>['data'], undefined>['reportStatus'],
|
||||
null
|
||||
>;
|
||||
|
||||
export type StrikeReasons = Exclude<
|
||||
ActionReturnType<typeof actions.report.strikeReasons>['data'],
|
||||
undefined
|
||||
>['strikeReasons'];
|
Reference in New Issue
Block a user