This commit is contained in:
63
src/app/admin/admins/Admins.svelte
Normal file
63
src/app/admin/admins/Admins.svelte
Normal file
@@ -0,0 +1,63 @@
|
||||
<script lang="ts">
|
||||
import Badges from './Badges.svelte';
|
||||
import { Permissions } from '@util/permissions.ts';
|
||||
import type { Admin } from './types.ts';
|
||||
import CreateOrEditPopup from './CreateOrEditPopup.svelte';
|
||||
import { admins } from './state.ts';
|
||||
import Icon from '@iconify/svelte';
|
||||
import { editAdmin } from './actions.ts';
|
||||
|
||||
// consts
|
||||
const availablePermissionBadges = {
|
||||
[Permissions.Admin.value]: 'Admin',
|
||||
[Permissions.Users.value]: 'Users',
|
||||
[Permissions.Reports.value]: 'Reports',
|
||||
[Permissions.Feedback.value]: 'Feedback',
|
||||
[Permissions.Settings.value]: 'Settings',
|
||||
[Permissions.Tools.value]: 'Tools'
|
||||
};
|
||||
|
||||
// states
|
||||
let editAdminPopupAdmin = $state<Admin | null>(null);
|
||||
</script>
|
||||
|
||||
<div class="h-screen overflow-x-auto">
|
||||
<table class="table table-pin-rows">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 5%">#</th>
|
||||
<th style="width: 30%">Benutzername</th>
|
||||
<th style="width: 60%">Berechtigungen</th>
|
||||
<th style="width: 5%"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each $admins as admin, i (admin.id)}
|
||||
<tr class="hover:bg-base-200">
|
||||
<td>{i + 1}</td>
|
||||
<td>{admin.username}</td>
|
||||
<td>
|
||||
<Badges available={availablePermissionBadges} set={new Permissions(admin.permissions).toNumberArray()} />
|
||||
</td>
|
||||
<td>
|
||||
<button class="cursor-pointer" onclick={() => (editAdminPopupAdmin = admin)}>
|
||||
<Icon icon="heroicons:pencil-square" />
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{#key editAdminPopupAdmin}
|
||||
<CreateOrEditPopup
|
||||
popupTitle="Admin bearbeiten"
|
||||
submitButtonTitle="Admin bearbeiten"
|
||||
confirmPopupTitle="Admin bearbeiten"
|
||||
confirmPopupMessage="Bist du sicher, dass du den Admin bearbeiten möchtest?"
|
||||
admin={editAdminPopupAdmin}
|
||||
open={editAdminPopupAdmin != null}
|
||||
onSubmit={editAdmin}
|
||||
/>
|
||||
{/key}
|
||||
49
src/app/admin/admins/Badges.svelte
Normal file
49
src/app/admin/admins/Badges.svelte
Normal file
@@ -0,0 +1,49 @@
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
available: { [k: number]: string };
|
||||
set: number[];
|
||||
onUpdate?: (set: number[]) => void;
|
||||
}
|
||||
|
||||
// inputs
|
||||
let { available, set, onUpdate }: Props = $props();
|
||||
let reactiveSet = $state(set);
|
||||
|
||||
// callbacks
|
||||
function onOptionSelect(e: Event) {
|
||||
const value = Number((e.target as HTMLSelectElement).value);
|
||||
reactiveSet.push(value);
|
||||
|
||||
onUpdate?.(reactiveSet);
|
||||
|
||||
(e.target as HTMLSelectElement).value = '-';
|
||||
}
|
||||
|
||||
function onBadgeRemove(badge: number) {
|
||||
const index = reactiveSet.indexOf(badge);
|
||||
if (index !== -1) {
|
||||
reactiveSet.splice(index, 1);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-4">
|
||||
{#if onUpdate}
|
||||
<select class="select select-xs w-min" onchange={onOptionSelect}>
|
||||
<option selected hidden>-</option>
|
||||
{#each Object.entries(available) as [value, badge] (value)}
|
||||
<option {value} hidden={reactiveSet.indexOf(Number(value)) !== -1}>{badge}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{/if}
|
||||
<div class="flex flow flex-wrap gap-2">
|
||||
{#each reactiveSet as badge (badge)}
|
||||
<div class="badge badge-outline gap-1">
|
||||
{#if onUpdate}
|
||||
<button class="cursor-pointer" type="button" onclick={() => onBadgeRemove(badge)}>✕</button>
|
||||
{/if}
|
||||
<span>{available[badge]}</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
115
src/app/admin/admins/CreateOrEditPopup.svelte
Normal file
115
src/app/admin/admins/CreateOrEditPopup.svelte
Normal file
@@ -0,0 +1,115 @@
|
||||
<script lang="ts">
|
||||
import { confirmPopupState } from '@components/popup/ConfirmPopup.ts';
|
||||
import Input from '@components/input/Input.svelte';
|
||||
import Badges from './Badges.svelte';
|
||||
import type { Admin } from './types.ts';
|
||||
import { Permissions } from '@util/permissions.ts';
|
||||
import Password from '@components/input/Password.svelte';
|
||||
|
||||
// html bindings
|
||||
let modal: HTMLDialogElement;
|
||||
let modalForm: HTMLFormElement;
|
||||
|
||||
// types
|
||||
interface Props {
|
||||
popupTitle: string;
|
||||
submitButtonTitle: string;
|
||||
confirmPopupTitle: string;
|
||||
confirmPopupMessage: string;
|
||||
|
||||
admin: Admin | null;
|
||||
|
||||
open: boolean;
|
||||
|
||||
onSubmit: (admin: Admin & { password: string }) => void;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
// consts
|
||||
const availablePermissionBadges = {
|
||||
[Permissions.Admin.value]: 'Admin',
|
||||
[Permissions.Users.value]: 'Users',
|
||||
[Permissions.Reports.value]: 'Reports',
|
||||
[Permissions.Feedback.value]: 'Feedback',
|
||||
[Permissions.Settings.value]: 'Settings',
|
||||
[Permissions.Tools.value]: 'Tools'
|
||||
};
|
||||
|
||||
// inputs
|
||||
let { popupTitle, submitButtonTitle, confirmPopupTitle, confirmPopupMessage, admin, open, onSubmit, onClose }: Props =
|
||||
$props();
|
||||
|
||||
// states
|
||||
let username = $state<string | null>(admin?.username ?? null);
|
||||
let password = $state<string | null>(null);
|
||||
let permissions = $state<number | null>(admin?.permissions ?? 0);
|
||||
|
||||
let submitEnabled = $derived(!!(username && password));
|
||||
|
||||
// lifecycle
|
||||
$effect(() => {
|
||||
if (open) modal.show();
|
||||
});
|
||||
|
||||
// callbacks
|
||||
function onBadgesUpdate(newPermissions: number[]) {
|
||||
permissions = new Permissions(newPermissions).value;
|
||||
}
|
||||
|
||||
function onSaveButtonClick() {
|
||||
$confirmPopupState = {
|
||||
title: confirmPopupTitle,
|
||||
message: confirmPopupMessage,
|
||||
onConfirm: () => {
|
||||
onSubmit({
|
||||
id: admin?.id ?? -1,
|
||||
username: username!,
|
||||
password: password!,
|
||||
permissions: permissions!
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
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 w-min" 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-xl font-geist font-bold">{popupTitle}</h3>
|
||||
<div class="w-full gap-x-4 gap-y-2">
|
||||
<div class="w-[20rem]">
|
||||
<Input type="text" bind:value={username} label="Username" required />
|
||||
<Password bind:value={password} label="Password" required />
|
||||
</div>
|
||||
<fieldset class="fieldset">
|
||||
<legend class="fieldset-legend">Berechtigungen</legend>
|
||||
{#key admin}
|
||||
<Badges
|
||||
available={availablePermissionBadges}
|
||||
set={new Permissions(permissions).toNumberArray()}
|
||||
onUpdate={onBadgesUpdate}
|
||||
/>
|
||||
{/key}
|
||||
</fieldset>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
class="btn btn-success"
|
||||
class:disabled={!submitEnabled}
|
||||
disabled={!submitEnabled}
|
||||
onclick={onSaveButtonClick}>{submitButtonTitle}</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>
|
||||
34
src/app/admin/admins/SidebarActions.svelte
Normal file
34
src/app/admin/admins/SidebarActions.svelte
Normal file
@@ -0,0 +1,34 @@
|
||||
<script lang="ts">
|
||||
import { addAdmin, fetchAdmins } from './actions.ts';
|
||||
import Icon from '@iconify/svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import CreateOrEditPopup from '@app/admin/admins/CreateOrEditPopup.svelte';
|
||||
|
||||
// lifecycle
|
||||
onMount(() => {
|
||||
fetchAdmins();
|
||||
});
|
||||
|
||||
// states
|
||||
let newTeamPopupOpen = $state(false);
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<button class="btn btn-soft w-full" onclick={() => (newTeamPopupOpen = true)}>
|
||||
<Icon icon="heroicons:plus-16-solid" />
|
||||
<span>Neuer Admin</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#key newTeamPopupOpen}
|
||||
<CreateOrEditPopup
|
||||
popupTitle="Admin erstellen"
|
||||
submitButtonTitle="Admin erstellen"
|
||||
confirmPopupTitle="Admin erstellen"
|
||||
confirmPopupMessage="Bist du sicher, dass du den Admin erstellen möchtest?"
|
||||
admin={null}
|
||||
open={newTeamPopupOpen}
|
||||
onSubmit={addAdmin}
|
||||
onClose={() => (newTeamPopupOpen = false)}
|
||||
/>
|
||||
{/key}
|
||||
41
src/app/admin/admins/actions.ts
Normal file
41
src/app/admin/admins/actions.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import type { Admin } from './types.ts';
|
||||
import { actions } from 'astro:actions';
|
||||
import { admins } from './state.ts';
|
||||
import { actionErrorPopup } from '@util/action.ts';
|
||||
|
||||
export async function fetchAdmins() {
|
||||
const { data, error } = await actions.admin.admins();
|
||||
if (error) {
|
||||
actionErrorPopup(error);
|
||||
return;
|
||||
}
|
||||
|
||||
admins.set(data.admins);
|
||||
}
|
||||
|
||||
export async function addAdmin(admin: Admin & { password: string }) {
|
||||
const { data, error } = await actions.admin.addAdmin(admin);
|
||||
if (error) {
|
||||
actionErrorPopup(error);
|
||||
return;
|
||||
}
|
||||
|
||||
admins.update((old) => {
|
||||
old.push(Object.assign(admin, { id: data.id }));
|
||||
return old;
|
||||
});
|
||||
}
|
||||
|
||||
export async function editAdmin(admin: Admin & { password: string }) {
|
||||
const { error } = await actions.admin.editAdmin(admin);
|
||||
if (error) {
|
||||
actionErrorPopup(error);
|
||||
return;
|
||||
}
|
||||
|
||||
admins.update((old) => {
|
||||
const index = old.findIndex((a) => a.id == admin.id);
|
||||
old[index] = admin;
|
||||
return old;
|
||||
});
|
||||
}
|
||||
4
src/app/admin/admins/state.ts
Normal file
4
src/app/admin/admins/state.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import type { Admin } from './types.ts';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export const admins = writable<Admin[]>([]);
|
||||
4
src/app/admin/admins/types.ts
Normal file
4
src/app/admin/admins/types.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import type { ActionReturnType, actions } from 'astro:actions';
|
||||
|
||||
export type Admins = Exclude<ActionReturnType<typeof actions.admin.admins>['data'], undefined>['admins'];
|
||||
export type Admin = Admins[0];
|
||||
Reference in New Issue
Block a user