refactor admin crud popups
All checks were successful
deploy / build-and-deploy (push) Successful in 23s

This commit is contained in:
2025-05-21 17:22:20 +02:00
parent 8b18623232
commit e47268111a
46 changed files with 889 additions and 1041 deletions

View File

@@ -1,63 +1,52 @@
<script lang="ts">
import Badges from './Badges.svelte';
import BitBadge from '@components/input/BitBadge.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';
import { admins, editAdmin } from '@app/admin/admins/admins.ts';
import CrudPopup from '@components/admin/popup/CrudPopup.svelte';
import DataTable from '@components/admin/table/DataTable.svelte';
// 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);
// state
let editPopupAdmin = $state(null);
let editPopupOpen = $derived(!!editPopupAdmin);
</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)}
<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>
{#snippet permissionsBadge(permissions: number)}
<BitBadge available={Permissions.asOptions()} value={permissions} readonly />
{/snippet}
{#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}
<DataTable
data={admins}
count={true}
keys={[
{ key: 'username', label: 'Username', width: 30 },
{ key: 'permissions', label: 'Berechtigungen', width: 60, transform: permissionsBadge }
]}
onEdit={(admin) => (editPopupAdmin = admin)}
/>
<CrudPopup
texts={{
title: 'Admin bearbeiten',
submitButtonTitle: 'Speichern',
confirmPopupTitle: 'Änderungen speichern',
confirmPopupMessage: 'Sollen die Änderungen gespeichert werden?'
}}
target={editPopupAdmin}
keys={[
[
{ key: 'username', type: 'text', label: 'Username', options: { required: true } },
{ key: 'password', type: 'password', label: 'Passwort', default: null, options: { convert: (v) => v || null } }
],
[
{
key: 'permissions',
type: 'bit-badge',
label: 'Berechtigungen',
default: 0,
options: { available: Permissions.asOptions() }
}
]
]}
onSubmit={editAdmin}
bind:open={editPopupOpen}
/>

View File

@@ -1,50 +0,0 @@
<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);
onUpdate?.(reactiveSet);
}
}
</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>

View File

@@ -1,115 +0,0 @@
<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 && (admin || 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={!admin} />
</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>

View File

@@ -1,34 +1,48 @@
<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';
import CrudPopup from '@components/admin/popup/CrudPopup.svelte';
import { addAdmin, fetchAdmins } from '@app/admin/admins/admins.ts';
import { Permissions } from '@util/permissions.ts';
// state
let createPopupOpen = $state(false);
// lifecycle
onMount(() => {
$effect(() => {
fetchAdmins();
});
// states
let newTeamPopupOpen = $state(false);
</script>
<div>
<button class="btn btn-soft w-full" onclick={() => (newTeamPopupOpen = true)}>
<button class="btn btn-soft w-full" onclick={() => (createPopupOpen = 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}
<CrudPopup
texts={{
title: 'Admin erstellen',
submitButtonTitle: 'Erstellen',
confirmPopupTitle: 'Admin erstellen?',
confirmPopupMessage: 'Soll der Admin erstellt werden?'
}}
target={null}
keys={[
[
{ key: 'username', type: 'text', label: 'Username', options: { required: true } },
{ key: 'password', type: 'password', label: 'Passwort', options: { required: true } }
],
[
{
key: 'permissions',
type: 'bit-badge',
label: 'Berechtigungen',
default: 0,
options: { available: Permissions.asOptions() }
}
]
]}
onSubmit={addAdmin}
bind:open={createPopupOpen}
/>

View File

@@ -1,8 +1,15 @@
import type { Admin } from './types.ts';
import { actions } from 'astro:actions';
import { admins } from './state.ts';
import { type ActionReturnType, actions } from 'astro:actions';
import { writable } from 'svelte/store';
import { actionErrorPopup } from '@util/action.ts';
// types
export type Admins = Exclude<ActionReturnType<typeof actions.admin.admins>['data'], undefined>['admins'];
export type Admin = Admins[0];
// state
export const admins = writable<Admin[]>([]);
// actions
export async function fetchAdmins() {
const { data, error } = await actions.admin.admins();
if (error) {

View File

@@ -1,4 +0,0 @@
import type { Admin } from './types.ts';
import { writable } from 'svelte/store';
export const admins = writable<Admin[]>([]);

View File

@@ -1,4 +0,0 @@
import type { ActionReturnType, actions } from 'astro:actions';
export type Admins = Exclude<ActionReturnType<typeof actions.admin.admins>['data'], undefined>['admins'];
export type Admin = Admins[0];