add strike reason admin panel

This commit is contained in:
bytedream 2025-05-22 00:25:22 +02:00
parent 45f984e4da
commit a21d3d283a
11 changed files with 250 additions and 5 deletions

View File

@ -68,6 +68,39 @@ export const report = {
};
}
}),
addStrikeReason: defineAction({
input: z.object({
name: z.string(),
weight: z.number()
}),
handler: async (input, context) => {
Session.actionSessionFromCookies(context.cookies, Permissions.Reports);
return await db.addStrikeReason(input);
}
}),
editStrikeReason: defineAction({
input: z.object({
id: z.number(),
name: z.string(),
weight: z.number()
}),
handler: async (input, context) => {
Session.actionSessionFromCookies(context.cookies, Permissions.Reports);
await db.editStrikeReason(input);
}
}),
deleteStrikeReason: defineAction({
input: z.object({
id: z.number()
}),
handler: async (input, context) => {
Session.actionSessionFromCookies(context.cookies, Permissions.Reports);
await db.deleteStrikeReason(input);
}
}),
strikeReasons: defineAction({
handler: async (_, context) => {
Session.actionSessionFromCookies(context.cookies, Permissions.Reports);

View File

@ -1,7 +1,7 @@
<script lang="ts">
import Icon from '@iconify/svelte';
import CrudPopup from '@components/admin/popup/CrudPopup.svelte';
import { fetchBlockedUsers, addBlockedUser } from '@app/admin/usersBlocked/usersBlocked.ts';
import { fetchBlockedUsers, addBlockedUser } from '@app/admin/blockedUsers/blockedUsers.ts';
// states
let createPopupOpen = $state(false);

View File

@ -0,0 +1,36 @@
<script>
import Icon from '@iconify/svelte';
import CrudPopup from '@components/admin/popup/CrudPopup.svelte';
import { addStrikeReason, fetchStrikeReasons } from '@app/admin/strikeReasons/strikeReasons.js';
// states
let createPopupOpen = $state(false);
// lifecycle
$effect(() => {
fetchStrikeReasons();
});
</script>
<div>
<button class="btn btn-soft w-full" onclick={() => (createPopupOpen = true)}>
<Icon icon="heroicons:plus-16-solid" />
<span>Neuer Strikegrund</span>
</button>
</div>
<CrudPopup
texts={{
title: 'Strikegrund erstellen',
submitButtonTitle: 'Erstellen',
confirmPopupTitle: 'Strikegrund erstellen?',
confirmPopupMessage: 'Soll der Strikegrund erstellt werden?'
}}
target={null}
keys={[
[{ key: 'name', type: 'text', label: 'Name', options: { required: true, dynamicWidth: true } }],
[{ key: 'weight', type: 'number', label: 'Gewichtung', options: { required: true, dynamicWidth: true } }]
]}
onSubmit={addStrikeReason}
bind:open={createPopupOpen}
/>

View File

@ -0,0 +1,56 @@
<script lang="ts">
import DataTable from '@components/admin/table/DataTable.svelte';
import {
deleteStrikeReason,
editStrikeReason,
type StrikeReason,
strikeReasons
} from '@app/admin/strikeReasons/strikeReasons.ts';
import { confirmPopupState } from '@components/popup/ConfirmPopup.ts';
import CrudPopup from '@components/admin/popup/CrudPopup.svelte';
// state
let editPopupStrikeReason = $state(null);
let editPopupOpen = $derived(!!editPopupStrikeReason);
// lifecycle
$effect(() => {
if (!editPopupOpen) editPopupStrikeReason = null;
});
// callback
function onBlockedUserDelete(strikeReason: StrikeReason) {
$confirmPopupState = {
title: 'Nutzer entblockieren?',
message: 'Soll der Nutzer wirklich entblockiert werden?\nDieser kann sich danach wieder registrieren.',
onConfirm: () => deleteStrikeReason(strikeReason)
};
}
</script>
<DataTable
data={strikeReasons}
count={true}
keys={[
{ key: 'name', label: 'Name', width: 20 },
{ key: 'weight', label: 'Gewichtung', width: 70, sortable: true }
]}
onDelete={onBlockedUserDelete}
onEdit={(strikeReason) => (editPopupStrikeReason = strikeReason)}
/>
<CrudPopup
texts={{
title: 'Strikegrund bearbeiten',
submitButtonTitle: 'Speichern',
confirmPopupTitle: 'Änderungen speichern',
confirmPopupMessage: 'Sollen die Änderungen gespeichert werden?'
}}
target={editPopupStrikeReason}
keys={[
[{ key: 'name', type: 'text', label: 'Name', options: { required: true, dynamicWidth: true } }],
[{ key: 'weight', type: 'number', label: 'Gewichtung', options: { required: true, dynamicWidth: true } }]
]}
onSubmit={editStrikeReason}
bind:open={editPopupOpen}
/>

View File

@ -0,0 +1,55 @@
import { type ActionReturnType, actions } from 'astro:actions';
import { writable } from 'svelte/store';
import { actionErrorPopup } from '@util/action.ts';
import { addToWritableArray, deleteFromWritableArray, updateWritableArray } from '@util/state.ts';
// types
export type StrikeReasons = Exclude<
ActionReturnType<typeof actions.report.strikeReasons>['data'],
undefined
>['strikeReasons'];
export type StrikeReason = StrikeReasons[0];
// state
export const strikeReasons = writable<StrikeReasons>([]);
// actions
export async function fetchStrikeReasons() {
const { data, error } = await actions.report.strikeReasons();
if (error) {
actionErrorPopup(error);
return;
}
strikeReasons.set(data.strikeReasons);
}
export async function addStrikeReason(strikeReason: StrikeReason) {
const { data, error } = await actions.report.addStrikeReason(strikeReason);
if (error) {
actionErrorPopup(error);
return;
}
addToWritableArray(strikeReasons, Object.assign(strikeReason, { id: data.id }));
}
export async function editStrikeReason(strikeReason: StrikeReason) {
const { error } = await actions.report.editStrikeReason(strikeReason);
if (error) {
actionErrorPopup(error);
return;
}
updateWritableArray(strikeReasons, strikeReason, (t) => t.id == strikeReason.id);
}
export async function deleteStrikeReason(strikeReason: StrikeReason) {
const { error } = await actions.report.deleteStrikeReason(strikeReason);
if (error) {
actionErrorPopup(error);
return;
}
deleteFromWritableArray(strikeReasons, (t) => t.id == strikeReason.id);
}

View File

@ -42,6 +42,7 @@
| 'color'
| 'date'
| 'datetime-local'
| 'number'
| 'password'
| 'tel'
| 'team-search'
@ -56,6 +57,7 @@
['color']: {};
['date']: {};
['datetime-local']: {};
['number']: {};
['password']: {};
['tel']: {};
['team-search']: {};
@ -168,7 +170,7 @@
class:grid-cols-2={key.length === 2}
>
{#each key as k (k)}
{#if k.type === 'color' || k.type === 'date' || k.type === 'datetime-local' || k.type === 'tel' || k.type === 'text'}
{#if k.type === 'color' || k.type === 'date' || k.type === 'datetime-local' || k.type === 'number' || k.type === 'tel' || k.type === 'text'}
<Input
type={k.type}
bind:value={() => target[k.key] ?? k.default, (v) => onBindChange(k.key, v, k.options)}

View File

@ -3,7 +3,7 @@
interface Props {
id?: string;
type?: 'color' | 'date' | 'datetime-local' | 'tel' | 'text' | 'email';
type?: 'color' | 'date' | 'datetime-local' | 'number' | 'tel' | 'text' | 'email';
value?: string | null;
label?: string;
required?: boolean;

View File

@ -98,7 +98,17 @@ import {
} from './schema/feedback.ts';
import { addReport, type AddReportReq, getReports, type GetReportsReq, report } from './schema/report.ts';
import { DATABASE_URI } from 'astro:env/server';
import { type GetStrikeReasonsReq, getStrikeReasons, strikeReason } from '@db/schema/strikeReason.ts';
import {
type GetStrikeReasonsReq,
getStrikeReasons,
strikeReason,
type AddStrikeReasonReq,
addStrikeReason,
type EditStrikeReasonReq,
editStrikeReason,
type DeleteStrikeReasonReq,
deleteStrikeReason
} from '@db/schema/strikeReason.ts';
import { getStrikesByTeamId, type GetStrikesByTeamId, strike } from '@db/schema/strike.ts';
import {
editReportStatus,
@ -232,7 +242,12 @@ export class Database {
editReportStatus = (values: EditReportStatusReq) => editReportStatus(this.db, values);
/* strike reason */
addStrikeReason = (values: AddStrikeReasonReq) => addStrikeReason(this.db, values);
editStrikeReason = (values: EditStrikeReasonReq) => editStrikeReason(this.db, values);
deleteStrikeReason = (values: DeleteStrikeReasonReq) => deleteStrikeReason(this.db, values);
getStrikeReasons = (values: GetStrikeReasonsReq) => getStrikeReasons(this.db, values);
/* strikes */
getStrikesByTeamId = (values: GetStrikesByTeamId) => getStrikesByTeamId(this.db, values);
/* feedback */

View File

@ -1,6 +1,6 @@
import { int, mysqlTable, tinyint, varchar } from 'drizzle-orm/mysql-core';
import type { MySql2Database } from 'drizzle-orm/mysql2';
import { asc } from 'drizzle-orm';
import { asc, eq } from 'drizzle-orm';
type Database = MySql2Database<{ strikeReason: typeof strikeReason }>;
@ -10,8 +10,33 @@ export const strikeReason = mysqlTable('strike_reason', {
weight: tinyint('weight').notNull()
});
export type AddStrikeReasonReq = {
name: string;
weight: number;
};
export type EditStrikeReasonReq = typeof strikeReason.$inferSelect;
export type DeleteStrikeReasonReq = {
id: number;
};
export type GetStrikeReasonsReq = {};
export async function addStrikeReason(db: Database, values: AddStrikeReasonReq) {
const sr = await db.insert(strikeReason).values(values).$returningId();
return sr[0];
}
export async function editStrikeReason(db: Database, values: EditStrikeReasonReq) {
await db.update(strikeReason).set(values).where(eq(strikeReason.id, values.id));
}
export async function deleteStrikeReason(db: Database, values: DeleteStrikeReasonReq) {
await db.delete(strikeReason).where(eq(strikeReason.id, values.id));
}
export async function getStrikeReasons(db: Database, _values: GetStrikeReasonsReq) {
return db.select().from(strikeReason).orderBy(asc(strikeReason.weight));
}

View File

@ -46,6 +46,13 @@ const adminTabs = [
href: 'admin/reports',
name: 'Reports',
icon: 'heroicons:flag',
subTabs: [
{
href: 'admin/reports/reasons',
name: 'Strikegründe',
icon: 'heroicons:shield-exclamation'
}
],
enabled: session?.permissions.reports
},
{

View File

@ -0,0 +1,16 @@
---
import { Session } from '@util/session';
import { Permissions } from '@util/permissions';
import { BASE_PATH } from 'astro:env/server';
import AdminLayout from '@layouts/admin/AdminLayout.astro';
import SidebarActions from '@app/admin/strikeReasons/SidebarActions.svelte';
import StrikeReasons from '@app/admin/strikeReasons/StrikeReasons.svelte';
const session = Session.sessionFromCookies(Astro.cookies, Permissions.Reports);
if (!session) return Astro.redirect(`${BASE_PATH}/admin`);
---
<AdminLayout title="Reports">
<SidebarActions slot="actions" client:load />
<StrikeReasons client:load />
</AdminLayout>