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

This commit is contained in:
Valentin Kolb 2024-05-14 16:52:40 +02:00
parent 0fc99bd187
commit ad2a69e6fe
39 changed files with 334 additions and 216 deletions

View File

@ -52,8 +52,7 @@ Dieser Client ist ein React Hook und kann in jeder React Komponente verwendet we
import {usePB} from "@/lib/pocketbase" import {usePB} from "@/lib/pocketbase"
import {useQuery} from "@tanstack/react-query"; import {useQuery} from "@tanstack/react-query";
const {pb} = usePB() const {pb,user} = usePB()
const user = useUser()
const query = useQuery({ const query = useQuery({
queryKey: ["collection", id], queryKey: ["collection", id],

View File

@ -1,17 +1,17 @@
import {useMutation} from "@tanstack/react-query"; import {useMutation} from "@tanstack/react-query";
import {IconUsers} from "@tabler/icons-react"; import {IconUsers} from "@tabler/icons-react";
import {usePB} from "@/lib/pocketbase.tsx"; 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"; import RecordSearchInput, {GenericRecordSearchInputProps} from "../input/RecordSearchInput.tsx";
export default function LdapUserInput(props: GenericRecordSearchInputProps<LdapUserModel>) { export default function UserInput(props: GenericRecordSearchInputProps<UserModal>) {
const {pb} = usePB() const {pb} = usePB()
return ( return (
<RecordSearchInput <RecordSearchInput
<LdapUserModel> <UserModal>
recordToString={(user) => ({displayName: `${user.givenName} ${user.sn}`})} recordToString={(user) => ({displayName: `${user.givenName} ${user.sn}`})}
{...props} {...props}
placeholder={props.placeholder || "Suche nach Personen..."} placeholder={props.placeholder || "Suche nach Personen..."}
@ -23,7 +23,7 @@ export default function LdapUserInput(props: GenericRecordSearchInputProps<LdapU
return [] return []
} }
return (await pb.collection('ldap_users').getList(1, 5, { return (await pb.collection('users').getList(1, 5, {
filter: `(username ~ "${ filter: `(username ~ "${
search.trim().toLowerCase().split(" ").map(s => s.trim()).join(".") search.trim().toLowerCase().split(" ").map(s => s.trim()).join(".")
}%")`, }%")`,

View File

@ -1,18 +1,18 @@
import {LdapUserModel} from "@/models/AuthTypes.ts"; import {UserModal} from "@/models/AuthTypes.ts";
import {List} from "@mantine/core"; import {List} from "@mantine/core";
import {IconUser} from "@tabler/icons-react"; import {IconUser} from "@tabler/icons-react";
import {usePB} from "@/lib/pocketbase.tsx"; 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 <> return <>
<List size={"sm"} icon={<IconUser size={16}/>}> <List size={"sm"} icon={<IconUser size={16}/>}>
{ {
users.map((u) => ( users.map((u) => (
<List.Item key={u.id} fw={500} c={ldapUser && ldapUser.id == u.id ? "green" : ""}> <List.Item key={u.id} fw={500} c={user && user.id == u.id ? "green" : ""}>
{u.givenName} {u.sn} {u.username}
</List.Item> </List.Item>
)) ))
} }

View File

@ -8,7 +8,6 @@ import {showSuccessNotification} from "@/components/util.tsx";
import {useSearchParams} from "react-router-dom"; import {useSearchParams} from "react-router-dom";
import EmailSVG from "@/illustrations/email.svg?react" import EmailSVG from "@/illustrations/email.svg?react"
import {useUser} from "@/lib/user.ts";
import PromptLoginModal from "@/components/auth/modals/PromptLoginModal.tsx"; import PromptLoginModal from "@/components/auth/modals/PromptLoginModal.tsx";
export const CHANGE_EMAIL_TOKEN_KEY = "changeEmailToken" export const CHANGE_EMAIL_TOKEN_KEY = "changeEmailToken"
@ -31,7 +30,7 @@ const RequestEmailChangeModal = ({open, onClose}: {
const mutation = useMutation({ const mutation = useMutation({
mutationFn: async () => { mutationFn: async () => {
await pb.collection("guest_users").requestEmailChange(formValues.values.email) await pb.collection("users").requestEmailChange(formValues.values.email)
}, },
onSuccess: () => { onSuccess: () => {
formValues.reset() formValues.reset()
@ -96,7 +95,7 @@ const ConfirmEmailChangeModal = ({open, onClose, token}: {
const mutation = useMutation({ const mutation = useMutation({
mutationFn: async () => { mutationFn: async () => {
await pb.collection("guest_users").confirmEmailChange( await pb.collection("users").confirmEmailChange(
token, token,
formValues.values.password, formValues.values.password,
) )
@ -115,7 +114,7 @@ const ConfirmEmailChangeModal = ({open, onClose, token}: {
<form className={"stack"} onSubmit={formValues.onSubmit(() => mutation.mutate())}> <form className={"stack"} onSubmit={formValues.onSubmit(() => mutation.mutate())}>
<Center> <Center>
<EmailSVG height={"200px"} width={"200px"}/> <EmailSVG height={"200px"} width={"200px"}/>
</Center> </Center>
<PocketBaseErrorAlert error={mutation.error}/> <PocketBaseErrorAlert error={mutation.error}/>
@ -153,7 +152,7 @@ const ConfirmEmailChangeModal = ({open, onClose, token}: {
export default function ChangeEmailModal() { export default function ChangeEmailModal() {
const {value, handler} = useChangeEmail() const {value, handler} = useChangeEmail()
const user = useUser() const {user} = usePB()
const [searchParams] = useSearchParams() const [searchParams] = useSearchParams()

View File

@ -6,7 +6,6 @@ import {showErrorNotification, showSuccessNotification} from "@/components/util.
import {Alert, Button, Group, Modal, TextInput, Title} from "@mantine/core"; import {Alert, Button, Group, Modal, TextInput, Title} from "@mantine/core";
import {IconAt} from "@tabler/icons-react"; import {IconAt} from "@tabler/icons-react";
import {useLogin} from "@/components/auth/modals/hooks.ts"; import {useLogin} from "@/components/auth/modals/hooks.ts";
import {useUser} from "@/lib/user.ts";
export const EMAIL_TOKEN_KEY = "emailVerificationToken" export const EMAIL_TOKEN_KEY = "emailVerificationToken"
@ -29,13 +28,11 @@ export default function EmailTokenVerification() {
const [email, setEmail] = useState<string>("") const [email, setEmail] = useState<string>("")
const {pb, refreshUser} = usePB() const {pb, user, refreshUser} = usePB()
const user = useUser()
const verifyTokenMutation = useMutation({ const verifyTokenMutation = useMutation({
mutationFn: async (token: string) => { mutationFn: async (token: string) => {
await pb.collection("guest_users").confirmVerification(token) await pb.collection("users").confirmVerification(token)
}, },
onSuccess: () => { onSuccess: () => {
showSuccessNotification("E-Mail erfolgreich verifiziert") showSuccessNotification("E-Mail erfolgreich verifiziert")
@ -77,7 +74,7 @@ export default function EmailTokenVerification() {
<Button <Button
disabled={email.length === 0} disabled={email.length === 0}
onClick={() => { onClick={() => {
pb.collection("guest_users").requestVerification(email) pb.collection("users").requestVerification(email)
}} }}
> >
Token erneut senden Token erneut senden

View File

@ -30,7 +30,7 @@ const RequestResetPasswordModal = ({open, onClose}: {
const requestResetPasswordMutation = useMutation({ const requestResetPasswordMutation = useMutation({
mutationFn: async () => { mutationFn: async () => {
await pb.collection("guest_users").requestPasswordReset(formValues.values.email) await pb.collection("users").requestPasswordReset(formValues.values.email)
}, },
onSuccess: () => { onSuccess: () => {
formValues.reset() formValues.reset()
@ -84,7 +84,7 @@ const ResetPasswordModal = ({open, onClose, token}: {
onClose: () => void, onClose: () => void,
token: string token: string
}) => { }) => {
const {pb, userRecord} = usePB() const {pb, user} = usePB()
const {handler: loginHandler} = useLogin() const {handler: loginHandler} = useLogin()
@ -102,7 +102,7 @@ const ResetPasswordModal = ({open, onClose, token}: {
const requestResetPasswordMutation = useMutation({ const requestResetPasswordMutation = useMutation({
mutationFn: async () => { mutationFn: async () => {
await pb.collection("guest_users").confirmPasswordReset( await pb.collection("users").confirmPasswordReset(
token, token,
formValues.values.password, formValues.values.password,
formValues.values.passwordConfirm formValues.values.passwordConfirm
@ -111,7 +111,7 @@ const ResetPasswordModal = ({open, onClose, token}: {
onSuccess: () => { onSuccess: () => {
formValues.reset() formValues.reset()
showSuccessNotification("Passwort erfolgreich zurückgesetzt") showSuccessNotification("Passwort erfolgreich zurückgesetzt")
if (!userRecord) { if (!user) {
loginHandler.open() loginHandler.open()
} }
} }

View File

@ -25,7 +25,7 @@ export default function LoginModal() {
const {value, handler} = useLogin() const {value, handler} = useLogin()
const {ldapLogin, guestLogin, userRecord} = usePB() const {ldapLogin, guestLogin, user} = usePB()
const {handler: registerHandler} = useRegister() const {handler: registerHandler} = useRegister()
@ -56,7 +56,7 @@ export default function LoginModal() {
}) })
return <> 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())}> <form className={"stack"} onSubmit={formValues.onSubmit(() => loginMutation.mutate())}>
<Title ta={"center"} order={3}>StuVe IT Login</Title> <Title ta={"center"} order={3}>StuVe IT Login</Title>

View File

@ -11,7 +11,7 @@ import {Link} from "react-router-dom";
export default function RegisterModal() { export default function RegisterModal() {
const {value, handler} = useRegister() const {value, handler} = useRegister()
const {userRecord, pb} = usePB() const {user, pb} = usePB()
const formValues = useForm({ const formValues = useForm({
initialValues: { initialValues: {
@ -36,10 +36,11 @@ export default function RegisterModal() {
const registerMutation = useMutation({ const registerMutation = useMutation({
mutationFn: async () => { mutationFn: async () => {
await pb.collection("guest_users").create({ await pb.collection("users").create({
...formValues.values, ...formValues.values,
REALM: "GUEST"
}) })
await pb.collection("guest_users").requestVerification(formValues.values.email) await pb.collection("users").requestVerification(formValues.values.email)
}, },
onSuccess: () => { onSuccess: () => {
handler.close() handler.close()
@ -50,7 +51,7 @@ export default function RegisterModal() {
}) })
return <> return <>
<Modal size={"lg"} opened={value && !userRecord} onClose={handler.close} <Modal size={"lg"} opened={value && !user} onClose={handler.close}
title={"Gast Account anlegen"}> title={"Gast Account anlegen"}>
<form className={"stack"} onSubmit={formValues.onSubmit(() => registerMutation.mutate())}> <form className={"stack"} onSubmit={formValues.onSubmit(() => registerMutation.mutate())}>

View File

@ -11,7 +11,7 @@ import {
IconMailCog, IconMailCog,
IconPassword, IconPassword,
IconServer, IconServer,
IconServerOff IconServerOff, IconUser
} from "@tabler/icons-react"; } from "@tabler/icons-react";
import LdapGroupsDisplay from "@/components/auth/LdapGroupsDisplay.tsx"; import LdapGroupsDisplay from "@/components/auth/LdapGroupsDisplay.tsx";
@ -22,26 +22,44 @@ export default function UserMenuModal() {
const {handler: changeEmailHandler} = useChangeEmail() const {handler: changeEmailHandler} = useChangeEmail()
const {logout, apiIsHealthy, userRecord} = usePB() const {logout, apiIsHealthy, user} = usePB()
const {showHelp, toggleShowHelp} = useShowHelp() const {showHelp, toggleShowHelp} = useShowHelp()
const {showDebug, toggleShowDebug} = useShowDebug() const {showDebug, toggleShowDebug} = useShowDebug()
return <> 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}> <div className={classes.stack}>
<Title order={3}>Hallo {userRecord?.username}</Title> <Title order={3}>Hallo {user?.username}</Title>
<ShowDebug> <ShowDebug>
Datenbank ID: <Code>{userRecord?.id}</Code> Datenbank ID: <Code>{user?.id}</Code>
{userRecord?.objectGUID && <> <br/>
REALM: <Code>{user?.REALM}</Code>
{user?.objectGUID && <>
<br/> <br/>
GUID: <Code>{userRecord?.objectGUID}</Code> GUID: <Code>{user?.objectGUID}</Code>
</>} </>}
</ShowDebug> </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}> <div className={classes.row}>
<ThemeIcon <ThemeIcon
variant={"transparent"} variant={"transparent"}
@ -51,9 +69,11 @@ export default function UserMenuModal() {
</ThemeIcon> </ThemeIcon>
<Text> <Tooltip label={`Dein Email ist ${user?.emailVisibility ? "sichtbar" : "nicht sichtbar"}`}>
{userRecord?.email} <Text>
</Text> {user?.email}
</Text>
</Tooltip>
</div> </div>
<div className={classes.row}> <div className={classes.row}>
@ -65,10 +85,10 @@ export default function UserMenuModal() {
</ThemeIcon> </ThemeIcon>
<Text> <Text>
{userRecord?.accountExpires ? ( {user?.accountExpires ? (
new Date(userRecord?.accountExpires).getTime() > Date.now() ? ( new Date(user?.accountExpires).getTime() > Date.now() ? (
"Account ist aktiv und läuft am " + new Date(userRecord?.accountExpires).toLocaleDateString() + " ab" "Account ist aktiv und läuft am " + new Date(user?.accountExpires).toLocaleDateString() + " ab"
) : ( ) : (
"Account ist abgelaufen" "Account ist abgelaufen"
) )
@ -103,9 +123,9 @@ export default function UserMenuModal() {
</Text> </Text>
</div> </div>
{userRecord?.memberOf?.length && <> {(user?.memberOf?.length ?? 0) > 0 && <>
<Title order={5}>Deine Gruppen</Title> <Title order={5}>Deine Gruppen</Title>
<LdapGroupsDisplay groups={userRecord?.expand?.memberOf}/> <LdapGroupsDisplay groups={user?.expand?.memberOf}/>
</>} </>}
<Divider label={"Einstellungen"}/> <Divider label={"Einstellungen"}/>

View File

@ -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>
</>
}

View File

@ -1,12 +1,12 @@
import {Menu} from "@mantine/core"; import {Menu} from "@mantine/core";
import {NavLink} from "react-router-dom"; import {NavLink} from "react-router-dom";
import {Fragment} from "react"; import {Fragment} from "react";
import {IconConfetti, IconHome, IconQrcode} from "@tabler/icons-react"; import {IconConfetti, IconHome, IconList, IconQrcode} from "@tabler/icons-react";
const NavItems = [ const NavItems = [
{ {
section: "Seiten", section: "StuVe IT",
items: [ items: [
{ {
title: "Home", title: "Home",
@ -14,12 +14,29 @@ const NavItems = [
description: "Home", description: "Home",
link: "/" link: "/"
}, },
]
},
{
section: "Events",
items: [
{ {
title: "Events", title: "Übersicht",
icon: IconConfetti, icon: IconConfetti,
description: "Administration für StuVe Events.", description: "Übersicht über alle Events.",
link: "/events" link: "/events"
}, },
{
title: "Deine Anmeldungen",
icon: IconList,
description: "Deine Anmeldungen bei Events.",
link: "/events/entries"
},
]
},
{
section: "Tools",
items: [
{ {
title: "QR Code Generator", title: "QR Code Generator",
icon: IconQrcode, icon: IconQrcode,

View File

@ -7,7 +7,7 @@ import {useLogin, useUserMenu} from "@/components/auth/modals/hooks.ts";
export default function NavBar() { export default function NavBar() {
const {userRecord} = usePB() const {user} = usePB()
const {colorScheme, toggleColorScheme} = useMantineColorScheme() const {colorScheme, toggleColorScheme} = useMantineColorScheme()
@ -58,7 +58,7 @@ export default function NavBar() {
} }
</ActionIcon> </ActionIcon>
{userRecord ? {user ?
<ActionIcon <ActionIcon
variant={"transparent"} variant={"transparent"}
color={"gray"} color={"gray"}

View File

@ -5,7 +5,7 @@ import {useInterval} from "@mantine/hooks";
import {useQuery} from "@tanstack/react-query"; import {useQuery} from "@tanstack/react-query";
import {TypedPocketBase} from "@/models"; import {TypedPocketBase} from "@/models";
import {PB_BASE_URL, PB_STORAGE_KEY, PB_USER_COLLECTION} from "../../config.ts"; 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 {Alert, List} from "@mantine/core";
import {IconAlertTriangle} from "@tabler/icons-react"; import {IconAlertTriangle} from "@tabler/icons-react";
import {showSuccessNotification} from "@/components/util.tsx"; import {showSuccessNotification} from "@/components/util.tsx";
@ -104,7 +104,7 @@ const PocketData = () => {
}, [pb]) }, [pb])
const guestLogin = useCallback(async (usernameOrEmail: string, password: string) => { 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.clear()
pb.authStore.save(res.token, res.record) pb.authStore.save(res.token, res.record)
console.log(res.record) console.log(res.record)
@ -134,9 +134,7 @@ const PocketData = () => {
ldapLogin, ldapLogin,
guestLogin, guestLogin,
logout, logout,
userRecord: user as LdapUserModel | GuestUserModel | null, user: user as UserModal | null,
guestUser: user && !Object.keys(user).includes("objectGUID") ? user as GuestUserModel : null,
ldapUser: user && Object.keys(user).includes("objectGUID") ? user as LdapUserModel : null,
pb, pb,
refreshUser, refreshUser,
useSubscription, useSubscription,

View File

@ -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
}

View File

@ -1,21 +1,27 @@
import {AuthModel, RecordModel} from "pocketbase"; import {RecordModel} from "pocketbase";
export type UserModal = { export type UserModal = {
name: string;
verified: boolean;
realm: string;
email: string;
} & RecordModel
export type GuestUserModel = {
username: string; username: string;
verified: boolean; verified: boolean;
email: string; 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; cn: string;
dn: string; dn: string;
sn: string; sn: string;
@ -27,7 +33,7 @@ export type LdapUserModel = {
expand: { expand: {
memberOf: LdapGroupModel[] memberOf: LdapGroupModel[]
} }
} & AuthModel & RecordModel }
export type LdapGroupModel = { export type LdapGroupModel = {
description: string; description: string;

View File

@ -1,4 +1,4 @@
import {GuestUserModel, LdapUserModel} from "./AuthTypes.ts"; import {UserModal} from "./AuthTypes.ts";
import {RecordModel} from "pocketbase"; import {RecordModel} from "pocketbase";
import {FieldEntries} from "@/components/formUtil/FromInput/types.ts"; import {FieldEntries} from "@/components/formUtil/FromInput/types.ts";
import {FormSchema} from "@/components/formUtil/formBuilder/types.ts"; import {FormSchema} from "@/components/formUtil/formBuilder/types.ts";
@ -19,8 +19,8 @@ export type EventModel = {
defaultEntryQuestionSchema: FormSchema | null; defaultEntryQuestionSchema: FormSchema | null;
defaultEntryStatusSchema: FormSchema | null; defaultEntryStatusSchema: FormSchema | null;
expand?: { expand?: {
eventAdmins: LdapUserModel[] | null | []; eventAdmins: UserModal[] | null;
eventListAdmins: LdapUserModel[] | null | []; eventListAdmins: UserModal[] | null;
} }
} & RecordModel } & RecordModel
@ -34,6 +34,8 @@ export type EventListModel = {
description: string | null; description: string | null;
open: boolean | null; open: boolean | null;
favourite: boolean | null; favourite: boolean | null;
allowOverlappingEntries: boolean | null;
onlyStuVeAccounts: boolean | null;
event: string event: string
entryQuestionSchema: FormSchema | null entryQuestionSchema: FormSchema | null
entryStatusSchema: FormSchema | null; entryStatusSchema: FormSchema | null;
@ -63,20 +65,17 @@ export type EventListSlotEntryModel = {
entryQuestionData: FieldEntries; entryQuestionData: FieldEntries;
entryStatusData: FieldEntries | null; entryStatusData: FieldEntries | null;
eventListsSlot: string; eventListsSlot: string;
ldapUser: string | null user: string | null
guestUser: string | null
expand?: { expand?: {
eventListsSlot: EventListSlotModel; eventListsSlot: EventListSlotModel;
ldapUser: LdapUserModel | null; user: UserModal | null;
guestUser: GuestUserModel | null;
} }
} & RecordModel } & RecordModel
export type EventListSlotEntriesWithUserModel = export type EventListSlotEntriesWithUserModel =
EventListSlotEntryModel EventListSlotEntryModel
& { & {
userId: string; user: string;
userName: string;
listName: string, listName: string,
slotStartDate: string, slotStartDate: string,
slotEndDate: string, slotEndDate: string,
@ -85,6 +84,11 @@ export type EventListSlotEntriesWithUserModel =
listDescription: string | null, listDescription: string | null,
event: string, event: string,
eventName: string, eventName: string,
expand?: {
event: EventModel;
eventList: EventListModel;
user: UserModal;
}
} }
& Pick<EventModel, "defaultEntryQuestionSchema" | "defaultEntryStatusSchema"> & Pick<EventModel, "defaultEntryQuestionSchema" | "defaultEntryStatusSchema">
& Pick<EventListModel, "entryQuestionSchema" | "entryStatusSchema"> & Pick<EventListModel, "entryQuestionSchema" | "entryStatusSchema">

View File

@ -1,5 +1,5 @@
import PocketBase, {RecordModel, RecordService} from "pocketbase"; import PocketBase, {RecordModel, RecordService} from "pocketbase";
import {GuestUserModel, LdapGroupModel, LdapSyncLogModel, LdapUserModel, UserModal} from "./AuthTypes.ts"; import {LdapGroupModel, LdapSyncLogModel, UserModal} from "./AuthTypes.ts";
import { import {
EventListModel, EventListModel,
EventListSlotEntriesWithUserModel, EventListSlotEntriesWithUserModel,
@ -32,10 +32,6 @@ export interface TypedPocketBase extends PocketBase {
collection(idOrName: 'users'): RecordService<UserModal> 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_groups'): RecordService<LdapGroupModel>
collection(idOrName: 'ldap_sync_logs'): RecordService<LdapSyncLogModel> collection(idOrName: 'ldap_sync_logs'): RecordService<LdapSyncLogModel>

View File

@ -4,9 +4,9 @@ import {DateTimePicker} from "@mantine/dates";
import {IconCheck, IconInfoCircle, IconX} from "@tabler/icons-react"; import {IconCheck, IconInfoCircle, IconX} from "@tabler/icons-react";
import {useMutation} from "@tanstack/react-query"; import {useMutation} from "@tanstack/react-query";
import dayjs from "dayjs"; import dayjs from "dayjs";
import {LdapUserModel} from "@/models/AuthTypes.ts"; import {UserModal} from "@/models/AuthTypes.ts";
import {PocketBaseErrorAlert, usePB} from "@/lib/pocketbase.tsx"; 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 {EventModel} from "@/models/EventTypes.ts";
import ShowHelp from "@/components/ShowHelp.tsx"; import ShowHelp from "@/components/ShowHelp.tsx";
import {useSettings} from "@/lib/settings.ts"; import {useSettings} from "@/lib/settings.ts";
@ -23,7 +23,7 @@ export default function CreateEvent({onSuccess, onAbort}: {
onSuccess: (event: EventModel) => void, onSuccess: (event: EventModel) => void,
onAbort?: () => void onAbort?: () => void
}) { }) {
const {ldapUser, pb} = usePB() const {user, pb} = usePB()
const settings = useSettings() const settings = useSettings()
const stuveQuestions = JSON.parse(settings?.stuveEventQuestions?.value ?? `{ "fields":[] }`) as FormSchema const stuveQuestions = JSON.parse(settings?.stuveEventQuestions?.value ?? `{ "fields":[] }`) as FormSchema
@ -33,7 +33,7 @@ export default function CreateEvent({onSuccess, onAbort}: {
name: "", name: "",
startDate: null, startDate: null,
endDate: null, endDate: null,
eventAdmins: [ldapUser] as LdapUserModel[], eventAdmins: [user] as UserModal[],
isStuveEvent: true, isStuveEvent: true,
}, },
validate: { validate: {
@ -46,7 +46,7 @@ export default function CreateEvent({onSuccess, onAbort}: {
const createEventMutation = useMutation({ const createEventMutation = useMutation({
mutationFn: async () => { mutationFn: async () => {
if (!ldapUser) { if (!user || user.REALM === "LDAP") {
throw new Error("Nur mit StuVe IT Account eingeloggte Personen können Events erstellen") throw new Error("Nur mit StuVe IT Account eingeloggte Personen können Events erstellen")
} }
return await pb.collection("events").create({ return await pb.collection("events").create({
@ -93,7 +93,7 @@ export default function CreateEvent({onSuccess, onAbort}: {
</Grid.Col> </Grid.Col>
<Grid.Col span={{base: 12, sm: 6}}> <Grid.Col span={{base: 12, sm: 6}}>
<LdapUserInput <UserInput
required required
label={"Event-Admins"} label={"Event-Admins"}
description={"Die Event-Admins können das Event bearbeiten."} description={"Die Event-Admins können das Event bearbeiten."}

View File

@ -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 {EventCalendar} from "./EventCalendar.tsx";
import {EventList} from "./EventList.tsx"; import {EventList} from "./EventList.tsx";
import {Anchor, Breadcrumbs, Button, Collapse} from "@mantine/core"; import {Anchor, Breadcrumbs, Button, Collapse} from "@mantine/core";
@ -10,7 +10,7 @@ import {Link, useNavigate} from "react-router-dom";
export default function EventOverview() { export default function EventOverview() {
const {ldapUser} = usePB() const {user} = usePB()
const [showCreateEvent, showCreateEventHandler] = useDisclosure(false) const [showCreateEvent, showCreateEventHandler] = useDisclosure(false)
const navigate = useNavigate(); const navigate = useNavigate();
@ -30,7 +30,7 @@ export default function EventOverview() {
</div> </div>
{ {
ldapUser && <> (user && user.REALM === "LDAP") && <>
<Collapse in={showCreateEvent} className={"section"}> <Collapse in={showCreateEvent} className={"section"}>
<CreateEvent <CreateEvent
onAbort={showCreateEventHandler.close} onAbort={showCreateEventHandler.close}
@ -39,6 +39,15 @@ export default function EventOverview() {
</Collapse> </Collapse>
{!showCreateEvent && <div className={"section-transparent group"} style={{gap: 0}}> {!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 <Button
leftSection={<IconPlus/>} leftSection={<IconPlus/>}
variant={"transparent"} variant={"transparent"}

View File

@ -2,7 +2,7 @@ import {EventModel} from "@/models/EventTypes.ts";
import classes from "../EditEventRouter.module.css"; import classes from "../EditEventRouter.module.css";
import {IconAdjustments, IconCalendar, IconHourglass, IconList, IconMap, IconSparkles} from "@tabler/icons-react"; import {IconAdjustments, IconCalendar, IconHourglass, IconList, IconMap, IconSparkles} from "@tabler/icons-react";
import {areDatesSame, humanDeltaFromNow, pprintDate} from "@/lib/datetime.ts"; 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"; import {Text, ThemeIcon, Title} from "@mantine/core";
/** /**
@ -86,7 +86,7 @@ export default function EventData({event, hideHeader}: { event: EventModel, hide
<IconAdjustments size={16}/> <IconAdjustments size={16}/>
Event Admins Event Admins
</div> </div>
<LdapUsersDisplay users={event.expand.eventAdmins}/> <UsersDisplay users={event.expand.eventAdmins}/>
</div> </div>
} }
@ -97,7 +97,7 @@ export default function EventData({event, hideHeader}: { event: EventModel, hide
<IconList size={16}/> <IconList size={16}/>
Listen Admins Listen Admins
</div> </div>
<LdapUsersDisplay users={event.expand.eventListAdmins}/> <UsersDisplay users={event.expand.eventListAdmins}/>
</div> </div>
} }
</div> </div>

View File

@ -1,9 +1,9 @@
import {EventModel} from "@/models/EventTypes.ts"; import {EventModel} from "@/models/EventTypes.ts";
import {useForm} from "@mantine/form"; 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 {PocketBaseErrorAlert, usePB} from "@/lib/pocketbase.tsx";
import {Button, Group, Title} from "@mantine/core"; 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 {useMutation} from "@tanstack/react-query";
import {queryClient} from "@/main.tsx"; import {queryClient} from "@/main.tsx";
import {showSuccessNotification} from "@/components/util.tsx"; import {showSuccessNotification} from "@/components/util.tsx";
@ -15,12 +15,12 @@ import ShowHelp from "@/components/ShowHelp.tsx";
*/ */
export default function EditEventMembers({event}: { event: EventModel }) { export default function EditEventMembers({event}: { event: EventModel }) {
const {ldapUser, pb} = usePB() const {user, pb} = usePB()
const formValues = useForm({ const formValues = useForm({
initialValues: { initialValues: {
eventAdmins: event?.expand?.eventAdmins ?? [ldapUser] as LdapUserModel[], eventAdmins: event?.expand?.eventAdmins ?? [user] as UserModal[],
eventListAdmins: event?.expand?.eventListAdmins ?? [] as LdapUserModel[], eventListAdmins: event?.expand?.eventListAdmins ?? [] as UserModal[],
}, },
validate: { validate: {
eventAdmins: (value) => value.length > 0 ? null : "Es muss mindestens ein Admin ausgewählt werden.", 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({ const editMutation = useMutation({
mutationFn: async () => { mutationFn: async () => {
return await pb.collection("events").update(event.id, { 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) eventListAdmins: formValues.values.eventListAdmins.map((member) => member.id)
}) })
}, },
@ -66,7 +66,7 @@ export default function EditEventMembers({event}: { event: EventModel }) {
<PocketBaseErrorAlert error={editMutation.error}/> <PocketBaseErrorAlert error={editMutation.error}/>
<LdapUserInput <UserInput
required required
label={"Event Admins"} label={"Event Admins"}
description={"Die Event-Admins können das ganze Event bearbeiten."} 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)} setSelectedRecords={(records) => formValues.setFieldValue("eventAdmins", records)}
/> />
<LdapUserInput <UserInput
label={"Listen Admins"} label={"Listen Admins"}
description={"Die Listen Admins können Listen bearbeiten und verwalten."} description={"Die Listen Admins können Listen bearbeiten und verwalten."}
error={formValues.errors.eventListAdmins} error={formValues.errors.eventListAdmins}

View File

@ -1,6 +1,14 @@
import {useQuery} from "@tanstack/react-query"; import {useQuery} from "@tanstack/react-query";
import {Alert, Breadcrumbs, Button, Group, LoadingOverlay, Title} from "@mantine/core"; 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 {Link, Navigate, NavLink, Route, Routes, useParams} from "react-router-dom";
import InnerHtml from "@/components/InnerHtml"; import InnerHtml from "@/components/InnerHtml";
import {EventModel} from "@/models/EventTypes.ts"; import {EventModel} from "@/models/EventTypes.ts";
@ -11,6 +19,7 @@ import EventListEntryQuestionSettings
from "@/pages/events/e/:eventId/EventLists/:listId/EventListSettings/EventListEntryQuestionSettings.tsx"; from "@/pages/events/e/:eventId/EventLists/:listId/EventListSettings/EventListEntryQuestionSettings.tsx";
import EventListEntryStatusSettings import EventListEntryStatusSettings
from "@/pages/events/e/:eventId/EventLists/:listId/EventListSettings/EventListEntryStatusSettings.tsx"; from "@/pages/events/e/:eventId/EventLists/:listId/EventListSettings/EventListEntryStatusSettings.tsx";
import TextWithIcon from "@/components/layout/TextWithIcon";
export default function EventListRouter({event}: { event: EventModel }) { export default function EventListRouter({event}: { event: EventModel }) {
@ -48,10 +57,19 @@ export default function EventListRouter({event}: { event: EventModel }) {
</Link> </Link>
</Breadcrumbs> </Breadcrumbs>
<Alert color={list.open ? "green" : "red"} <Alert color={list.open ? "green" : "red"}>
icon={list.open ? <IconLockOpen/> : <IconLock/>} <TextWithIcon icon={list.open ? <IconLockOpen size={16}/> : <IconLock size={16}/>}>
> Liste ist für Anmeldungen <b>{list.open ? "geöffnet" : "geschlossen"}</b>
{list.open ? "Liste ist für Anmeldungen geöffnet" : "Liste ist für Anmeldungen geschlossen"} </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> </Alert>
<div className={"section"}> <div className={"section"}>

View File

@ -1,14 +1,15 @@
import {EventListModel, EventModel} from "@/models/EventTypes.ts"; import {EventListModel, EventModel} from "@/models/EventTypes.ts";
import {useForm} from "@mantine/form"; 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 TextEditor from "@/components/input/Editor";
import {useMutation} from "@tanstack/react-query"; import {useMutation} from "@tanstack/react-query";
import {PocketBaseErrorAlert, usePB} from "@/lib/pocketbase.tsx"; import {PocketBaseErrorAlert, usePB} from "@/lib/pocketbase.tsx";
import {queryClient} from "@/main.tsx"; import {queryClient} from "@/main.tsx";
import {showSuccessNotification} from "@/components/util.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 {useNavigate} from "react-router-dom";
import {useConfirmModal} from "@/components/ConfirmModal.tsx"; import {useConfirmModal} from "@/components/ConfirmModal.tsx";
import {CheckboxCard} from "@/components/input/CheckboxCard";
export default function EventListSettings({list, event}: { list: EventListModel, event: EventModel }) { export default function EventListSettings({list, event}: { list: EventListModel, event: EventModel }) {
const {pb} = usePB() const {pb} = usePB()
@ -18,6 +19,8 @@ export default function EventListSettings({list, event}: { list: EventListModel,
initialValues: { initialValues: {
name: list.name, name: list.name,
open: list.open, open: list.open,
allowOverlappingEntries: list.allowOverlappingEntries,
onlyStuVeAccounts: list.onlyStuVeAccounts,
favorite: list.favorite, favorite: list.favorite,
description: list.description || "" description: list.description || ""
} }
@ -26,7 +29,7 @@ export default function EventListSettings({list, event}: { list: EventListModel,
const {ConfirmModal, toggleConfirmModal} = useConfirmModal({ const {ConfirmModal, toggleConfirmModal} = useConfirmModal({
onConfirm: () => deleteMutation.mutate(), onConfirm: () => deleteMutation.mutate(),
description: "Bist du sicher, dass du diese Liste löschen möchtest? " + 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.", "Diese Aktion kann nicht rückgängig gemacht werden.",
}) })
@ -68,23 +71,38 @@ export default function EventListSettings({list, event}: { list: EventListModel,
label={"Name"} label={"Name"}
placeholder={"Name der Liste"} placeholder={"Name der Liste"}
required 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")} {...formValues.getInputProps("name")}
/> />
<Switch <CheckboxCard
onLabel={<IconStarFilled size={16}/>} label={"Anmeldungen erlauben"}
offLabel={<IconStarOff size={16}/>} description={"Ob sich Personen für Zeitslots dieser Liste anmelden können"}
label={"Liste favorisiert"} {...formValues.getInputProps("open", {type: "checkbox"})}
color={"green"}
{...formValues.getInputProps("favorite", {type: "checkbox"})}
/> />
<Switch <CheckboxCard
onLabel={<IconLockOpen size={16}/>} label={"Nur StuVe Accounts"}
offLabel={<IconLock size={16}/>} description={"Nur Personen mit StuVe IT Account können sich für diese Liste Anmelden"}
label={"Anmeldungen erlauben"} {...formValues.getInputProps("onlyStuVeAccounts", {type: "checkbox"})}
color={"green"} />
{...formValues.getInputProps("open", {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 <TextEditor
@ -95,6 +113,7 @@ export default function EventListSettings({list, event}: { list: EventListModel,
/> />
<Group> <Group>
<Button <Button
variant={"outline"} variant={"outline"}
color={"red"} color={"red"}

View File

@ -26,6 +26,7 @@ export const EventListSlotEntriesTable = ({slot, list, event, refetch, visible}:
const res = await pb.collection("eventListSlotEntriesWithUser").getList(page, 50, { const res = await pb.collection("eventListSlotEntriesWithUser").getList(page, 50, {
filter: `eventListsSlot='${slot.id}'`, filter: `eventListsSlot='${slot.id}'`,
expand: "user"
}) })
return { return {
@ -43,7 +44,7 @@ export const EventListSlotEntriesTable = ({slot, list, event, refetch, visible}:
if (query.isLoading) { if (query.isLoading) {
return <Stack align={"center"} gap={"xs"}> return <Stack align={"center"} gap={"xs"}>
<Loader size={"sm"}/> <Loader size={"sm"}/>
<Text size={"xs"} c={"dimmed"}>Einträge werden geladen</Text> <Text size={"xs"} c={"dimmed"}>Anmeldungen werden geladen</Text>
</Stack> </Stack>
} }
@ -52,7 +53,7 @@ export const EventListSlotEntriesTable = ({slot, list, event, refetch, visible}:
<ThemeIcon variant={"transparent"} color={"gray"} size={"md"}> <ThemeIcon variant={"transparent"} color={"gray"} size={"md"}>
<IconDatabaseOff/> <IconDatabaseOff/>
</ThemeIcon> </ThemeIcon>
<Text size={"xs"} c={"dimmed"}>Keine Einträge vorhanden</Text> <Text size={"xs"} c={"dimmed"}>Keine Anmeldungen vorhanden</Text>
</Stack> </Stack>
} }
@ -77,7 +78,7 @@ export const EventListSlotEntriesTable = ({slot, list, event, refetch, visible}:
<div className={classes.bottomRow}> <div className={classes.bottomRow}>
<Text size={"xs"} c={"dimmed"}> <Text size={"xs"} c={"dimmed"}>
{query.data?.totalItems} Einträge {query.data?.totalItems} Anmeldungen
</Text> </Text>
<Pagination size={"xs"} value={page} onChange={setPage} total={query.data?.totalPages ?? 1}/> <Pagination size={"xs"} value={page} onChange={setPage} total={query.data?.totalPages ?? 1}/>
</div> </div>

View File

@ -11,6 +11,7 @@ import {IconCheckupList, IconForms, IconUserMinus, IconUserPlus} from "@tabler/i
import {renderEntries} from "@/components/formUtil/formTable"; import {renderEntries} from "@/components/formUtil/formTable";
import RenderCell from "@/components/formUtil/formTable/RenderCell.tsx"; import RenderCell from "@/components/formUtil/formTable/RenderCell.tsx";
import EditSlotEntryMenu from "@/pages/events/e/:eventId/EventLists/components/EditSlotEntryMenu.tsx"; import EditSlotEntryMenu from "@/pages/events/e/:eventId/EventLists/components/EditSlotEntryMenu.tsx";
import {RenderUserName} from "@/components/auth/modals/util.tsx";
export const EventListSlotEntryDetails = ({entry}: { export const EventListSlotEntryDetails = ({entry}: {
@ -77,9 +78,7 @@ export const EventListSlotEntryRow = ({entry, refetch}: {
event: EventModel, event: EventModel,
refetch: () => void refetch: () => void
}) => { }) => {
const [expanded, expandedHandler] = useDisclosure(false) const [expanded, expandedHandler] = useDisclosure(false)
return <div className={classes.entryContainer} aria-expanded={expanded}> return <div className={classes.entryContainer} aria-expanded={expanded}>
<div className={classes.entryRow}> <div className={classes.entryRow}>
<Tooltip label={"Details anzeigen"} withArrow> <Tooltip label={"Details anzeigen"} withArrow>
@ -88,7 +87,7 @@ export const EventListSlotEntryRow = ({entry, refetch}: {
</ActionIcon> </ActionIcon>
</Tooltip> </Tooltip>
<div>{`${entry.userName}`}</div> <div><RenderUserName user={entry.expand?.user}/></div>
<Group gap={4} justify="right" wrap="nowrap"> <Group gap={4} justify="right" wrap="nowrap">
<EditSlotEntryMenu entry={entry} refetch={refetch}/> <EditSlotEntryMenu entry={entry} refetch={refetch}/>

View File

@ -10,6 +10,7 @@ import {
import {useDisclosure} from "@mantine/hooks"; import {useDisclosure} from "@mantine/hooks";
import {RenderDateRange} from "@/pages/events/e/:eventId/EventLists/components/RenderDateRange.tsx"; import {RenderDateRange} from "@/pages/events/e/:eventId/EventLists/components/RenderDateRange.tsx";
import EditSlotEntryMenu from "@/pages/events/e/:eventId/EventLists/components/EditSlotEntryMenu.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}: { export default function EventListSearchResult({entry, refetch}: {
entry: EventListSlotEntriesWithUserModel, entry: EventListSlotEntriesWithUserModel,
@ -32,7 +33,7 @@ export default function EventListSearchResult({entry, refetch}: {
<IconUser/> <IconUser/>
</ThemeIcon> </ThemeIcon>
}> }>
{entry.userName} <RenderUserName user={entry.expand?.user}/>
</TextWithIcon> </TextWithIcon>
<TextWithIcon icon={ <TextWithIcon icon={

View File

@ -41,7 +41,7 @@ export default function ListSearch({event}: { event: EventModel }) {
const filter: string[] = [`event='${event.id}'`] const filter: string[] = [`event='${event.id}'`]
if (debouncedSearchQueryString) { if (debouncedSearchQueryString) {
filter.push(`userName ~ '${debouncedSearchQueryString}'`) filter.push(`user.username ~ '${debouncedSearchQueryString.trim().replace(" ", ".")}'`)
} }
if (selectedLists.length > 0) { if (selectedLists.length > 0) {
@ -49,7 +49,8 @@ export default function ListSearch({event}: { event: EventModel }) {
} }
return await pb.collection("eventListSlotEntriesWithUser").getList(1, 50, { 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 ..."} placeholder={"Nach Namen suchen ..."}
leftSection={<IconUserSearch/>} leftSection={<IconUserSearch/>}
rightSection={searchQuery.isLoading ? <Loader size={"xs"}/> : ( rightSection={searchQuery.isLoading ? <Loader size={"xs"}/> : (
<Tooltip label={"Einträge Filtern"} withArrow> <Tooltip label={"Anmeldungen Filtern"} withArrow>
<ActionIcon <ActionIcon
onClick={showFilterHandler.toggle} onClick={showFilterHandler.toggle}
variant={"transparent"} variant={"transparent"}
@ -75,7 +76,7 @@ export default function ListSearch({event}: { event: EventModel }) {
searchQuery searchQuery
.data .data
?.items ?.items
.map(e => e.userName) .map(e => e.id)
.filter(onlyUnique) ?? [] .filter(onlyUnique) ?? []
} }
value={searchQueryString} onChange={setSearchQueryString} value={searchQueryString} onChange={setSearchQueryString}
@ -86,17 +87,17 @@ export default function ListSearch({event}: { event: EventModel }) {
event={event} event={event}
selectedRecords={selectedLists} selectedRecords={selectedLists}
setSelectedRecords={setSelectedLists} setSelectedRecords={setSelectedLists}
placeholder={"Einträge nur für bestimmte Listen anzeigen"} placeholder={"Anmeldungen nur für bestimmte Listen anzeigen"}
/> />
</Collapse> </Collapse>
<Group justify={"space-between"}> <Group justify={"space-between"}>
<Text c={"dimmed"} size={"xs"}> <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>
<Text c={"dimmed"} size={"xs"}> <Text c={"dimmed"} size={"xs"}>
{searchQuery.data?.totalItems ?? 0} {searchQueryString ? "Ergebnisse" : "Einträge"} {searchQuery.data?.totalItems ?? 0} {searchQueryString ? "Ergebnisse" : "Anmeldungen"}
</Text> </Text>
</Group> </Group>

View File

@ -15,6 +15,7 @@ import {
} from "@/pages/events/e/:eventId/EventLists/components/UpdateEventListSlotEntryFormModal.tsx"; } from "@/pages/events/e/:eventId/EventLists/components/UpdateEventListSlotEntryFormModal.tsx";
import {ActionIcon, Button, Menu} from "@mantine/core"; import {ActionIcon, Button, Menu} from "@mantine/core";
import {IconArrowsMove, IconCheckupList, IconForms, IconSettings, IconTrash} from "@tabler/icons-react"; import {IconArrowsMove, IconCheckupList, IconForms, IconSettings, IconTrash} from "@tabler/icons-react";
import {getUserName} from "@/components/auth/modals/util.tsx";
export default function EditSlotEntryMenu({entry, refetch}: { export default function EditSlotEntryMenu({entry, refetch}: {
refetch: () => void, refetch: () => void,
@ -41,7 +42,7 @@ export default function EditSlotEntryMenu({entry, refetch}: {
const {ConfirmModal, toggleConfirmModal} = useConfirmModal({ const {ConfirmModal, toggleConfirmModal} = useConfirmModal({
title: 'Eintrag löschen', 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() onConfirm: () => deleteEntryMutation.mutate()
}) })

View File

@ -5,6 +5,7 @@ import {showSuccessNotification} from "@/components/util.tsx";
import {Alert, Button, Group, Modal, Select} from "@mantine/core"; import {Alert, Button, Group, Modal, Select} from "@mantine/core";
import {useEffect, useState} from "react"; import {useEffect, useState} from "react";
import {pprintDateTime} from "@/lib/datetime.ts"; import {pprintDateTime} from "@/lib/datetime.ts";
import {getUserName, RenderUserName} from "@/components/auth/modals/util.tsx";
export const MoveEventListSlotEntryModal = ({opened, close, refetch, entry}: { export const MoveEventListSlotEntryModal = ({opened, close, refetch, entry}: {
opened: boolean, opened: boolean,
@ -30,7 +31,7 @@ export const MoveEventListSlotEntryModal = ({opened, close, refetch, entry}: {
}) })
}, },
onSuccess: () => { onSuccess: () => {
showSuccessNotification(`Eintrag von ${entry.userName} erfolgreich verschoben`) showSuccessNotification(`Eintrag von ${getUserName(entry.expand?.user)} erfolgreich verschoben`)
slotsQuery.refetch() slotsQuery.refetch()
listsQuery.refetch() listsQuery.refetch()
refetch() refetch()
@ -66,7 +67,7 @@ export const MoveEventListSlotEntryModal = ({opened, close, refetch, entry}: {
size={"lg"} size={"lg"}
opened={opened} opened={opened}
onClose={close} onClose={close}
title={<> Eintrag von <b>{entry.userName}</b> verschieben </>} title={<> Eintrag von <b><RenderUserName user={entry.expand?.user}/></b> verschieben </>}
> >
<form <form
onSubmit={(e) => { onSubmit={(e) => {

View File

@ -5,6 +5,7 @@ import {FieldEntries} from "@/components/formUtil/FromInput/types.ts";
import {showSuccessNotification} from "@/components/util.tsx"; import {showSuccessNotification} from "@/components/util.tsx";
import {Modal} from "@mantine/core"; import {Modal} from "@mantine/core";
import FormInput from "@/components/formUtil/FromInput"; import FormInput from "@/components/formUtil/FromInput";
import {getUserName, RenderUserName} from "@/components/auth/modals/util.tsx";
export const UpdateEventListSlotEntryFormModal = ({opened, close, refetch, entry}: { export const UpdateEventListSlotEntryFormModal = ({opened, close, refetch, entry}: {
opened: boolean, opened: boolean,
@ -23,7 +24,7 @@ export const UpdateEventListSlotEntryFormModal = ({opened, close, refetch, entry
}) })
}, },
onSuccess: () => { onSuccess: () => {
showSuccessNotification(`Formular Daten von ${entry.userName} erfolgreich aktualisiert`) showSuccessNotification(`Formular Daten von ${getUserName(entry.expand?.user)} erfolgreich aktualisiert`)
refetch() refetch()
close() close()
} }
@ -38,7 +39,7 @@ export const UpdateEventListSlotEntryFormModal = ({opened, close, refetch, entry
size={"lg"} size={"lg"}
opened={opened} opened={opened}
onClose={close} 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}/> <PocketBaseErrorAlert error={mutation.error}/>

View File

@ -7,6 +7,7 @@ import {Alert, Modal} from "@mantine/core";
import InnerHtml from "@/components/InnerHtml"; import InnerHtml from "@/components/InnerHtml";
import {RenderDateRange} from "./RenderDateRange.tsx"; import {RenderDateRange} from "./RenderDateRange.tsx";
import FormInput from "@/components/formUtil/FromInput"; import FormInput from "@/components/formUtil/FromInput";
import {getUserName, RenderUserName} from "@/components/auth/modals/util.tsx";
export const UpdateEventListSlotEntryStatusModal = ({opened, close, refetch, entry}: { export const UpdateEventListSlotEntryStatusModal = ({opened, close, refetch, entry}: {
opened: boolean, opened: boolean,
@ -29,7 +30,7 @@ export const UpdateEventListSlotEntryStatusModal = ({opened, close, refetch, ent
}) })
}, },
onSuccess: () => { onSuccess: () => {
showSuccessNotification(`Status von ${entry.userName} erfolgreich aktualisiert`) showSuccessNotification(`Status von ${getUserName(entry.expand?.user)} erfolgreich aktualisiert`)
refetch() refetch()
close() close()
} }
@ -39,7 +40,7 @@ export const UpdateEventListSlotEntryStatusModal = ({opened, close, refetch, ent
size={"lg"} size={"lg"}
opened={opened} opened={opened}
onClose={close} 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}/> <PocketBaseErrorAlert error={mutation.error}/>

View File

@ -1,6 +1,6 @@
import {EventListModel, EventListSlotModel} from "@/models/EventTypes.ts"; import {EventListModel, EventListSlotModel} from "@/models/EventTypes.ts";
import {PocketBaseErrorAlert, usePB} from "@/lib/pocketbase.tsx"; 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 {useMutation} from "@tanstack/react-query";
import {useConfirmModal} from "@/components/ConfirmModal.tsx"; import {useConfirmModal} from "@/components/ConfirmModal.tsx";
import {Button, Center, Group, NumberInput} from "@mantine/core"; import {Button, Center, Group, NumberInput} from "@mantine/core";
@ -26,7 +26,11 @@ export default function UpsertEventListSlot({list, slot, onSuccess, onAbort}: {
description: slot?.description ?? "" description: slot?.description ?? ""
}, },
validate: { validate: {
startDate: isNotEmpty("Startdatum muss angegeben werden"), startDate: (val) => {
if (!val) {
return "Startdatum muss angegeben werden"
}
},
endDate: (val, values) => { endDate: (val, values) => {
if (!val) return "Enddatum muss angegeben werden" if (!val) return "Enddatum muss angegeben werden"
if (val < values.startDate) return "Enddatum muss nach dem Startdatum liegen" 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({ 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, onConfirm: deleteSlotMutation.mutate,
}) })
const duration = formatDuration(formValues.values.startDate, formValues.values.endDate) const duration = formatDuration(formValues.values.startDate, formValues.values.endDate)
return <form className={"stack"} onSubmit={formValues.onSubmit(() => upsertSlotMutation.mutate())}> return <form className={"stack"} onSubmit={formValues.onSubmit(() => upsertSlotMutation.mutate())}>
<ConfirmModal/> <ConfirmModal/>
<PocketBaseErrorAlert error={upsertSlotMutation.error}/> <PocketBaseErrorAlert error={upsertSlotMutation.error}/>

View File

@ -1,4 +1,3 @@
import {useUser} from "@/lib/user.ts";
import PromptLoginModal from "@/components/auth/modals/PromptLoginModal.tsx"; import PromptLoginModal from "@/components/auth/modals/PromptLoginModal.tsx";
import {Link, useNavigate} from "react-router-dom"; import {Link, useNavigate} from "react-router-dom";
import {useState} from "react"; import {useState} from "react";
@ -27,10 +26,9 @@ import UserEntryRow from "@/pages/events/entries/UserEntryRow.tsx";
export default function UserEntries() { export default function UserEntries() {
const user = useUser()
const navigate = useNavigate() const navigate = useNavigate()
const {pb} = usePB() const {pb, user} = usePB()
const [page, setPage] = useState(1) const [page, setPage] = useState(1)
@ -44,8 +42,9 @@ export default function UserEntries() {
queryKey: ["userEntries", user?.id, page, debouncedSearch, sortDirection], queryKey: ["userEntries", user?.id, page, debouncedSearch, sortDirection],
queryFn: async () => ( queryFn: async () => (
await pb.collection("eventListSlotEntriesWithUser").getList(page, 50, { await pb.collection("eventListSlotEntriesWithUser").getList(page, 50, {
filter: `userId='${user?.id}'${debouncedSearch ? `&&event.name~'${debouncedSearch}'` : ""}`, filter: `user='${user?.id}'${debouncedSearch ? `&&event.name~'${debouncedSearch}'` : ""}`,
sort: sortDirection === "ASC" ? "-event.startDate" : "event.startDate" sort: sortDirection === "ASC" ? "-event.startDate" : "event.startDate",
expand: "user"
}) })
), ),
enabled: !!user enabled: !!user
@ -62,7 +61,7 @@ export default function UserEntries() {
<Breadcrumbs>{[ <Breadcrumbs>{[
{title: "Home", to: "/"}, {title: "Home", to: "/"},
{title: "Events", to: "/events"}, {title: "Events", to: "/events"},
{title: "Meine Einträge", to: `/events/entries`}, {title: "Anmeldungen", to: `/events/entries`},
].map(({title, to}) => ( ].map(({title, to}) => (
<Anchor component={Link} to={to} key={title}> <Anchor component={Link} to={to} key={title}>
{title} {title}
@ -70,11 +69,11 @@ export default function UserEntries() {
))}</Breadcrumbs> ))}</Breadcrumbs>
</div> </div>
<div className={"section"}> <div className={"section"}>
<Title order={1} c={"blue"}>Meine Einträge</Title> <Title order={1} c={"blue"}>Anmeldungen</Title>
</div> </div>
<div className={"section-transparent"}> <div className={"section-transparent"}>
<ShowHelp> <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> </ShowHelp>
</div> </div>
@ -101,11 +100,11 @@ export default function UserEntries() {
<Group justify={"space-between"}> <Group justify={"space-between"}>
<Text c={"dimmed"} size={"xs"}> <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>
<Text c={"dimmed"} size={"xs"}> <Text c={"dimmed"} size={"xs"}>
{entriesQuery.data?.totalItems ?? 0} {eventSearch ? "Ergebnisse" : "Einträge"} {entriesQuery.data?.totalItems ?? 0} {eventSearch ? "Ergebnisse" : "Anmeldungen"}
</Text> </Text>
</Group> </Group>
@ -116,7 +115,7 @@ export default function UserEntries() {
<IconDatabaseOff/> <IconDatabaseOff/>
</ThemeIcon> </ThemeIcon>
<Title order={4} c={"dimmed"}>Keine Einträge vorhanden</Title> <Title order={4} c={"dimmed"}>Keine Anmeldungen vorhanden</Title>
</Stack> </Stack>
} }

View File

@ -17,6 +17,7 @@ import {usePB} from "@/lib/pocketbase.tsx";
import { import {
UpdateEventListSlotEntryFormModal UpdateEventListSlotEntryFormModal
} from "@/pages/events/e/:eventId/EventLists/components/UpdateEventListSlotEntryFormModal.tsx"; } from "@/pages/events/e/:eventId/EventLists/components/UpdateEventListSlotEntryFormModal.tsx";
import {getUserName} from "@/components/auth/modals/util.tsx";
export default function UserEntryRow({entry, refetch}: { export default function UserEntryRow({entry, refetch}: {
entry: EventListSlotEntriesWithUserModel, entry: EventListSlotEntriesWithUserModel,
@ -42,7 +43,7 @@ export default function UserEntryRow({entry, refetch}: {
const {ConfirmModal, toggleConfirmModal} = useConfirmModal({ const {ConfirmModal, toggleConfirmModal} = useConfirmModal({
title: 'Eintrag löschen', 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() onConfirm: () => deleteEntryMutation.mutate()
}) })
@ -90,7 +91,7 @@ export default function UserEntryRow({entry, refetch}: {
<Group gap={"xs"}> <Group gap={"xs"}>
<Tooltip <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> withArrow>
<ActionIcon <ActionIcon
variant={"light"} variant={"light"}
@ -104,7 +105,7 @@ export default function UserEntryRow({entry, refetch}: {
</Tooltip> </Tooltip>
<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> withArrow>
<ActionIcon <ActionIcon
variant={"light"} variant={"light"}

View File

@ -1,4 +1,4 @@
import {EventListSlotsWithEntriesCountModel} from "@/models/EventTypes.ts"; import {EventListModel, EventListSlotsWithEntriesCountModel} from "@/models/EventTypes.ts";
import classes from "./EventListSlotView.module.css"; import classes from "./EventListSlotView.module.css";
import {RenderDateRange} from "@/pages/events/e/:eventId/EventLists/components/RenderDateRange.tsx"; import {RenderDateRange} from "@/pages/events/e/:eventId/EventLists/components/RenderDateRange.tsx";
import EventListSlotProgress from "@/pages/events/e/:eventId/EventLists/components/EventListSlotProgress.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 {showSuccessNotification} from "@/components/util.tsx";
import InnerHtml from "@/components/InnerHtml"; import InnerHtml from "@/components/InnerHtml";
import FormInput from "@/components/formUtil/FromInput"; 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, slot: EventListSlotsWithEntriesCountModel,
refetch: () => void refetch: () => void
}) { }) {
@ -31,17 +31,14 @@ export default function EventListSlotView({slot, refetch}: {
] ]
} }
const {pb} = usePB() const {pb, user} = usePB()
const user = useUser()
const createEntryMutation = useMutation({ const createEntryMutation = useMutation({
mutationFn: async (data: FieldEntries) => { mutationFn: async (data: FieldEntries) => {
await pb.collection("eventListSlotEntries").create({ await pb.collection("eventListSlotEntries").create({
entryQuestionData: data, entryQuestionData: data,
eventListsSlot: slot.id, eventListsSlot: slot.id,
ldapUser: user?.realm === "STUVE" ? user.id : null, user: user?.id
guestUser: user?.realm === "GUEST" ? user.id : null
}) })
}, },
onSuccess: () => { 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"}> <Alert color={"red"}>
Dieser Zeitslot ist bereits vorbei Dieser Zeitslot ist bereits vorbei
</Alert> </Alert>

View File

@ -48,7 +48,7 @@ export default function EventListView({event, listId}: { event: EventModel, list
{!list.open && ( {!list.open && (
<Alert icon={<IconLock/>}> <Alert icon={<IconLock/>}>
Diese Liste ist nicht für Einträge geöffnet Diese Liste ist nicht für Anmeldungen geöffnet
</Alert> </Alert>
)} )}
@ -59,10 +59,25 @@ export default function EventListView({event, listId}: { event: EventModel, list
</Alert> </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) && <> (list.open && slots.length !== 0) && <>
{slots.map(slot => ( {slots.map(slot => (
<EventListSlotView key={slot.id} slot={slot} refetch={() => { <EventListSlotView key={slot.id} list={list} slot={slot} refetch={() => {
listSlotsQuery.refetch() listSlotsQuery.refetch()
}}/> }}/>
))} ))}

View File

@ -9,14 +9,12 @@ import {IconExternalLink, IconLogin, IconPencil, IconSectionSign} from "@tabler/
import EventData from "@/pages/events/e/:eventId/EventComponents/EventData.tsx"; import EventData from "@/pages/events/e/:eventId/EventComponents/EventData.tsx";
import EventListView from "@/pages/events/s/EventListView.tsx"; import EventListView from "@/pages/events/s/EventListView.tsx";
import {useEventRights} from "@/pages/events/util.ts"; import {useEventRights} from "@/pages/events/util.ts";
import {useUser} from "@/lib/user.ts";
export default function SharedEvent() { export default function SharedEvent() {
const {pb} = usePB() const {pb,user} = usePB()
const user = useUser()
const {eventId} = useParams() as { eventId: string } const {eventId} = useParams() as { eventId: string }

View File

@ -1,20 +1,21 @@
import {EventModel} from "@/models/EventTypes.ts"; import {EventModel} from "@/models/EventTypes.ts";
import {usePB} from "@/lib/pocketbase.tsx"; import {usePB} from "@/lib/pocketbase.tsx";
import {useSettings} from "@/lib/settings.ts"; import {useSettings} from "@/lib/settings.ts";
import {LdapGroupModel} from "@/models/AuthTypes.ts";
export const useEventRights = (event?: EventModel) => { export const useEventRights = (event?: EventModel) => {
const {ldapUser} = usePB() const {user} = usePB()
const settings = useSettings() const settings = useSettings()
const isStex = !!( const isStex = !!(
ldapUser && user &&
settings?.stexGroupId?.value !== undefined && 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 isEventAdmin = !!(user && event && event.eventAdmins.includes(user.id))
const isEventListAdmin = !!(ldapUser && event && event.eventListAdmins.includes(ldapUser.id)) const isEventListAdmin = !!(user && event && event.eventListAdmins.includes(user.id))
return { return {
canEditEvent: isEventAdmin || isStex, canEditEvent: isEventAdmin || isStex,

View File

@ -3,14 +3,11 @@ import {Alert, Button, Group, ThemeIcon, Title} from "@mantine/core";
import {IconConfetti, IconHandLoveYou, IconQrcode} from "@tabler/icons-react"; import {IconConfetti, IconHandLoveYou, IconQrcode} from "@tabler/icons-react";
import {usePB} from "@/lib/pocketbase.tsx"; import {usePB} from "@/lib/pocketbase.tsx";
import {NavLink} from "react-router-dom"; import {NavLink} from "react-router-dom";
import {useUser} from "@/lib/user.ts";
export default function HomePage() { export default function HomePage() {
const {userRecord} = usePB() const {user} = usePB()
const user = useUser()
const nav = [ const nav = [
{ {
@ -34,10 +31,10 @@ export default function HomePage() {
</ThemeIcon> </ThemeIcon>
Hallo Hallo
{user && `, ${user.name}`} {user && `, ${user.username}`}
</Title> </Title>
{!userRecord && <> {!user && <>
<Alert color={"blue"} title={"Willkommen bei der StuVe!"}> <Alert color={"blue"} title={"Willkommen bei der StuVe!"}>
Um alle Funktionen nutzen zu können, logge dich bitte mit deinem StuVe IT Account ein. Um alle Funktionen nutzen zu können, logge dich bitte mit deinem StuVe IT Account ein.
</Alert> </Alert>