add delete to admin panel
All checks were successful
deploy / build-and-deploy (push) Successful in 21s

This commit is contained in:
bytedream 2025-05-21 20:53:53 +02:00
parent e47268111a
commit 45f984e4da
16 changed files with 207 additions and 50 deletions

View File

@ -33,6 +33,16 @@ export const admin = {
await db.editAdmin(input); await db.editAdmin(input);
} }
}), }),
deleteAdmin: defineAction({
input: z.object({
id: z.number()
}),
handler: async (input, context) => {
Session.actionSessionFromCookies(context.cookies, Permissions.Admin);
await db.deleteAdmin(input);
}
}),
admins: defineAction({ admins: defineAction({
handler: async (_, context) => { handler: async (_, context) => {
Session.actionSessionFromCookies(context.cookies, Permissions.Admin); Session.actionSessionFromCookies(context.cookies, Permissions.Admin);

View File

@ -105,6 +105,16 @@ export const team = {
}); });
} }
}), }),
deleteTeam: defineAction({
input: z.object({
id: z.number()
}),
handler: async (input, context) => {
Session.actionSessionFromCookies(context.cookies, Permissions.Users);
await db.deleteTeam(input);
}
}),
teams: defineAction({ teams: defineAction({
input: z.object({ input: z.object({
name: z.string().nullish(), name: z.string().nullish(),

View File

@ -70,6 +70,16 @@ export const user = {
}); });
} }
}), }),
deleteUser: defineAction({
input: z.object({
id: z.number()
}),
handler: async (input, context) => {
Session.actionSessionFromCookies(context.cookies, Permissions.Users);
await db.deleteUser(input);
}
}),
users: defineAction({ users: defineAction({
input: z.object({ input: z.object({
username: z.string().nullish(), username: z.string().nullish(),
@ -112,6 +122,16 @@ export const user = {
await db.editBlockedUser(input); await db.editBlockedUser(input);
} }
}), }),
deleteBlocked: defineAction({
input: z.object({
id: z.number()
}),
handler: async (input, context) => {
Session.actionSessionFromCookies(context.cookies, Permissions.Users);
await db.deleteBlockedUser(input);
}
}),
blocked: defineAction({ blocked: defineAction({
handler: async (_, context) => { handler: async (_, context) => {
Session.actionSessionFromCookies(context.cookies, Permissions.Users); Session.actionSessionFromCookies(context.cookies, Permissions.Users);

View File

@ -1,13 +1,23 @@
<script lang="ts"> <script lang="ts">
import BitBadge from '@components/input/BitBadge.svelte'; import BitBadge from '@components/input/BitBadge.svelte';
import { Permissions } from '@util/permissions.ts'; import { Permissions } from '@util/permissions.ts';
import { admins, editAdmin } from '@app/admin/admins/admins.ts'; import { type Admin, admins, deleteAdmin, editAdmin } from '@app/admin/admins/admins.ts';
import CrudPopup from '@components/admin/popup/CrudPopup.svelte'; import CrudPopup from '@components/admin/popup/CrudPopup.svelte';
import DataTable from '@components/admin/table/DataTable.svelte'; import DataTable from '@components/admin/table/DataTable.svelte';
import { confirmPopupState } from '@components/popup/ConfirmPopup.ts';
// state // state
let editPopupAdmin = $state(null); let editPopupAdmin = $state(null);
let editPopupOpen = $derived(!!editPopupAdmin); let editPopupOpen = $derived(!!editPopupAdmin);
// callback
function onAdminDelete(admin: Admin) {
$confirmPopupState = {
title: 'Admin löschen',
message: 'Soll der Admin wirklich gelöscht werden?',
onConfirm: () => deleteAdmin(admin)
};
}
</script> </script>
{#snippet permissionsBadge(permissions: number)} {#snippet permissionsBadge(permissions: number)}
@ -22,6 +32,7 @@
{ key: 'permissions', label: 'Berechtigungen', width: 60, transform: permissionsBadge } { key: 'permissions', label: 'Berechtigungen', width: 60, transform: permissionsBadge }
]} ]}
onEdit={(admin) => (editPopupAdmin = admin)} onEdit={(admin) => (editPopupAdmin = admin)}
onDelete={onAdminDelete}
/> />
<CrudPopup <CrudPopup

View File

@ -1,6 +1,7 @@
import { type ActionReturnType, actions } from 'astro:actions'; import { type ActionReturnType, actions } from 'astro:actions';
import { writable } from 'svelte/store'; import { writable } from 'svelte/store';
import { actionErrorPopup } from '@util/action.ts'; import { actionErrorPopup } from '@util/action.ts';
import { addToWritableArray, deleteFromWritableArray, updateWritableArray } from '@util/state.ts';
// types // types
export type Admins = Exclude<ActionReturnType<typeof actions.admin.admins>['data'], undefined>['admins']; export type Admins = Exclude<ActionReturnType<typeof actions.admin.admins>['data'], undefined>['admins'];
@ -27,10 +28,7 @@ export async function addAdmin(admin: Admin & { password: string }) {
return; return;
} }
admins.update((old) => { addToWritableArray(admins, Object.assign(admin, { id: data.id }));
old.push(Object.assign(admin, { id: data.id }));
return old;
});
} }
export async function editAdmin(admin: Admin & { password: string }) { export async function editAdmin(admin: Admin & { password: string }) {
@ -40,9 +38,15 @@ export async function editAdmin(admin: Admin & { password: string }) {
return; return;
} }
admins.update((old) => { updateWritableArray(admins, admin, (t) => t.id == admin.id);
const index = old.findIndex((a) => a.id == admin.id); }
old[index] = admin;
return old; export async function deleteAdmin(admin: Admin) {
}); const { error } = await actions.admin.deleteAdmin(admin);
if (error) {
actionErrorPopup(error);
return;
}
deleteFromWritableArray(admins, (t) => t.id == admin.id);
} }

View File

@ -1,7 +1,8 @@
<script lang="ts"> <script lang="ts">
import { addTeam, teams } from '@app/admin/teams/teams.ts'; import { addTeam, deleteTeam, type Team, teams } from '@app/admin/teams/teams.ts';
import DataTable from '@components/admin/table/DataTable.svelte'; import DataTable from '@components/admin/table/DataTable.svelte';
import CrudPopup from '@components/admin/popup/CrudPopup.svelte'; import CrudPopup from '@components/admin/popup/CrudPopup.svelte';
import { confirmPopupState } from '@components/popup/ConfirmPopup.ts';
// state // state
let editPopupTeam = $state(null); let editPopupTeam = $state(null);
@ -11,6 +12,15 @@
$effect(() => { $effect(() => {
if (!editPopupOpen) editPopupTeam = null; if (!editPopupOpen) editPopupTeam = null;
}); });
// callback
function onTeamDelete(team: Team) {
$confirmPopupState = {
title: 'Team löschen?',
message: 'Soll das Team wirklich gelöscht werden?',
onConfirm: () => deleteTeam(team)
};
}
</script> </script>
{#snippet color(value: string)} {#snippet color(value: string)}
@ -27,6 +37,7 @@
{ key: 'memberTwo.username', label: 'Spieler 2', width: 30 } { key: 'memberTwo.username', label: 'Spieler 2', width: 30 }
]} ]}
onEdit={(team) => (editPopupTeam = team)} onEdit={(team) => (editPopupTeam = team)}
onDelete={onTeamDelete}
/> />
<CrudPopup <CrudPopup

View File

@ -1,13 +1,12 @@
import { type ActionReturnType, actions } from 'astro:actions'; import { type ActionReturnType, actions } from 'astro:actions';
import { writable } from 'svelte/store'; import { writable } from 'svelte/store';
import { actionErrorPopup } from '@util/action.ts'; import { actionErrorPopup } from '@util/action.ts';
import { addToWritableArray, deleteFromWritableArray, updateWritableArray } from '@util/state.ts';
// types // types
export type Teams = Exclude<ActionReturnType<typeof actions.team.teams>['data'], undefined>['teams']; export type Teams = Exclude<ActionReturnType<typeof actions.team.teams>['data'], undefined>['teams'];
export type Team = Teams[0]; export type Team = Teams[0];
export type Users = Exclude<ActionReturnType<typeof actions.user.users>['data'], undefined>['users'];
// state // state
export const teams = writable<Teams>([]); export const teams = writable<Teams>([]);
@ -29,10 +28,7 @@ export async function addTeam(team: Team) {
return; return;
} }
teams.update((old) => { addToWritableArray(teams, Object.assign(team, { id: data.id }));
old.push(Object.assign(team, { id: data.id }));
return old;
});
} }
export async function editTeam(team: Team) { export async function editTeam(team: Team) {
@ -42,9 +38,15 @@ export async function editTeam(team: Team) {
return; return;
} }
teams.update((old) => { updateWritableArray(teams, team, (t) => t.id == team.id);
const index = old.findIndex((a) => a.id == team.id); }
old[index] = team;
return old; export async function deleteTeam(team: Team) {
}); const { error } = await actions.team.deleteTeam(team);
if (error) {
actionErrorPopup(error);
return;
}
deleteFromWritableArray(teams, (t) => t.id == team.id);
} }

View File

@ -1,7 +1,8 @@
<script lang="ts"> <script lang="ts">
import DataTable from '@components/admin/table/DataTable.svelte'; import DataTable from '@components/admin/table/DataTable.svelte';
import { editUser, users } from '@app/admin/users/users.ts'; import { deleteUser, editUser, type User, users } from '@app/admin/users/users.ts';
import CrudPopup from '@components/admin/popup/CrudPopup.svelte'; import CrudPopup from '@components/admin/popup/CrudPopup.svelte';
import { confirmPopupState } from '@components/popup/ConfirmPopup.ts';
// state // state
let editPopupUser = $state(null); let editPopupUser = $state(null);
@ -11,6 +12,15 @@
$effect(() => { $effect(() => {
if (!editPopupOpen) editPopupUser = null; if (!editPopupOpen) editPopupUser = null;
}); });
// callback
function onUserDelete(user: User) {
$confirmPopupState = {
title: 'Nutzer löschen?',
message: 'Soll der Nutzer wirklich gelöscht werden?',
onConfirm: () => deleteUser(user)
};
}
</script> </script>
<DataTable <DataTable
@ -25,6 +35,7 @@
{ key: 'uuid', label: 'UUID', width: 23 } { key: 'uuid', label: 'UUID', width: 23 }
]} ]}
onEdit={(user) => (editPopupUser = user)} onEdit={(user) => (editPopupUser = user)}
onDelete={onUserDelete}
/> />
<CrudPopup <CrudPopup

View File

@ -1,6 +1,7 @@
import { writable } from 'svelte/store'; import { writable } from 'svelte/store';
import { type ActionReturnType, actions } from 'astro:actions'; import { type ActionReturnType, actions } from 'astro:actions';
import { actionErrorPopup } from '@util/action.ts'; import { actionErrorPopup } from '@util/action.ts';
import { addToWritableArray, deleteFromWritableArray, updateWritableArray } from '@util/state.ts';
// types // types
export type Users = Exclude<ActionReturnType<typeof actions.user.users>['data'], undefined>['users']; export type Users = Exclude<ActionReturnType<typeof actions.user.users>['data'], undefined>['users'];
@ -34,10 +35,7 @@ export async function addUser(user: User) {
return; return;
} }
users.update((old) => { addToWritableArray(users, user);
old.push(Object.assign(user, { id: data.id }));
return old;
});
} }
export async function editUser(user: User) { export async function editUser(user: User) {
@ -55,9 +53,15 @@ export async function editUser(user: User) {
return; return;
} }
users.update((old) => { updateWritableArray(users, user, (t) => t.id == user.id);
const index = old.findIndex((a) => a.id == user.id); }
old[index] = user;
return old; export async function deleteUser(user: User) {
}); const { error } = await actions.user.deleteUser({ id: user.id });
if (error) {
actionErrorPopup(error);
return;
}
deleteFromWritableArray(users, (t) => t.id == user.id);
} }

View File

@ -1,7 +1,13 @@
<script lang="ts"> <script lang="ts">
import { blockedUsers, editBlockedUser } from '@app/admin/usersBlocked/usersBlocked.ts'; import {
type BlockedUser,
blockedUsers,
deleteBlockedUser,
editBlockedUser
} from '@app/admin/usersBlocked/usersBlocked.ts';
import CrudPopup from '@components/admin/popup/CrudPopup.svelte'; import CrudPopup from '@components/admin/popup/CrudPopup.svelte';
import DataTable from '@components/admin/table/DataTable.svelte'; import DataTable from '@components/admin/table/DataTable.svelte';
import { confirmPopupState } from '@components/popup/ConfirmPopup.ts';
// state // state
let blockedUserEditPopupBlockedUser = $state(null); let blockedUserEditPopupBlockedUser = $state(null);
@ -11,6 +17,15 @@
$effect(() => { $effect(() => {
if (!blockedUserEditPopupOpen) blockedUserEditPopupBlockedUser = null; if (!blockedUserEditPopupOpen) blockedUserEditPopupBlockedUser = null;
}); });
// callback
function onBlockedUserDelete(blockedUser: BlockedUser) {
$confirmPopupState = {
title: 'Nutzer entblockieren?',
message: 'Soll der Nutzer wirklich entblockiert werden?\nDieser kann sich danach wieder registrieren.',
onConfirm: () => deleteBlockedUser(blockedUser)
};
}
</script> </script>
<DataTable <DataTable
@ -21,6 +36,7 @@
{ key: 'comment', label: 'Kommentar', width: 70 } { key: 'comment', label: 'Kommentar', width: 70 }
]} ]}
onEdit={(blockedUser) => (blockedUserEditPopupBlockedUser = blockedUser)} onEdit={(blockedUser) => (blockedUserEditPopupBlockedUser = blockedUser)}
onDelete={onBlockedUserDelete}
/> />
<CrudPopup <CrudPopup

View File

@ -1,6 +1,7 @@
import { type ActionReturnType, actions } from 'astro:actions'; import { type ActionReturnType, actions } from 'astro:actions';
import { writable } from 'svelte/store'; import { writable } from 'svelte/store';
import { actionErrorPopup } from '@util/action.ts'; import { actionErrorPopup } from '@util/action.ts';
import { addToWritableArray, deleteFromWritableArray, updateWritableArray } from '@util/state.ts';
// types // types
export type BlockedUsers = Exclude<ActionReturnType<typeof actions.user.blocked>['data'], undefined>['blocked']; export type BlockedUsers = Exclude<ActionReturnType<typeof actions.user.blocked>['data'], undefined>['blocked'];
@ -27,10 +28,7 @@ export async function addBlockedUser(blockedUser: BlockedUser) {
return; return;
} }
blockedUsers.update((old) => { addToWritableArray(blockedUsers, Object.assign(blockedUser, { id: data.id }));
old.push(Object.assign(blockedUser, { id: data.id }));
return old;
});
} }
export async function editBlockedUser(blockedUser: BlockedUser) { export async function editBlockedUser(blockedUser: BlockedUser) {
@ -40,9 +38,15 @@ export async function editBlockedUser(blockedUser: BlockedUser) {
return; return;
} }
blockedUsers.update((old) => { updateWritableArray(blockedUsers, blockedUser, (t) => t.id == blockedUser.id);
const index = old.findIndex((a) => a.id == blockedUser.id); }
old[index] = blockedUser;
return old; export async function deleteBlockedUser(blockedUser: BlockedUser) {
}); const { error } = await actions.user.deleteBlocked(blockedUser);
if (error) {
actionErrorPopup(error);
return;
}
deleteFromWritableArray(blockedUsers, (t) => t.id == blockedUser.id);
} }

View File

@ -21,10 +21,11 @@
onClick?: (t: T) => void; onClick?: (t: T) => void;
onEdit?: (t: T) => void; onEdit?: (t: T) => void;
onDelete?: (t: T) => void;
} }
// input // input
let { data, count, keys, onClick, onEdit }: Props<any> = $props(); let { data, count, keys, onClick, onEdit, onDelete }: Props<any> = $props();
</script> </script>
<div class="h-screen overflow-x-auto"> <div class="h-screen overflow-x-auto">
@ -39,7 +40,7 @@
>{key.label}</SortableTh >{key.label}</SortableTh
> >
{/each} {/each}
{#if onEdit} {#if onEdit || onDelete}
<SortableTh style="width: 5%"></SortableTh> <SortableTh style="width: 5%"></SortableTh>
{/if} {/if}
</SortableTr> </SortableTr>
@ -59,11 +60,18 @@
{/if} {/if}
</td> </td>
{/each} {/each}
{#if onEdit} {#if onEdit || onDelete}
<td> <td>
{#if onEdit}
<button class="cursor-pointer" onclick={() => onEdit(d)}> <button class="cursor-pointer" onclick={() => onEdit(d)}>
<Icon icon="heroicons:pencil-square" /> <Icon icon="heroicons:pencil-square" />
</button> </button>
{/if}
{#if onDelete}
<button class="cursor-pointer" onclick={() => onDelete(d)}>
<Icon icon="heroicons:trash" />
</button>
{/if}
</td> </td>
{/if} {/if}
</tr> </tr>

View File

@ -9,7 +9,9 @@ import {
type AddAdminReq, type AddAdminReq,
addAdmin, addAdmin,
type EditAdminReq, type EditAdminReq,
editAdmin editAdmin,
type DeleteAdminReq,
deleteAdmin
} from './schema/admin'; } from './schema/admin';
import { import {
addTeam, addTeam,
@ -114,7 +116,9 @@ import {
type GetBlockedUserByUuidReq, type GetBlockedUserByUuidReq,
getBlockedUserByUuid, getBlockedUserByUuid,
type EditBlockedUserReq, type EditBlockedUserReq,
editBlockedUser editBlockedUser,
type DeleteBlockedUserReq,
deleteBlockedUser
} from '@db/schema/blockedUser.ts'; } from '@db/schema/blockedUser.ts';
export class Database { export class Database {
@ -173,6 +177,7 @@ export class Database {
/* admins */ /* admins */
addAdmin = (values: AddAdminReq) => addAdmin(this.db, values); addAdmin = (values: AddAdminReq) => addAdmin(this.db, values);
editAdmin = (values: EditAdminReq) => editAdmin(this.db, values); editAdmin = (values: EditAdminReq) => editAdmin(this.db, values);
deleteAdmin = (values: DeleteAdminReq) => deleteAdmin(this.db, values);
getAdmins = (values: GetAdminReq) => getAdmins(this.db, values); getAdmins = (values: GetAdminReq) => getAdmins(this.db, values);
existsAdmin = (values: ExistsAdminReq) => existsAdmin(this.db, values); existsAdmin = (values: ExistsAdminReq) => existsAdmin(this.db, values);
@ -189,6 +194,7 @@ export class Database {
/* user blocks */ /* user blocks */
addBlockedUser = (values: AddBlockedUserReq) => addBlockedUser(this.db, values); addBlockedUser = (values: AddBlockedUserReq) => addBlockedUser(this.db, values);
editBlockedUser = (values: EditBlockedUserReq) => editBlockedUser(this.db, values); editBlockedUser = (values: EditBlockedUserReq) => editBlockedUser(this.db, values);
deleteBlockedUser = (values: DeleteBlockedUserReq) => deleteBlockedUser(this.db, values);
getBlockedUserByUuid = (values: GetBlockedUserByUuidReq) => getBlockedUserByUuid(this.db, values); getBlockedUserByUuid = (values: GetBlockedUserByUuidReq) => getBlockedUserByUuid(this.db, values);
getBlockedUsers = (values: GetBlockedUsersReq) => getBlockedUsers(this.db, values); getBlockedUsers = (values: GetBlockedUsersReq) => getBlockedUsers(this.db, values);

View File

@ -22,6 +22,10 @@ export type EditAdminReq = {
permissions: number; permissions: number;
}; };
export type DeleteAdminReq = {
id: number;
};
export type GetAdminReq = {}; export type GetAdminReq = {};
export type GetAdminRes = Omit<typeof admin.$inferSelect, 'password'>[]; export type GetAdminRes = Omit<typeof admin.$inferSelect, 'password'>[];
@ -55,6 +59,10 @@ export async function editAdmin(db: Database, values: EditAdminReq) {
.where(eq(admin.id, values.id)); .where(eq(admin.id, values.id));
} }
export async function deleteAdmin(db: Database, values: DeleteAdminReq) {
return db.delete(admin).where(eq(admin.id, values.id));
}
export async function getAdmins(db: Database, _values: GetAdminReq): Promise<GetAdminRes> { export async function getAdmins(db: Database, _values: GetAdminReq): Promise<GetAdminRes> {
return db.select({ id: admin.id, username: admin.username, permissions: admin.permissions }).from(admin); return db.select({ id: admin.id, username: admin.username, permissions: admin.permissions }).from(admin);
} }

View File

@ -21,6 +21,10 @@ export type EditBlockedUserReq = {
comment?: string | null; comment?: string | null;
}; };
export type DeleteBlockedUserReq = {
id: number;
};
export type GetBlockedUserByUuidReq = { export type GetBlockedUserByUuidReq = {
uuid: string; uuid: string;
}; };
@ -37,6 +41,10 @@ export async function editBlockedUser(db: Database, values: EditBlockedUserReq)
await db.update(blockedUser).set(values).where(eq(blockedUser.id, values.id)); await db.update(blockedUser).set(values).where(eq(blockedUser.id, values.id));
} }
export async function deleteBlockedUser(db: Database, values: DeleteBlockedUserReq) {
return db.delete(blockedUser).where(eq(blockedUser.id, values.id));
}
export async function getBlockedUserByUuid(db: Database, values: GetBlockedUserByUuidReq) { export async function getBlockedUserByUuid(db: Database, values: GetBlockedUserByUuidReq) {
const bu = await db.query.blockedUser.findFirst({ const bu = await db.query.blockedUser.findFirst({
where: eq(blockedUser.uuid, values.uuid) where: eq(blockedUser.uuid, values.uuid)

24
src/util/state.ts Normal file
View File

@ -0,0 +1,24 @@
import type { Writable } from 'svelte/store';
export function addToWritableArray<T>(writable: Writable<T[]>, t: T) {
writable.update((old) => {
old.push(t);
return old;
});
}
export function updateWritableArray<T>(writable: Writable<T[]>, t: T, cmp: (t: T) => boolean) {
writable.update((old) => {
const index = old.findIndex(cmp);
old[index] = t;
return old;
});
}
export function deleteFromWritableArray<T>(writable: Writable<T[]>, cmp: (t: T) => boolean) {
writable.update((old) => {
const index = old.findIndex(cmp);
old.splice(index, 1);
return old;
});
}