feat(app): users are now all stored in a users table and more list settings are available
Build and Push Docker image / build-and-push (push) Successful in 5m36s
Details
Build and Push Docker image / build-and-push (push) Successful in 5m36s
Details
This commit is contained in:
parent
0fc99bd187
commit
ad2a69e6fe
|
@ -52,8 +52,7 @@ Dieser Client ist ein React Hook und kann in jeder React Komponente verwendet we
|
|||
import {usePB} from "@/lib/pocketbase"
|
||||
import {useQuery} from "@tanstack/react-query";
|
||||
|
||||
const {pb} = usePB()
|
||||
const user = useUser()
|
||||
const {pb,user} = usePB()
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: ["collection", id],
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
import {useMutation} from "@tanstack/react-query";
|
||||
import {IconUsers} from "@tabler/icons-react";
|
||||
import {usePB} from "@/lib/pocketbase.tsx";
|
||||
import {LdapUserModel} from "@/models/AuthTypes.ts";
|
||||
import {UserModal} from "@/models/AuthTypes.ts";
|
||||
import RecordSearchInput, {GenericRecordSearchInputProps} from "../input/RecordSearchInput.tsx";
|
||||
|
||||
|
||||
export default function LdapUserInput(props: GenericRecordSearchInputProps<LdapUserModel>) {
|
||||
export default function UserInput(props: GenericRecordSearchInputProps<UserModal>) {
|
||||
|
||||
const {pb} = usePB()
|
||||
|
||||
return (
|
||||
<RecordSearchInput
|
||||
<LdapUserModel>
|
||||
<UserModal>
|
||||
recordToString={(user) => ({displayName: `${user.givenName} ${user.sn}`})}
|
||||
{...props}
|
||||
placeholder={props.placeholder || "Suche nach Personen..."}
|
||||
|
@ -23,7 +23,7 @@ export default function LdapUserInput(props: GenericRecordSearchInputProps<LdapU
|
|||
return []
|
||||
}
|
||||
|
||||
return (await pb.collection('ldap_users').getList(1, 5, {
|
||||
return (await pb.collection('users').getList(1, 5, {
|
||||
filter: `(username ~ "${
|
||||
search.trim().toLowerCase().split(" ").map(s => s.trim()).join(".")
|
||||
}%")`,
|
|
@ -1,18 +1,18 @@
|
|||
import {LdapUserModel} from "@/models/AuthTypes.ts";
|
||||
import {UserModal} from "@/models/AuthTypes.ts";
|
||||
import {List} from "@mantine/core";
|
||||
import {IconUser} from "@tabler/icons-react";
|
||||
import {usePB} from "@/lib/pocketbase.tsx";
|
||||
|
||||
export default function LdapUsersDisplay({users}: { users: LdapUserModel[] }) {
|
||||
export default function UsersDisplay({users}: { users: UserModal[] }) {
|
||||
|
||||
const {ldapUser} = usePB()
|
||||
const {user} = usePB()
|
||||
|
||||
return <>
|
||||
<List size={"sm"} icon={<IconUser size={16}/>}>
|
||||
{
|
||||
users.map((u) => (
|
||||
<List.Item key={u.id} fw={500} c={ldapUser && ldapUser.id == u.id ? "green" : ""}>
|
||||
{u.givenName} {u.sn}
|
||||
<List.Item key={u.id} fw={500} c={user && user.id == u.id ? "green" : ""}>
|
||||
{u.username}
|
||||
</List.Item>
|
||||
))
|
||||
}
|
|
@ -8,7 +8,6 @@ import {showSuccessNotification} from "@/components/util.tsx";
|
|||
import {useSearchParams} from "react-router-dom";
|
||||
|
||||
import EmailSVG from "@/illustrations/email.svg?react"
|
||||
import {useUser} from "@/lib/user.ts";
|
||||
import PromptLoginModal from "@/components/auth/modals/PromptLoginModal.tsx";
|
||||
|
||||
export const CHANGE_EMAIL_TOKEN_KEY = "changeEmailToken"
|
||||
|
@ -31,7 +30,7 @@ const RequestEmailChangeModal = ({open, onClose}: {
|
|||
|
||||
const mutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
await pb.collection("guest_users").requestEmailChange(formValues.values.email)
|
||||
await pb.collection("users").requestEmailChange(formValues.values.email)
|
||||
},
|
||||
onSuccess: () => {
|
||||
formValues.reset()
|
||||
|
@ -96,7 +95,7 @@ const ConfirmEmailChangeModal = ({open, onClose, token}: {
|
|||
|
||||
const mutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
await pb.collection("guest_users").confirmEmailChange(
|
||||
await pb.collection("users").confirmEmailChange(
|
||||
token,
|
||||
formValues.values.password,
|
||||
)
|
||||
|
@ -153,7 +152,7 @@ const ConfirmEmailChangeModal = ({open, onClose, token}: {
|
|||
export default function ChangeEmailModal() {
|
||||
const {value, handler} = useChangeEmail()
|
||||
|
||||
const user = useUser()
|
||||
const {user} = usePB()
|
||||
|
||||
const [searchParams] = useSearchParams()
|
||||
|
||||
|
|
|
@ -6,7 +6,6 @@ import {showErrorNotification, showSuccessNotification} from "@/components/util.
|
|||
import {Alert, Button, Group, Modal, TextInput, Title} from "@mantine/core";
|
||||
import {IconAt} from "@tabler/icons-react";
|
||||
import {useLogin} from "@/components/auth/modals/hooks.ts";
|
||||
import {useUser} from "@/lib/user.ts";
|
||||
|
||||
export const EMAIL_TOKEN_KEY = "emailVerificationToken"
|
||||
|
||||
|
@ -29,13 +28,11 @@ export default function EmailTokenVerification() {
|
|||
|
||||
const [email, setEmail] = useState<string>("")
|
||||
|
||||
const {pb, refreshUser} = usePB()
|
||||
|
||||
const user = useUser()
|
||||
const {pb, user, refreshUser} = usePB()
|
||||
|
||||
const verifyTokenMutation = useMutation({
|
||||
mutationFn: async (token: string) => {
|
||||
await pb.collection("guest_users").confirmVerification(token)
|
||||
await pb.collection("users").confirmVerification(token)
|
||||
},
|
||||
onSuccess: () => {
|
||||
showSuccessNotification("E-Mail erfolgreich verifiziert")
|
||||
|
@ -77,7 +74,7 @@ export default function EmailTokenVerification() {
|
|||
<Button
|
||||
disabled={email.length === 0}
|
||||
onClick={() => {
|
||||
pb.collection("guest_users").requestVerification(email)
|
||||
pb.collection("users").requestVerification(email)
|
||||
}}
|
||||
>
|
||||
Token erneut senden
|
||||
|
|
|
@ -30,7 +30,7 @@ const RequestResetPasswordModal = ({open, onClose}: {
|
|||
|
||||
const requestResetPasswordMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
await pb.collection("guest_users").requestPasswordReset(formValues.values.email)
|
||||
await pb.collection("users").requestPasswordReset(formValues.values.email)
|
||||
},
|
||||
onSuccess: () => {
|
||||
formValues.reset()
|
||||
|
@ -84,7 +84,7 @@ const ResetPasswordModal = ({open, onClose, token}: {
|
|||
onClose: () => void,
|
||||
token: string
|
||||
}) => {
|
||||
const {pb, userRecord} = usePB()
|
||||
const {pb, user} = usePB()
|
||||
|
||||
const {handler: loginHandler} = useLogin()
|
||||
|
||||
|
@ -102,7 +102,7 @@ const ResetPasswordModal = ({open, onClose, token}: {
|
|||
|
||||
const requestResetPasswordMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
await pb.collection("guest_users").confirmPasswordReset(
|
||||
await pb.collection("users").confirmPasswordReset(
|
||||
token,
|
||||
formValues.values.password,
|
||||
formValues.values.passwordConfirm
|
||||
|
@ -111,7 +111,7 @@ const ResetPasswordModal = ({open, onClose, token}: {
|
|||
onSuccess: () => {
|
||||
formValues.reset()
|
||||
showSuccessNotification("Passwort erfolgreich zurückgesetzt")
|
||||
if (!userRecord) {
|
||||
if (!user) {
|
||||
loginHandler.open()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,7 +25,7 @@ export default function LoginModal() {
|
|||
|
||||
const {value, handler} = useLogin()
|
||||
|
||||
const {ldapLogin, guestLogin, userRecord} = usePB()
|
||||
const {ldapLogin, guestLogin, user} = usePB()
|
||||
|
||||
const {handler: registerHandler} = useRegister()
|
||||
|
||||
|
@ -56,7 +56,7 @@ export default function LoginModal() {
|
|||
})
|
||||
|
||||
return <>
|
||||
<Modal opened={value && !userRecord} onClose={handler.close} withCloseButton={false} size={"sm"}>
|
||||
<Modal opened={value && !user} onClose={handler.close} withCloseButton={false} size={"sm"}>
|
||||
<form className={"stack"} onSubmit={formValues.onSubmit(() => loginMutation.mutate())}>
|
||||
<Title ta={"center"} order={3}>StuVe IT Login</Title>
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ import {Link} from "react-router-dom";
|
|||
export default function RegisterModal() {
|
||||
const {value, handler} = useRegister()
|
||||
|
||||
const {userRecord, pb} = usePB()
|
||||
const {user, pb} = usePB()
|
||||
|
||||
const formValues = useForm({
|
||||
initialValues: {
|
||||
|
@ -36,10 +36,11 @@ export default function RegisterModal() {
|
|||
|
||||
const registerMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
await pb.collection("guest_users").create({
|
||||
await pb.collection("users").create({
|
||||
...formValues.values,
|
||||
REALM: "GUEST"
|
||||
})
|
||||
await pb.collection("guest_users").requestVerification(formValues.values.email)
|
||||
await pb.collection("users").requestVerification(formValues.values.email)
|
||||
},
|
||||
onSuccess: () => {
|
||||
handler.close()
|
||||
|
@ -50,7 +51,7 @@ export default function RegisterModal() {
|
|||
})
|
||||
|
||||
return <>
|
||||
<Modal size={"lg"} opened={value && !userRecord} onClose={handler.close}
|
||||
<Modal size={"lg"} opened={value && !user} onClose={handler.close}
|
||||
title={"Gast Account anlegen"}>
|
||||
|
||||
<form className={"stack"} onSubmit={formValues.onSubmit(() => registerMutation.mutate())}>
|
||||
|
|
|
@ -11,7 +11,7 @@ import {
|
|||
IconMailCog,
|
||||
IconPassword,
|
||||
IconServer,
|
||||
IconServerOff
|
||||
IconServerOff, IconUser
|
||||
} from "@tabler/icons-react";
|
||||
import LdapGroupsDisplay from "@/components/auth/LdapGroupsDisplay.tsx";
|
||||
|
||||
|
@ -22,26 +22,44 @@ export default function UserMenuModal() {
|
|||
|
||||
const {handler: changeEmailHandler} = useChangeEmail()
|
||||
|
||||
const {logout, apiIsHealthy, userRecord} = usePB()
|
||||
const {logout, apiIsHealthy, user} = usePB()
|
||||
|
||||
const {showHelp, toggleShowHelp} = useShowHelp()
|
||||
|
||||
const {showDebug, toggleShowDebug} = useShowDebug()
|
||||
|
||||
return <>
|
||||
<Modal opened={value && !!userRecord} onClose={handler.close} withCloseButton={false} size={"md"}>
|
||||
<Modal opened={value && !!user} onClose={handler.close} withCloseButton={false} size={"md"}>
|
||||
<div className={classes.stack}>
|
||||
<Title order={3}>Hallo {userRecord?.username}</Title>
|
||||
<Title order={3}>Hallo {user?.username}</Title>
|
||||
|
||||
<ShowDebug>
|
||||
Datenbank ID: <Code>{userRecord?.id}</Code>
|
||||
Datenbank ID: <Code>{user?.id}</Code>
|
||||
|
||||
{userRecord?.objectGUID && <>
|
||||
<br/>
|
||||
GUID: <Code>{userRecord?.objectGUID}</Code>
|
||||
|
||||
REALM: <Code>{user?.REALM}</Code>
|
||||
|
||||
{user?.objectGUID && <>
|
||||
<br/>
|
||||
GUID: <Code>{user?.objectGUID}</Code>
|
||||
</>}
|
||||
</ShowDebug>
|
||||
|
||||
<div className={classes.row}>
|
||||
<ThemeIcon
|
||||
variant={"transparent"}
|
||||
size={"xl"}
|
||||
>
|
||||
<IconUser/>
|
||||
</ThemeIcon>
|
||||
|
||||
|
||||
<Text>
|
||||
{user?.REALM === "LDAP" ? "StuVe IT Account" : "Gast Account"}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<div className={classes.row}>
|
||||
<ThemeIcon
|
||||
variant={"transparent"}
|
||||
|
@ -51,9 +69,11 @@ export default function UserMenuModal() {
|
|||
</ThemeIcon>
|
||||
|
||||
|
||||
<Tooltip label={`Dein Email ist ${user?.emailVisibility ? "sichtbar" : "nicht sichtbar"}`}>
|
||||
<Text>
|
||||
{userRecord?.email}
|
||||
{user?.email}
|
||||
</Text>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div className={classes.row}>
|
||||
|
@ -65,10 +85,10 @@ export default function UserMenuModal() {
|
|||
</ThemeIcon>
|
||||
|
||||
<Text>
|
||||
{userRecord?.accountExpires ? (
|
||||
{user?.accountExpires ? (
|
||||
|
||||
new Date(userRecord?.accountExpires).getTime() > Date.now() ? (
|
||||
"Account ist aktiv und läuft am " + new Date(userRecord?.accountExpires).toLocaleDateString() + " ab"
|
||||
new Date(user?.accountExpires).getTime() > Date.now() ? (
|
||||
"Account ist aktiv und läuft am " + new Date(user?.accountExpires).toLocaleDateString() + " ab"
|
||||
) : (
|
||||
"Account ist abgelaufen"
|
||||
)
|
||||
|
@ -103,9 +123,9 @@ export default function UserMenuModal() {
|
|||
</Text>
|
||||
</div>
|
||||
|
||||
{userRecord?.memberOf?.length && <>
|
||||
{(user?.memberOf?.length ?? 0) > 0 && <>
|
||||
<Title order={5}>Deine Gruppen</Title>
|
||||
<LdapGroupsDisplay groups={userRecord?.expand?.memberOf}/>
|
||||
<LdapGroupsDisplay groups={user?.expand?.memberOf}/>
|
||||
</>}
|
||||
|
||||
<Divider label={"Einstellungen"}/>
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
import {UserModal} from "@/models/AuthTypes.ts";
|
||||
import {Tooltip} from "@mantine/core";
|
||||
|
||||
/**
|
||||
* Returns the username of a user
|
||||
* If the user has a first and last name, it will return the full name
|
||||
* @param user
|
||||
*/
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
export const getUserName = (user?: UserModal | null) => {
|
||||
if (!user) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (user.sn && user.givenName) {
|
||||
return `${user.givenName} ${user.sn}`
|
||||
}
|
||||
|
||||
return user.username
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the username with a tooltip indicating the account type
|
||||
* @param user
|
||||
* @constructor
|
||||
*/
|
||||
export const RenderUserName = ({user}: { user?: UserModal | null }) => {
|
||||
|
||||
if (!user) {
|
||||
return null
|
||||
}
|
||||
|
||||
return <>
|
||||
<Tooltip label={user.REALM === "LDAP" ? "StuVe IT Account" : "Gast Account"} withArrow>
|
||||
<span>
|
||||
{getUserName(user)}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</>
|
||||
}
|
|
@ -1,12 +1,12 @@
|
|||
import {Menu} from "@mantine/core";
|
||||
import {NavLink} from "react-router-dom";
|
||||
import {Fragment} from "react";
|
||||
import {IconConfetti, IconHome, IconQrcode} from "@tabler/icons-react";
|
||||
import {IconConfetti, IconHome, IconList, IconQrcode} from "@tabler/icons-react";
|
||||
|
||||
|
||||
const NavItems = [
|
||||
{
|
||||
section: "Seiten",
|
||||
section: "StuVe IT",
|
||||
items: [
|
||||
{
|
||||
title: "Home",
|
||||
|
@ -14,12 +14,29 @@ const NavItems = [
|
|||
description: "Home",
|
||||
link: "/"
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "Events",
|
||||
section: "Events",
|
||||
items: [
|
||||
|
||||
{
|
||||
title: "Übersicht",
|
||||
icon: IconConfetti,
|
||||
description: "Administration für StuVe Events.",
|
||||
description: "Übersicht über alle Events.",
|
||||
link: "/events"
|
||||
},
|
||||
{
|
||||
title: "Deine Anmeldungen",
|
||||
icon: IconList,
|
||||
description: "Deine Anmeldungen bei Events.",
|
||||
link: "/events/entries"
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
section: "Tools",
|
||||
items: [
|
||||
{
|
||||
title: "QR Code Generator",
|
||||
icon: IconQrcode,
|
||||
|
|
|
@ -7,7 +7,7 @@ import {useLogin, useUserMenu} from "@/components/auth/modals/hooks.ts";
|
|||
|
||||
export default function NavBar() {
|
||||
|
||||
const {userRecord} = usePB()
|
||||
const {user} = usePB()
|
||||
|
||||
const {colorScheme, toggleColorScheme} = useMantineColorScheme()
|
||||
|
||||
|
@ -58,7 +58,7 @@ export default function NavBar() {
|
|||
}
|
||||
</ActionIcon>
|
||||
|
||||
{userRecord ?
|
||||
{user ?
|
||||
<ActionIcon
|
||||
variant={"transparent"}
|
||||
color={"gray"}
|
||||
|
|
|
@ -5,7 +5,7 @@ import {useInterval} from "@mantine/hooks";
|
|||
import {useQuery} from "@tanstack/react-query";
|
||||
import {TypedPocketBase} from "@/models";
|
||||
import {PB_BASE_URL, PB_STORAGE_KEY, PB_USER_COLLECTION} from "../../config.ts";
|
||||
import {GuestUserModel, LdapUserModel} from "@/models/AuthTypes.ts";
|
||||
import {UserModal} from "@/models/AuthTypes.ts";
|
||||
import {Alert, List} from "@mantine/core";
|
||||
import {IconAlertTriangle} from "@tabler/icons-react";
|
||||
import {showSuccessNotification} from "@/components/util.tsx";
|
||||
|
@ -104,7 +104,7 @@ const PocketData = () => {
|
|||
}, [pb])
|
||||
|
||||
const guestLogin = useCallback(async (usernameOrEmail: string, password: string) => {
|
||||
await pb.collection("guest_users").authWithPassword(usernameOrEmail, password).then(res => {
|
||||
await pb.collection("users").authWithPassword(usernameOrEmail, password).then(res => {
|
||||
pb.authStore.clear()
|
||||
pb.authStore.save(res.token, res.record)
|
||||
console.log(res.record)
|
||||
|
@ -134,9 +134,7 @@ const PocketData = () => {
|
|||
ldapLogin,
|
||||
guestLogin,
|
||||
logout,
|
||||
userRecord: user as LdapUserModel | GuestUserModel | null,
|
||||
guestUser: user && !Object.keys(user).includes("objectGUID") ? user as GuestUserModel : null,
|
||||
ldapUser: user && Object.keys(user).includes("objectGUID") ? user as LdapUserModel : null,
|
||||
user: user as UserModal | null,
|
||||
pb,
|
||||
refreshUser,
|
||||
useSubscription,
|
||||
|
|
|
@ -1,27 +0,0 @@
|
|||
import {usePB} from "@/lib/pocketbase.tsx";
|
||||
import {useQuery} from "@tanstack/react-query";
|
||||
|
||||
/**
|
||||
* This hook returns the user record of the currently logged-in user.
|
||||
*
|
||||
* If the specific LDAP user model or guest user model is needed, use the usePB hook
|
||||
* to get the specific user model.
|
||||
*
|
||||
* @see usePB
|
||||
* @returns The user record of the currently logged-in user.
|
||||
*/
|
||||
export const useUser = () => {
|
||||
const {pb, userRecord} = usePB()
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: ["user", userRecord?.id ?? ""],
|
||||
queryFn: async () => {
|
||||
return await pb.collection("users").getOne(userRecord?.id ?? "", {
|
||||
expand: "memberOf"
|
||||
})
|
||||
},
|
||||
enabled: userRecord !== null
|
||||
})
|
||||
|
||||
return query.data
|
||||
}
|
|
@ -1,21 +1,27 @@
|
|||
import {AuthModel, RecordModel} from "pocketbase";
|
||||
import {RecordModel} from "pocketbase";
|
||||
|
||||
export type UserModal = {
|
||||
name: string;
|
||||
verified: boolean;
|
||||
realm: string;
|
||||
email: string;
|
||||
} & RecordModel
|
||||
|
||||
export type GuestUserModel = {
|
||||
username: string;
|
||||
verified: boolean;
|
||||
email: string;
|
||||
} & AuthModel & RecordModel
|
||||
emailVisibility: boolean;
|
||||
} & RecordModel & (GuestUser | LDAPUser)
|
||||
|
||||
type GuestUser = {
|
||||
REALM: "GUEST",
|
||||
|
||||
cn: null;
|
||||
dn: null;
|
||||
sn: null;
|
||||
givenName: null;
|
||||
accountExpires: null;
|
||||
objectGUID: null;
|
||||
memberOf: [];
|
||||
}
|
||||
|
||||
type LDAPUser = {
|
||||
REALM: "LDAP",
|
||||
|
||||
export type LdapUserModel = {
|
||||
username: string;
|
||||
email: string;
|
||||
cn: string;
|
||||
dn: string;
|
||||
sn: string;
|
||||
|
@ -27,7 +33,7 @@ export type LdapUserModel = {
|
|||
expand: {
|
||||
memberOf: LdapGroupModel[]
|
||||
}
|
||||
} & AuthModel & RecordModel
|
||||
}
|
||||
|
||||
export type LdapGroupModel = {
|
||||
description: string;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import {GuestUserModel, LdapUserModel} from "./AuthTypes.ts";
|
||||
import {UserModal} from "./AuthTypes.ts";
|
||||
import {RecordModel} from "pocketbase";
|
||||
import {FieldEntries} from "@/components/formUtil/FromInput/types.ts";
|
||||
import {FormSchema} from "@/components/formUtil/formBuilder/types.ts";
|
||||
|
@ -19,8 +19,8 @@ export type EventModel = {
|
|||
defaultEntryQuestionSchema: FormSchema | null;
|
||||
defaultEntryStatusSchema: FormSchema | null;
|
||||
expand?: {
|
||||
eventAdmins: LdapUserModel[] | null | [];
|
||||
eventListAdmins: LdapUserModel[] | null | [];
|
||||
eventAdmins: UserModal[] | null;
|
||||
eventListAdmins: UserModal[] | null;
|
||||
}
|
||||
} & RecordModel
|
||||
|
||||
|
@ -34,6 +34,8 @@ export type EventListModel = {
|
|||
description: string | null;
|
||||
open: boolean | null;
|
||||
favourite: boolean | null;
|
||||
allowOverlappingEntries: boolean | null;
|
||||
onlyStuVeAccounts: boolean | null;
|
||||
event: string
|
||||
entryQuestionSchema: FormSchema | null
|
||||
entryStatusSchema: FormSchema | null;
|
||||
|
@ -63,20 +65,17 @@ export type EventListSlotEntryModel = {
|
|||
entryQuestionData: FieldEntries;
|
||||
entryStatusData: FieldEntries | null;
|
||||
eventListsSlot: string;
|
||||
ldapUser: string | null
|
||||
guestUser: string | null
|
||||
user: string | null
|
||||
expand?: {
|
||||
eventListsSlot: EventListSlotModel;
|
||||
ldapUser: LdapUserModel | null;
|
||||
guestUser: GuestUserModel | null;
|
||||
user: UserModal | null;
|
||||
}
|
||||
} & RecordModel
|
||||
|
||||
export type EventListSlotEntriesWithUserModel =
|
||||
EventListSlotEntryModel
|
||||
& {
|
||||
userId: string;
|
||||
userName: string;
|
||||
user: string;
|
||||
listName: string,
|
||||
slotStartDate: string,
|
||||
slotEndDate: string,
|
||||
|
@ -85,6 +84,11 @@ export type EventListSlotEntriesWithUserModel =
|
|||
listDescription: string | null,
|
||||
event: string,
|
||||
eventName: string,
|
||||
expand?: {
|
||||
event: EventModel;
|
||||
eventList: EventListModel;
|
||||
user: UserModal;
|
||||
}
|
||||
}
|
||||
& Pick<EventModel, "defaultEntryQuestionSchema" | "defaultEntryStatusSchema">
|
||||
& Pick<EventListModel, "entryQuestionSchema" | "entryStatusSchema">
|
|
@ -1,5 +1,5 @@
|
|||
import PocketBase, {RecordModel, RecordService} from "pocketbase";
|
||||
import {GuestUserModel, LdapGroupModel, LdapSyncLogModel, LdapUserModel, UserModal} from "./AuthTypes.ts";
|
||||
import {LdapGroupModel, LdapSyncLogModel, UserModal} from "./AuthTypes.ts";
|
||||
import {
|
||||
EventListModel,
|
||||
EventListSlotEntriesWithUserModel,
|
||||
|
@ -32,10 +32,6 @@ export interface TypedPocketBase extends PocketBase {
|
|||
|
||||
collection(idOrName: 'users'): RecordService<UserModal>
|
||||
|
||||
collection(idOrName: 'guest_users'): RecordService<GuestUserModel>
|
||||
|
||||
collection(idOrName: 'ldap_users'): RecordService<LdapUserModel>
|
||||
|
||||
collection(idOrName: 'ldap_groups'): RecordService<LdapGroupModel>
|
||||
|
||||
collection(idOrName: 'ldap_sync_logs'): RecordService<LdapSyncLogModel>
|
||||
|
|
|
@ -4,9 +4,9 @@ import {DateTimePicker} from "@mantine/dates";
|
|||
import {IconCheck, IconInfoCircle, IconX} from "@tabler/icons-react";
|
||||
import {useMutation} from "@tanstack/react-query";
|
||||
import dayjs from "dayjs";
|
||||
import {LdapUserModel} from "@/models/AuthTypes.ts";
|
||||
import {UserModal} from "@/models/AuthTypes.ts";
|
||||
import {PocketBaseErrorAlert, usePB} from "@/lib/pocketbase.tsx";
|
||||
import LdapUserInput from "@/components/auth/LdapUserInput.tsx";
|
||||
import UserInput from "@/components/auth/UserInput.tsx";
|
||||
import {EventModel} from "@/models/EventTypes.ts";
|
||||
import ShowHelp from "@/components/ShowHelp.tsx";
|
||||
import {useSettings} from "@/lib/settings.ts";
|
||||
|
@ -23,7 +23,7 @@ export default function CreateEvent({onSuccess, onAbort}: {
|
|||
onSuccess: (event: EventModel) => void,
|
||||
onAbort?: () => void
|
||||
}) {
|
||||
const {ldapUser, pb} = usePB()
|
||||
const {user, pb} = usePB()
|
||||
const settings = useSettings()
|
||||
|
||||
const stuveQuestions = JSON.parse(settings?.stuveEventQuestions?.value ?? `{ "fields":[] }`) as FormSchema
|
||||
|
@ -33,7 +33,7 @@ export default function CreateEvent({onSuccess, onAbort}: {
|
|||
name: "",
|
||||
startDate: null,
|
||||
endDate: null,
|
||||
eventAdmins: [ldapUser] as LdapUserModel[],
|
||||
eventAdmins: [user] as UserModal[],
|
||||
isStuveEvent: true,
|
||||
},
|
||||
validate: {
|
||||
|
@ -46,7 +46,7 @@ export default function CreateEvent({onSuccess, onAbort}: {
|
|||
|
||||
const createEventMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
if (!ldapUser) {
|
||||
if (!user || user.REALM === "LDAP") {
|
||||
throw new Error("Nur mit StuVe IT Account eingeloggte Personen können Events erstellen")
|
||||
}
|
||||
return await pb.collection("events").create({
|
||||
|
@ -93,7 +93,7 @@ export default function CreateEvent({onSuccess, onAbort}: {
|
|||
</Grid.Col>
|
||||
|
||||
<Grid.Col span={{base: 12, sm: 6}}>
|
||||
<LdapUserInput
|
||||
<UserInput
|
||||
required
|
||||
label={"Event-Admins"}
|
||||
description={"Die Event-Admins können das Event bearbeiten."}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import {IconConfetti, IconPlus} from "@tabler/icons-react";
|
||||
import {IconConfetti, IconPlus, IconUser} from "@tabler/icons-react";
|
||||
import {EventCalendar} from "./EventCalendar.tsx";
|
||||
import {EventList} from "./EventList.tsx";
|
||||
import {Anchor, Breadcrumbs, Button, Collapse} from "@mantine/core";
|
||||
|
@ -10,7 +10,7 @@ import {Link, useNavigate} from "react-router-dom";
|
|||
|
||||
export default function EventOverview() {
|
||||
|
||||
const {ldapUser} = usePB()
|
||||
const {user} = usePB()
|
||||
const [showCreateEvent, showCreateEventHandler] = useDisclosure(false)
|
||||
const navigate = useNavigate();
|
||||
|
||||
|
@ -30,7 +30,7 @@ export default function EventOverview() {
|
|||
</div>
|
||||
|
||||
{
|
||||
ldapUser && <>
|
||||
(user && user.REALM === "LDAP") && <>
|
||||
<Collapse in={showCreateEvent} className={"section"}>
|
||||
<CreateEvent
|
||||
onAbort={showCreateEventHandler.close}
|
||||
|
@ -39,6 +39,15 @@ export default function EventOverview() {
|
|||
</Collapse>
|
||||
|
||||
{!showCreateEvent && <div className={"section-transparent group"} style={{gap: 0}}>
|
||||
<Button
|
||||
leftSection={<IconUser/>}
|
||||
variant={"transparent"}
|
||||
color={"green"}
|
||||
component={Link}
|
||||
to={"/events/entries"}
|
||||
>
|
||||
Deine Anmeldungen bei Events
|
||||
</Button>
|
||||
<Button
|
||||
leftSection={<IconPlus/>}
|
||||
variant={"transparent"}
|
||||
|
|
|
@ -2,7 +2,7 @@ import {EventModel} from "@/models/EventTypes.ts";
|
|||
import classes from "../EditEventRouter.module.css";
|
||||
import {IconAdjustments, IconCalendar, IconHourglass, IconList, IconMap, IconSparkles} from "@tabler/icons-react";
|
||||
import {areDatesSame, humanDeltaFromNow, pprintDate} from "@/lib/datetime.ts";
|
||||
import LdapUsersDisplay from "@/components/auth/LdapUsersDisplay.tsx";
|
||||
import UsersDisplay from "@/components/auth/UsersDisplay.tsx";
|
||||
import {Text, ThemeIcon, Title} from "@mantine/core";
|
||||
|
||||
/**
|
||||
|
@ -86,7 +86,7 @@ export default function EventData({event, hideHeader}: { event: EventModel, hide
|
|||
<IconAdjustments size={16}/>
|
||||
Event Admins
|
||||
</div>
|
||||
<LdapUsersDisplay users={event.expand.eventAdmins}/>
|
||||
<UsersDisplay users={event.expand.eventAdmins}/>
|
||||
</div>
|
||||
}
|
||||
|
||||
|
@ -97,7 +97,7 @@ export default function EventData({event, hideHeader}: { event: EventModel, hide
|
|||
<IconList size={16}/>
|
||||
Listen Admins
|
||||
</div>
|
||||
<LdapUsersDisplay users={event.expand.eventListAdmins}/>
|
||||
<UsersDisplay users={event.expand.eventListAdmins}/>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import {EventModel} from "@/models/EventTypes.ts";
|
||||
import {useForm} from "@mantine/form";
|
||||
import {LdapUserModel} from "@/models/AuthTypes.ts";
|
||||
import {UserModal} from "@/models/AuthTypes.ts";
|
||||
import {PocketBaseErrorAlert, usePB} from "@/lib/pocketbase.tsx";
|
||||
import {Button, Group, Title} from "@mantine/core";
|
||||
import LdapUserInput from "@/components/auth/LdapUserInput.tsx";
|
||||
import UserInput from "@/components/auth/UserInput.tsx";
|
||||
import {useMutation} from "@tanstack/react-query";
|
||||
import {queryClient} from "@/main.tsx";
|
||||
import {showSuccessNotification} from "@/components/util.tsx";
|
||||
|
@ -15,12 +15,12 @@ import ShowHelp from "@/components/ShowHelp.tsx";
|
|||
*/
|
||||
export default function EditEventMembers({event}: { event: EventModel }) {
|
||||
|
||||
const {ldapUser, pb} = usePB()
|
||||
const {user, pb} = usePB()
|
||||
|
||||
const formValues = useForm({
|
||||
initialValues: {
|
||||
eventAdmins: event?.expand?.eventAdmins ?? [ldapUser] as LdapUserModel[],
|
||||
eventListAdmins: event?.expand?.eventListAdmins ?? [] as LdapUserModel[],
|
||||
eventAdmins: event?.expand?.eventAdmins ?? [user] as UserModal[],
|
||||
eventListAdmins: event?.expand?.eventListAdmins ?? [] as UserModal[],
|
||||
},
|
||||
validate: {
|
||||
eventAdmins: (value) => value.length > 0 ? null : "Es muss mindestens ein Admin ausgewählt werden.",
|
||||
|
@ -34,7 +34,7 @@ export default function EditEventMembers({event}: { event: EventModel }) {
|
|||
const editMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
return await pb.collection("events").update(event.id, {
|
||||
eventAdmins: [...formValues.values.eventAdmins.map((member) => member.id), ldapUser?.id],
|
||||
eventAdmins: [...formValues.values.eventAdmins.map((member) => member.id), user?.id],
|
||||
eventListAdmins: formValues.values.eventListAdmins.map((member) => member.id)
|
||||
})
|
||||
},
|
||||
|
@ -66,7 +66,7 @@ export default function EditEventMembers({event}: { event: EventModel }) {
|
|||
<PocketBaseErrorAlert error={editMutation.error}/>
|
||||
|
||||
|
||||
<LdapUserInput
|
||||
<UserInput
|
||||
required
|
||||
label={"Event Admins"}
|
||||
description={"Die Event-Admins können das ganze Event bearbeiten."}
|
||||
|
@ -75,7 +75,7 @@ export default function EditEventMembers({event}: { event: EventModel }) {
|
|||
setSelectedRecords={(records) => formValues.setFieldValue("eventAdmins", records)}
|
||||
/>
|
||||
|
||||
<LdapUserInput
|
||||
<UserInput
|
||||
label={"Listen Admins"}
|
||||
description={"Die Listen Admins können Listen bearbeiten und verwalten."}
|
||||
error={formValues.errors.eventListAdmins}
|
||||
|
|
|
@ -1,6 +1,14 @@
|
|||
import {useQuery} from "@tanstack/react-query";
|
||||
import {Alert, Breadcrumbs, Button, Group, LoadingOverlay, Title} from "@mantine/core";
|
||||
import {IconCheckupList, IconClockCog, IconForms, IconLock, IconLockOpen, IconSettings} from "@tabler/icons-react";
|
||||
import {
|
||||
IconCheckupList,
|
||||
IconClockCog,
|
||||
IconForms,
|
||||
IconLock,
|
||||
IconLockOpen,
|
||||
IconSettings,
|
||||
IconUserCog
|
||||
} from "@tabler/icons-react";
|
||||
import {Link, Navigate, NavLink, Route, Routes, useParams} from "react-router-dom";
|
||||
import InnerHtml from "@/components/InnerHtml";
|
||||
import {EventModel} from "@/models/EventTypes.ts";
|
||||
|
@ -11,6 +19,7 @@ import EventListEntryQuestionSettings
|
|||
from "@/pages/events/e/:eventId/EventLists/:listId/EventListSettings/EventListEntryQuestionSettings.tsx";
|
||||
import EventListEntryStatusSettings
|
||||
from "@/pages/events/e/:eventId/EventLists/:listId/EventListSettings/EventListEntryStatusSettings.tsx";
|
||||
import TextWithIcon from "@/components/layout/TextWithIcon";
|
||||
|
||||
|
||||
export default function EventListRouter({event}: { event: EventModel }) {
|
||||
|
@ -48,10 +57,19 @@ export default function EventListRouter({event}: { event: EventModel }) {
|
|||
</Link>
|
||||
</Breadcrumbs>
|
||||
|
||||
<Alert color={list.open ? "green" : "red"}
|
||||
icon={list.open ? <IconLockOpen/> : <IconLock/>}
|
||||
>
|
||||
{list.open ? "Liste ist für Anmeldungen geöffnet" : "Liste ist für Anmeldungen geschlossen"}
|
||||
<Alert color={list.open ? "green" : "red"}>
|
||||
<TextWithIcon icon={list.open ? <IconLockOpen size={16}/> : <IconLock size={16}/>}>
|
||||
Liste ist für Anmeldungen <b>{list.open ? "geöffnet" : "geschlossen"}</b>
|
||||
</TextWithIcon>
|
||||
<br/>
|
||||
<TextWithIcon icon={<IconUserCog size={16}/>}>
|
||||
Anmeldung für Personen mit {list.onlyStuVeAccounts ?
|
||||
<><b>StuVe</b> Account</> : <>StuVe <b>und</b> Gast Account</>}
|
||||
</TextWithIcon>
|
||||
<br/>
|
||||
<TextWithIcon icon={<IconClockCog size={16}/>}>
|
||||
Überlappende Einträge sind {!list.allowOverlappingEntries && <b>nicht</b>} erlaubt
|
||||
</TextWithIcon>
|
||||
</Alert>
|
||||
|
||||
<div className={"section"}>
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
import {EventListModel, EventModel} from "@/models/EventTypes.ts";
|
||||
import {useForm} from "@mantine/form";
|
||||
import {Button, Group, Switch, TextInput, Title} from "@mantine/core";
|
||||
import {ActionIcon, Button, Group, TextInput, Title, Tooltip} from "@mantine/core";
|
||||
import TextEditor from "@/components/input/Editor";
|
||||
import {useMutation} from "@tanstack/react-query";
|
||||
import {PocketBaseErrorAlert, usePB} from "@/lib/pocketbase.tsx";
|
||||
import {queryClient} from "@/main.tsx";
|
||||
import {showSuccessNotification} from "@/components/util.tsx";
|
||||
import {IconLock, IconLockOpen, IconStarFilled, IconStarOff, IconTrash} from "@tabler/icons-react";
|
||||
import {IconStarFilled, IconStarOff, IconTrash} from "@tabler/icons-react";
|
||||
import {useNavigate} from "react-router-dom";
|
||||
import {useConfirmModal} from "@/components/ConfirmModal.tsx";
|
||||
import {CheckboxCard} from "@/components/input/CheckboxCard";
|
||||
|
||||
export default function EventListSettings({list, event}: { list: EventListModel, event: EventModel }) {
|
||||
const {pb} = usePB()
|
||||
|
@ -18,6 +19,8 @@ export default function EventListSettings({list, event}: { list: EventListModel,
|
|||
initialValues: {
|
||||
name: list.name,
|
||||
open: list.open,
|
||||
allowOverlappingEntries: list.allowOverlappingEntries,
|
||||
onlyStuVeAccounts: list.onlyStuVeAccounts,
|
||||
favorite: list.favorite,
|
||||
description: list.description || ""
|
||||
}
|
||||
|
@ -26,7 +29,7 @@ export default function EventListSettings({list, event}: { list: EventListModel,
|
|||
const {ConfirmModal, toggleConfirmModal} = useConfirmModal({
|
||||
onConfirm: () => deleteMutation.mutate(),
|
||||
description: "Bist du sicher, dass du diese Liste löschen möchtest? " +
|
||||
"Alle Einträge werden ebenfalls gelöscht. " +
|
||||
"Alle Anmeldungen werden ebenfalls gelöscht. " +
|
||||
"Diese Aktion kann nicht rückgängig gemacht werden.",
|
||||
})
|
||||
|
||||
|
@ -68,23 +71,38 @@ export default function EventListSettings({list, event}: { list: EventListModel,
|
|||
label={"Name"}
|
||||
placeholder={"Name der Liste"}
|
||||
required
|
||||
rightSection={<Tooltip label={"Liste im favoriten Menu anzeigen"}>
|
||||
<ActionIcon
|
||||
color={"blue"}
|
||||
onClick={() => formValues.setFieldValue("favorite", !formValues.values.favorite)}
|
||||
variant={"transparent"}
|
||||
>
|
||||
{
|
||||
formValues.values.favorite ? <IconStarFilled/> : <IconStarOff/>
|
||||
}
|
||||
</ActionIcon>
|
||||
</Tooltip>}
|
||||
{...formValues.getInputProps("name")}
|
||||
/>
|
||||
|
||||
<Switch
|
||||
onLabel={<IconStarFilled size={16}/>}
|
||||
offLabel={<IconStarOff size={16}/>}
|
||||
label={"Liste favorisiert"}
|
||||
color={"green"}
|
||||
{...formValues.getInputProps("favorite", {type: "checkbox"})}
|
||||
<CheckboxCard
|
||||
label={"Anmeldungen erlauben"}
|
||||
description={"Ob sich Personen für Zeitslots dieser Liste anmelden können"}
|
||||
{...formValues.getInputProps("open", {type: "checkbox"})}
|
||||
/>
|
||||
|
||||
<Switch
|
||||
onLabel={<IconLockOpen size={16}/>}
|
||||
offLabel={<IconLock size={16}/>}
|
||||
label={"Anmeldungen erlauben"}
|
||||
color={"green"}
|
||||
{...formValues.getInputProps("open", {type: "checkbox"})}
|
||||
<CheckboxCard
|
||||
label={"Nur StuVe Accounts"}
|
||||
description={"Nur Personen mit StuVe IT Account können sich für diese Liste Anmelden"}
|
||||
{...formValues.getInputProps("onlyStuVeAccounts", {type: "checkbox"})}
|
||||
/>
|
||||
|
||||
<CheckboxCard
|
||||
label={"Überlappende Anmeldungen erlauben"}
|
||||
description={" Wenn diese Option aktiviert ist, kann sich eine Person in überlappende Zeitslots anmelden. " +
|
||||
"Dabei werden alle Zeitslots von diesem Event beachtet. " +
|
||||
"Bei dem Verschieden von Anmeldungen wirds diese Regel nicht beachtet!"}
|
||||
{...formValues.getInputProps("allowOverlappingEntries", {type: "checkbox"})}
|
||||
/>
|
||||
|
||||
<TextEditor
|
||||
|
@ -95,6 +113,7 @@ export default function EventListSettings({list, event}: { list: EventListModel,
|
|||
/>
|
||||
|
||||
<Group>
|
||||
|
||||
<Button
|
||||
variant={"outline"}
|
||||
color={"red"}
|
||||
|
|
|
@ -26,6 +26,7 @@ export const EventListSlotEntriesTable = ({slot, list, event, refetch, visible}:
|
|||
|
||||
const res = await pb.collection("eventListSlotEntriesWithUser").getList(page, 50, {
|
||||
filter: `eventListsSlot='${slot.id}'`,
|
||||
expand: "user"
|
||||
})
|
||||
|
||||
return {
|
||||
|
@ -43,7 +44,7 @@ export const EventListSlotEntriesTable = ({slot, list, event, refetch, visible}:
|
|||
if (query.isLoading) {
|
||||
return <Stack align={"center"} gap={"xs"}>
|
||||
<Loader size={"sm"}/>
|
||||
<Text size={"xs"} c={"dimmed"}>Einträge werden geladen</Text>
|
||||
<Text size={"xs"} c={"dimmed"}>Anmeldungen werden geladen</Text>
|
||||
</Stack>
|
||||
}
|
||||
|
||||
|
@ -52,7 +53,7 @@ export const EventListSlotEntriesTable = ({slot, list, event, refetch, visible}:
|
|||
<ThemeIcon variant={"transparent"} color={"gray"} size={"md"}>
|
||||
<IconDatabaseOff/>
|
||||
</ThemeIcon>
|
||||
<Text size={"xs"} c={"dimmed"}>Keine Einträge vorhanden</Text>
|
||||
<Text size={"xs"} c={"dimmed"}>Keine Anmeldungen vorhanden</Text>
|
||||
</Stack>
|
||||
}
|
||||
|
||||
|
@ -77,7 +78,7 @@ export const EventListSlotEntriesTable = ({slot, list, event, refetch, visible}:
|
|||
|
||||
<div className={classes.bottomRow}>
|
||||
<Text size={"xs"} c={"dimmed"}>
|
||||
{query.data?.totalItems} Einträge
|
||||
{query.data?.totalItems} Anmeldungen
|
||||
</Text>
|
||||
<Pagination size={"xs"} value={page} onChange={setPage} total={query.data?.totalPages ?? 1}/>
|
||||
</div>
|
||||
|
|
|
@ -11,6 +11,7 @@ import {IconCheckupList, IconForms, IconUserMinus, IconUserPlus} from "@tabler/i
|
|||
import {renderEntries} from "@/components/formUtil/formTable";
|
||||
import RenderCell from "@/components/formUtil/formTable/RenderCell.tsx";
|
||||
import EditSlotEntryMenu from "@/pages/events/e/:eventId/EventLists/components/EditSlotEntryMenu.tsx";
|
||||
import {RenderUserName} from "@/components/auth/modals/util.tsx";
|
||||
|
||||
|
||||
export const EventListSlotEntryDetails = ({entry}: {
|
||||
|
@ -77,9 +78,7 @@ export const EventListSlotEntryRow = ({entry, refetch}: {
|
|||
event: EventModel,
|
||||
refetch: () => void
|
||||
}) => {
|
||||
|
||||
const [expanded, expandedHandler] = useDisclosure(false)
|
||||
|
||||
return <div className={classes.entryContainer} aria-expanded={expanded}>
|
||||
<div className={classes.entryRow}>
|
||||
<Tooltip label={"Details anzeigen"} withArrow>
|
||||
|
@ -88,7 +87,7 @@ export const EventListSlotEntryRow = ({entry, refetch}: {
|
|||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
<div>{`${entry.userName}`}</div>
|
||||
<div><RenderUserName user={entry.expand?.user}/></div>
|
||||
|
||||
<Group gap={4} justify="right" wrap="nowrap">
|
||||
<EditSlotEntryMenu entry={entry} refetch={refetch}/>
|
||||
|
|
|
@ -10,6 +10,7 @@ import {
|
|||
import {useDisclosure} from "@mantine/hooks";
|
||||
import {RenderDateRange} from "@/pages/events/e/:eventId/EventLists/components/RenderDateRange.tsx";
|
||||
import EditSlotEntryMenu from "@/pages/events/e/:eventId/EventLists/components/EditSlotEntryMenu.tsx";
|
||||
import {RenderUserName} from "@/components/auth/modals/util.tsx";
|
||||
|
||||
export default function EventListSearchResult({entry, refetch}: {
|
||||
entry: EventListSlotEntriesWithUserModel,
|
||||
|
@ -32,7 +33,7 @@ export default function EventListSearchResult({entry, refetch}: {
|
|||
<IconUser/>
|
||||
</ThemeIcon>
|
||||
}>
|
||||
{entry.userName}
|
||||
<RenderUserName user={entry.expand?.user}/>
|
||||
</TextWithIcon>
|
||||
|
||||
<TextWithIcon icon={
|
||||
|
|
|
@ -41,7 +41,7 @@ export default function ListSearch({event}: { event: EventModel }) {
|
|||
const filter: string[] = [`event='${event.id}'`]
|
||||
|
||||
if (debouncedSearchQueryString) {
|
||||
filter.push(`userName ~ '${debouncedSearchQueryString}'`)
|
||||
filter.push(`user.username ~ '${debouncedSearchQueryString.trim().replace(" ", ".")}'`)
|
||||
}
|
||||
|
||||
if (selectedLists.length > 0) {
|
||||
|
@ -49,7 +49,8 @@ export default function ListSearch({event}: { event: EventModel }) {
|
|||
}
|
||||
|
||||
return await pb.collection("eventListSlotEntriesWithUser").getList(1, 50, {
|
||||
filter: filter.join(" && ")
|
||||
filter: filter.join(" && "),
|
||||
expand: "user"
|
||||
})
|
||||
}
|
||||
})
|
||||
|
@ -61,7 +62,7 @@ export default function ListSearch({event}: { event: EventModel }) {
|
|||
placeholder={"Nach Namen suchen ..."}
|
||||
leftSection={<IconUserSearch/>}
|
||||
rightSection={searchQuery.isLoading ? <Loader size={"xs"}/> : (
|
||||
<Tooltip label={"Einträge Filtern"} withArrow>
|
||||
<Tooltip label={"Anmeldungen Filtern"} withArrow>
|
||||
<ActionIcon
|
||||
onClick={showFilterHandler.toggle}
|
||||
variant={"transparent"}
|
||||
|
@ -75,7 +76,7 @@ export default function ListSearch({event}: { event: EventModel }) {
|
|||
searchQuery
|
||||
.data
|
||||
?.items
|
||||
.map(e => e.userName)
|
||||
.map(e => e.id)
|
||||
.filter(onlyUnique) ?? []
|
||||
}
|
||||
value={searchQueryString} onChange={setSearchQueryString}
|
||||
|
@ -86,17 +87,17 @@ export default function ListSearch({event}: { event: EventModel }) {
|
|||
event={event}
|
||||
selectedRecords={selectedLists}
|
||||
setSelectedRecords={setSelectedLists}
|
||||
placeholder={"Einträge nur für bestimmte Listen anzeigen"}
|
||||
placeholder={"Anmeldungen nur für bestimmte Listen anzeigen"}
|
||||
/>
|
||||
</Collapse>
|
||||
|
||||
<Group justify={"space-between"}>
|
||||
<Text c={"dimmed"} size={"xs"}>
|
||||
{searchQueryString ? `Suche nach Person '${searchQueryString}'` : "Alle Einträge für dieses Event"}
|
||||
{searchQueryString ? `Suche nach Person '${searchQueryString}'` : "Alle Anmeldungen für dieses Event"}
|
||||
</Text>
|
||||
|
||||
<Text c={"dimmed"} size={"xs"}>
|
||||
{searchQuery.data?.totalItems ?? 0} {searchQueryString ? "Ergebnisse" : "Einträge"}
|
||||
{searchQuery.data?.totalItems ?? 0} {searchQueryString ? "Ergebnisse" : "Anmeldungen"}
|
||||
</Text>
|
||||
</Group>
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@ import {
|
|||
} from "@/pages/events/e/:eventId/EventLists/components/UpdateEventListSlotEntryFormModal.tsx";
|
||||
import {ActionIcon, Button, Menu} from "@mantine/core";
|
||||
import {IconArrowsMove, IconCheckupList, IconForms, IconSettings, IconTrash} from "@tabler/icons-react";
|
||||
import {getUserName} from "@/components/auth/modals/util.tsx";
|
||||
|
||||
export default function EditSlotEntryMenu({entry, refetch}: {
|
||||
refetch: () => void,
|
||||
|
@ -41,7 +42,7 @@ export default function EditSlotEntryMenu({entry, refetch}: {
|
|||
|
||||
const {ConfirmModal, toggleConfirmModal} = useConfirmModal({
|
||||
title: 'Eintrag löschen',
|
||||
description: `Möchtest du den Eintrag von ${entry.userName} wirklich löschen?`,
|
||||
description: `Möchtest du den Eintrag von ${getUserName(entry.expand?.user)} wirklich löschen?`,
|
||||
onConfirm: () => deleteEntryMutation.mutate()
|
||||
})
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ import {showSuccessNotification} from "@/components/util.tsx";
|
|||
import {Alert, Button, Group, Modal, Select} from "@mantine/core";
|
||||
import {useEffect, useState} from "react";
|
||||
import {pprintDateTime} from "@/lib/datetime.ts";
|
||||
import {getUserName, RenderUserName} from "@/components/auth/modals/util.tsx";
|
||||
|
||||
export const MoveEventListSlotEntryModal = ({opened, close, refetch, entry}: {
|
||||
opened: boolean,
|
||||
|
@ -30,7 +31,7 @@ export const MoveEventListSlotEntryModal = ({opened, close, refetch, entry}: {
|
|||
})
|
||||
},
|
||||
onSuccess: () => {
|
||||
showSuccessNotification(`Eintrag von ${entry.userName} erfolgreich verschoben`)
|
||||
showSuccessNotification(`Eintrag von ${getUserName(entry.expand?.user)} erfolgreich verschoben`)
|
||||
slotsQuery.refetch()
|
||||
listsQuery.refetch()
|
||||
refetch()
|
||||
|
@ -66,7 +67,7 @@ export const MoveEventListSlotEntryModal = ({opened, close, refetch, entry}: {
|
|||
size={"lg"}
|
||||
opened={opened}
|
||||
onClose={close}
|
||||
title={<> Eintrag von <b>{entry.userName}</b> verschieben </>}
|
||||
title={<> Eintrag von <b><RenderUserName user={entry.expand?.user}/></b> verschieben </>}
|
||||
>
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
|
|
|
@ -5,6 +5,7 @@ import {FieldEntries} from "@/components/formUtil/FromInput/types.ts";
|
|||
import {showSuccessNotification} from "@/components/util.tsx";
|
||||
import {Modal} from "@mantine/core";
|
||||
import FormInput from "@/components/formUtil/FromInput";
|
||||
import {getUserName, RenderUserName} from "@/components/auth/modals/util.tsx";
|
||||
|
||||
export const UpdateEventListSlotEntryFormModal = ({opened, close, refetch, entry}: {
|
||||
opened: boolean,
|
||||
|
@ -23,7 +24,7 @@ export const UpdateEventListSlotEntryFormModal = ({opened, close, refetch, entry
|
|||
})
|
||||
},
|
||||
onSuccess: () => {
|
||||
showSuccessNotification(`Formular Daten von ${entry.userName} erfolgreich aktualisiert`)
|
||||
showSuccessNotification(`Formular Daten von ${getUserName(entry.expand?.user)} erfolgreich aktualisiert`)
|
||||
refetch()
|
||||
close()
|
||||
}
|
||||
|
@ -38,7 +39,7 @@ export const UpdateEventListSlotEntryFormModal = ({opened, close, refetch, entry
|
|||
size={"lg"}
|
||||
opened={opened}
|
||||
onClose={close}
|
||||
title={<> Eintrag von <b>{entry.userName}</b> bearbeiten </>}
|
||||
title={<> Eintrag von <b><RenderUserName user={entry.expand?.user}/></b> bearbeiten </>}
|
||||
>
|
||||
<PocketBaseErrorAlert error={mutation.error}/>
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ import {Alert, Modal} from "@mantine/core";
|
|||
import InnerHtml from "@/components/InnerHtml";
|
||||
import {RenderDateRange} from "./RenderDateRange.tsx";
|
||||
import FormInput from "@/components/formUtil/FromInput";
|
||||
import {getUserName, RenderUserName} from "@/components/auth/modals/util.tsx";
|
||||
|
||||
export const UpdateEventListSlotEntryStatusModal = ({opened, close, refetch, entry}: {
|
||||
opened: boolean,
|
||||
|
@ -29,7 +30,7 @@ export const UpdateEventListSlotEntryStatusModal = ({opened, close, refetch, ent
|
|||
})
|
||||
},
|
||||
onSuccess: () => {
|
||||
showSuccessNotification(`Status von ${entry.userName} erfolgreich aktualisiert`)
|
||||
showSuccessNotification(`Status von ${getUserName(entry.expand?.user)} erfolgreich aktualisiert`)
|
||||
refetch()
|
||||
close()
|
||||
}
|
||||
|
@ -39,7 +40,7 @@ export const UpdateEventListSlotEntryStatusModal = ({opened, close, refetch, ent
|
|||
size={"lg"}
|
||||
opened={opened}
|
||||
onClose={close}
|
||||
title={<> Eintrag von <b>{entry.userName}</b> bearbeiten </>}
|
||||
title={<> Eintrag von <b><RenderUserName user={entry.expand?.user}/></b> bearbeiten </>}
|
||||
>
|
||||
<PocketBaseErrorAlert error={mutation.error}/>
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import {EventListModel, EventListSlotModel} from "@/models/EventTypes.ts";
|
||||
import {PocketBaseErrorAlert, usePB} from "@/lib/pocketbase.tsx";
|
||||
import {isNotEmpty, useForm} from "@mantine/form";
|
||||
import {useForm} from "@mantine/form";
|
||||
import {useMutation} from "@tanstack/react-query";
|
||||
import {useConfirmModal} from "@/components/ConfirmModal.tsx";
|
||||
import {Button, Center, Group, NumberInput} from "@mantine/core";
|
||||
|
@ -26,7 +26,11 @@ export default function UpsertEventListSlot({list, slot, onSuccess, onAbort}: {
|
|||
description: slot?.description ?? ""
|
||||
},
|
||||
validate: {
|
||||
startDate: isNotEmpty("Startdatum muss angegeben werden"),
|
||||
startDate: (val) => {
|
||||
if (!val) {
|
||||
return "Startdatum muss angegeben werden"
|
||||
}
|
||||
},
|
||||
endDate: (val, values) => {
|
||||
if (!val) return "Enddatum muss angegeben werden"
|
||||
if (val < values.startDate) return "Enddatum muss nach dem Startdatum liegen"
|
||||
|
@ -61,14 +65,13 @@ export default function UpsertEventListSlot({list, slot, onSuccess, onAbort}: {
|
|||
})
|
||||
|
||||
const {toggleConfirmModal, ConfirmModal} = useConfirmModal({
|
||||
description: "Möchtest du diesen Slot wirklich löschen? Damit werden auch alle Einträge für diesen Slot gelöscht.",
|
||||
description: "Möchtest du diesen Slot wirklich löschen? Damit werden auch alle Anmeldungen für diesen Slot gelöscht.",
|
||||
onConfirm: deleteSlotMutation.mutate,
|
||||
})
|
||||
|
||||
const duration = formatDuration(formValues.values.startDate, formValues.values.endDate)
|
||||
|
||||
return <form className={"stack"} onSubmit={formValues.onSubmit(() => upsertSlotMutation.mutate())}>
|
||||
|
||||
<ConfirmModal/>
|
||||
|
||||
<PocketBaseErrorAlert error={upsertSlotMutation.error}/>
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import {useUser} from "@/lib/user.ts";
|
||||
import PromptLoginModal from "@/components/auth/modals/PromptLoginModal.tsx";
|
||||
import {Link, useNavigate} from "react-router-dom";
|
||||
import {useState} from "react";
|
||||
|
@ -27,10 +26,9 @@ import UserEntryRow from "@/pages/events/entries/UserEntryRow.tsx";
|
|||
|
||||
export default function UserEntries() {
|
||||
|
||||
const user = useUser()
|
||||
const navigate = useNavigate()
|
||||
|
||||
const {pb} = usePB()
|
||||
const {pb, user} = usePB()
|
||||
|
||||
const [page, setPage] = useState(1)
|
||||
|
||||
|
@ -44,8 +42,9 @@ export default function UserEntries() {
|
|||
queryKey: ["userEntries", user?.id, page, debouncedSearch, sortDirection],
|
||||
queryFn: async () => (
|
||||
await pb.collection("eventListSlotEntriesWithUser").getList(page, 50, {
|
||||
filter: `userId='${user?.id}'${debouncedSearch ? `&&event.name~'${debouncedSearch}'` : ""}`,
|
||||
sort: sortDirection === "ASC" ? "-event.startDate" : "event.startDate"
|
||||
filter: `user='${user?.id}'${debouncedSearch ? `&&event.name~'${debouncedSearch}'` : ""}`,
|
||||
sort: sortDirection === "ASC" ? "-event.startDate" : "event.startDate",
|
||||
expand: "user"
|
||||
})
|
||||
),
|
||||
enabled: !!user
|
||||
|
@ -62,7 +61,7 @@ export default function UserEntries() {
|
|||
<Breadcrumbs>{[
|
||||
{title: "Home", to: "/"},
|
||||
{title: "Events", to: "/events"},
|
||||
{title: "Meine Einträge", to: `/events/entries`},
|
||||
{title: "Anmeldungen", to: `/events/entries`},
|
||||
].map(({title, to}) => (
|
||||
<Anchor component={Link} to={to} key={title}>
|
||||
{title}
|
||||
|
@ -70,11 +69,11 @@ export default function UserEntries() {
|
|||
))}</Breadcrumbs>
|
||||
</div>
|
||||
<div className={"section"}>
|
||||
<Title order={1} c={"blue"}>Meine Einträge</Title>
|
||||
<Title order={1} c={"blue"}>Anmeldungen</Title>
|
||||
</div>
|
||||
<div className={"section-transparent"}>
|
||||
<ShowHelp>
|
||||
Auf dieser Seite findest du alle deine Einträge zu Events und kannst diese verwalten.
|
||||
Auf dieser Seite findest du alle deine Anmeldungen zu Events und kannst diese verwalten.
|
||||
</ShowHelp>
|
||||
</div>
|
||||
|
||||
|
@ -101,11 +100,11 @@ export default function UserEntries() {
|
|||
|
||||
<Group justify={"space-between"}>
|
||||
<Text c={"dimmed"} size={"xs"}>
|
||||
{eventSearch ? `Suche nach Einträgen für Event '${eventSearch}'` : "Alle deine Einträge"}
|
||||
{eventSearch ? `Suche nach Anmeldungen für Event '${eventSearch}'` : "Alle deine Anmeldungen"}
|
||||
</Text>
|
||||
|
||||
<Text c={"dimmed"} size={"xs"}>
|
||||
{entriesQuery.data?.totalItems ?? 0} {eventSearch ? "Ergebnisse" : "Einträge"}
|
||||
{entriesQuery.data?.totalItems ?? 0} {eventSearch ? "Ergebnisse" : "Anmeldungen"}
|
||||
</Text>
|
||||
</Group>
|
||||
|
||||
|
@ -116,7 +115,7 @@ export default function UserEntries() {
|
|||
<IconDatabaseOff/>
|
||||
</ThemeIcon>
|
||||
|
||||
<Title order={4} c={"dimmed"}>Keine Einträge vorhanden</Title>
|
||||
<Title order={4} c={"dimmed"}>Keine Anmeldungen vorhanden</Title>
|
||||
</Stack>
|
||||
}
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@ import {usePB} from "@/lib/pocketbase.tsx";
|
|||
import {
|
||||
UpdateEventListSlotEntryFormModal
|
||||
} from "@/pages/events/e/:eventId/EventLists/components/UpdateEventListSlotEntryFormModal.tsx";
|
||||
import {getUserName} from "@/components/auth/modals/util.tsx";
|
||||
|
||||
export default function UserEntryRow({entry, refetch}: {
|
||||
entry: EventListSlotEntriesWithUserModel,
|
||||
|
@ -42,7 +43,7 @@ export default function UserEntryRow({entry, refetch}: {
|
|||
|
||||
const {ConfirmModal, toggleConfirmModal} = useConfirmModal({
|
||||
title: 'Eintrag löschen',
|
||||
description: `Möchtest du den Eintrag von ${entry.userName} wirklich löschen?`,
|
||||
description: `Möchtest du den Eintrag von ${getUserName(entry.expand?.user)} wirklich löschen?`,
|
||||
onConfirm: () => deleteEntryMutation.mutate()
|
||||
})
|
||||
|
||||
|
@ -90,7 +91,7 @@ export default function UserEntryRow({entry, refetch}: {
|
|||
|
||||
<Group gap={"xs"}>
|
||||
<Tooltip
|
||||
label={delta.delta === "PAST" ? "Vergangene Einträge können nicht bearbeiten werden" : "Eintrag bearbeiten"}
|
||||
label={delta.delta === "PAST" ? "Vergangene Anmeldungen können nicht bearbeiten werden" : "Eintrag bearbeiten"}
|
||||
withArrow>
|
||||
<ActionIcon
|
||||
variant={"light"}
|
||||
|
@ -104,7 +105,7 @@ export default function UserEntryRow({entry, refetch}: {
|
|||
</Tooltip>
|
||||
|
||||
<Tooltip
|
||||
label={delta.delta === "PAST" ? "Vergangene Einträge können nicht gelöscht werden" : "Eintrag löschen"}
|
||||
label={delta.delta === "PAST" ? "Vergangene Anmeldungen können nicht gelöscht werden" : "Eintrag löschen"}
|
||||
withArrow>
|
||||
<ActionIcon
|
||||
variant={"light"}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import {EventListSlotsWithEntriesCountModel} from "@/models/EventTypes.ts";
|
||||
import {EventListModel, EventListSlotsWithEntriesCountModel} from "@/models/EventTypes.ts";
|
||||
import classes from "./EventListSlotView.module.css";
|
||||
import {RenderDateRange} from "@/pages/events/e/:eventId/EventLists/components/RenderDateRange.tsx";
|
||||
import EventListSlotProgress from "@/pages/events/e/:eventId/EventLists/components/EventListSlotProgress.tsx";
|
||||
|
@ -11,9 +11,9 @@ import {FieldEntries} from "@/components/formUtil/FromInput/types.ts";
|
|||
import {showSuccessNotification} from "@/components/util.tsx";
|
||||
import InnerHtml from "@/components/InnerHtml";
|
||||
import FormInput from "@/components/formUtil/FromInput";
|
||||
import {useUser} from "@/lib/user.ts";
|
||||
|
||||
export default function EventListSlotView({slot, refetch}: {
|
||||
export default function EventListSlotView({slot, list, refetch}: {
|
||||
list: EventListModel,
|
||||
slot: EventListSlotsWithEntriesCountModel,
|
||||
refetch: () => void
|
||||
}) {
|
||||
|
@ -31,17 +31,14 @@ export default function EventListSlotView({slot, refetch}: {
|
|||
]
|
||||
}
|
||||
|
||||
const {pb} = usePB()
|
||||
|
||||
const user = useUser()
|
||||
const {pb, user} = usePB()
|
||||
|
||||
const createEntryMutation = useMutation({
|
||||
mutationFn: async (data: FieldEntries) => {
|
||||
await pb.collection("eventListSlotEntries").create({
|
||||
entryQuestionData: data,
|
||||
eventListsSlot: slot.id,
|
||||
ldapUser: user?.realm === "STUVE" ? user.id : null,
|
||||
guestUser: user?.realm === "GUEST" ? user.id : null
|
||||
user: user?.id
|
||||
})
|
||||
},
|
||||
onSuccess: () => {
|
||||
|
@ -75,7 +72,12 @@ export default function EventListSlotView({slot, refetch}: {
|
|||
}
|
||||
|
||||
{
|
||||
slotIsInPast ? <>
|
||||
|
||||
list.onlyStuVeAccounts && user?.REALM !== "LDAP" ? <>
|
||||
<Alert color={"red"}>
|
||||
Für diese Liste sind nur StuVe-Accounts zugelassen
|
||||
</Alert>
|
||||
</> : slotIsInPast ? <>
|
||||
<Alert color={"red"}>
|
||||
Dieser Zeitslot ist bereits vorbei
|
||||
</Alert>
|
||||
|
|
|
@ -48,7 +48,7 @@ export default function EventListView({event, listId}: { event: EventModel, list
|
|||
|
||||
{!list.open && (
|
||||
<Alert icon={<IconLock/>}>
|
||||
Diese Liste ist nicht für Einträge geöffnet
|
||||
Diese Liste ist nicht für Anmeldungen geöffnet
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
|
@ -59,10 +59,25 @@ export default function EventListView({event, listId}: { event: EventModel, list
|
|||
</Alert>
|
||||
}
|
||||
|
||||
{
|
||||
!list.allowOverlappingEntries &&
|
||||
<Alert>
|
||||
Diese Liste erlaubt Anmeldungen zu überschneidenden Zeiten mit anderen Listen von diesem Event
|
||||
</Alert>
|
||||
}
|
||||
|
||||
{
|
||||
list.onlyStuVeAccounts &&
|
||||
<Alert>
|
||||
Diese Liste erlaubt nur Anmeldungen von Personen mit einem StuVe Account
|
||||
</Alert>
|
||||
}
|
||||
|
||||
|
||||
{
|
||||
(list.open && slots.length !== 0) && <>
|
||||
{slots.map(slot => (
|
||||
<EventListSlotView key={slot.id} slot={slot} refetch={() => {
|
||||
<EventListSlotView key={slot.id} list={list} slot={slot} refetch={() => {
|
||||
listSlotsQuery.refetch()
|
||||
}}/>
|
||||
))}
|
||||
|
|
|
@ -9,14 +9,12 @@ import {IconExternalLink, IconLogin, IconPencil, IconSectionSign} from "@tabler/
|
|||
import EventData from "@/pages/events/e/:eventId/EventComponents/EventData.tsx";
|
||||
import EventListView from "@/pages/events/s/EventListView.tsx";
|
||||
import {useEventRights} from "@/pages/events/util.ts";
|
||||
import {useUser} from "@/lib/user.ts";
|
||||
|
||||
|
||||
export default function SharedEvent() {
|
||||
|
||||
const {pb} = usePB()
|
||||
const {pb,user} = usePB()
|
||||
|
||||
const user = useUser()
|
||||
|
||||
const {eventId} = useParams() as { eventId: string }
|
||||
|
||||
|
|
|
@ -1,20 +1,21 @@
|
|||
import {EventModel} from "@/models/EventTypes.ts";
|
||||
import {usePB} from "@/lib/pocketbase.tsx";
|
||||
import {useSettings} from "@/lib/settings.ts";
|
||||
import {LdapGroupModel} from "@/models/AuthTypes.ts";
|
||||
|
||||
|
||||
export const useEventRights = (event?: EventModel) => {
|
||||
const {ldapUser} = usePB()
|
||||
const {user} = usePB()
|
||||
|
||||
const settings = useSettings()
|
||||
|
||||
const isStex = !!(
|
||||
ldapUser &&
|
||||
user &&
|
||||
settings?.stexGroupId?.value !== undefined &&
|
||||
ldapUser.expand?.memberOf?.map(g => g.cn).includes(settings.stexGroupId.value)
|
||||
user.expand?.memberOf?.map((g: LdapGroupModel) => g.cn).includes(settings.stexGroupId.value)
|
||||
)
|
||||
const isEventAdmin = !!(ldapUser && event && event.eventAdmins.includes(ldapUser.id))
|
||||
const isEventListAdmin = !!(ldapUser && event && event.eventListAdmins.includes(ldapUser.id))
|
||||
const isEventAdmin = !!(user && event && event.eventAdmins.includes(user.id))
|
||||
const isEventListAdmin = !!(user && event && event.eventListAdmins.includes(user.id))
|
||||
|
||||
return {
|
||||
canEditEvent: isEventAdmin || isStex,
|
||||
|
|
|
@ -3,14 +3,11 @@ import {Alert, Button, Group, ThemeIcon, Title} from "@mantine/core";
|
|||
import {IconConfetti, IconHandLoveYou, IconQrcode} from "@tabler/icons-react";
|
||||
import {usePB} from "@/lib/pocketbase.tsx";
|
||||
import {NavLink} from "react-router-dom";
|
||||
import {useUser} from "@/lib/user.ts";
|
||||
|
||||
|
||||
export default function HomePage() {
|
||||
|
||||
const {userRecord} = usePB()
|
||||
|
||||
const user = useUser()
|
||||
const {user} = usePB()
|
||||
|
||||
const nav = [
|
||||
{
|
||||
|
@ -34,10 +31,10 @@ export default function HomePage() {
|
|||
</ThemeIcon>
|
||||
Hallo
|
||||
|
||||
{user && `, ${user.name}`}
|
||||
{user && `, ${user.username}`}
|
||||
</Title>
|
||||
|
||||
{!userRecord && <>
|
||||
{!user && <>
|
||||
<Alert color={"blue"} title={"Willkommen bei der StuVe!"}>
|
||||
Um alle Funktionen nutzen zu können, logge dich bitte mit deinem StuVe IT Account ein.
|
||||
</Alert>
|
||||
|
|
Loading…
Reference in New Issue