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 {useQuery} from "@tanstack/react-query";
const {pb} = usePB()
const user = useUser()
const {pb,user} = usePB()
const query = useQuery({
queryKey: ["collection", id],

View File

@ -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(".")
}%")`,

View File

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

View File

@ -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,
)
@ -115,7 +114,7 @@ const ConfirmEmailChangeModal = ({open, onClose, token}: {
<form className={"stack"} onSubmit={formValues.onSubmit(() => mutation.mutate())}>
<Center>
<EmailSVG height={"200px"} width={"200px"}/>
<EmailSVG height={"200px"} width={"200px"}/>
</Center>
<PocketBaseErrorAlert error={mutation.error}/>
@ -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()

View File

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

View File

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

View File

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

View File

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

View File

@ -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/>
REALM: <Code>{user?.REALM}</Code>
{user?.objectGUID && <>
<br/>
GUID: <Code>{userRecord?.objectGUID}</Code>
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>
<Text>
{userRecord?.email}
</Text>
<Tooltip label={`Dein Email ist ${user?.emailVisibility ? "sichtbar" : "nicht sichtbar"}`}>
<Text>
{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"}/>

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 {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: "/"
},
]
},
{
section: "Events",
items: [
{
title: "Events",
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,

View File

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

View File

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

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 = {
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;

View File

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

View File

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

View File

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

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 {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"}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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={

View File

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

View File

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

View File

@ -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) => {

View File

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

View File

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

View File

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

View File

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

View File

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

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 {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>

View File

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

View File

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

View File

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

View File

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