feat(entries filter): added filter for entries
Build and Push Docker image / build-and-push (push) Successful in 4m29s Details

This commit is contained in:
Valentin Kolb 2024-05-28 17:57:21 +02:00
parent 4f53566b61
commit d1569bce55
60 changed files with 1293 additions and 724 deletions

View File

@ -3,11 +3,11 @@
*/ */
// POCKETBASE // POCKETBASE
export const PB_USER_COLLECTION = "ldap_users" export const PB_USER_COLLECTION = "users"
export const PB_BASE_URL = "https://backend.stuve-it.de" export const PB_BASE_URL = "https://backend.stuve-it.de"
export const PB_STORAGE_KEY = "stuve-it-login-record" export const PB_STORAGE_KEY = "stuve-it-login-record"
// general // general
export const APP_NAME = "StuVe IT" export const APP_NAME = "StuVe IT"
export const APP_VERSION = "0.7.0 (beta)" export const APP_VERSION = "0.8.0 (beta)"
export const APP_URL = "https://it.stuve.uni-ulm.de" export const APP_URL = "https://it.stuve.uni-ulm.de"

View File

@ -13,14 +13,14 @@
"@fontsource/fira-code": "^5.0.15", "@fontsource/fira-code": "^5.0.15",
"@fontsource/overpass": "^5.0.15", "@fontsource/overpass": "^5.0.15",
"@hello-pangea/dnd": "^16.6.0", "@hello-pangea/dnd": "^16.6.0",
"@mantine/code-highlight": "^7.8.0", "@mantine/code-highlight": "^7.10.0",
"@mantine/core": "^7.8.0", "@mantine/core": "^7.10.0",
"@mantine/dates": "^7.8.0", "@mantine/dates": "^7.10.0",
"@mantine/form": "^7.8.0", "@mantine/form": "^7.10.0",
"@mantine/hooks": "^7.8.0", "@mantine/hooks": "^7.10.0",
"@mantine/modals": "^7.9.0", "@mantine/modals": "^7.10.0",
"@mantine/notifications": "^7.8.1", "@mantine/notifications": "^7.10.0",
"@mantine/tiptap": "^7.8.0", "@mantine/tiptap": "^7.10.0",
"@tabler/icons-react": "^3.2.0", "@tabler/icons-react": "^3.2.0",
"@tanstack/react-query": "^5.0.5", "@tanstack/react-query": "^5.0.5",
"@tanstack/react-query-devtools": "^5.31.0", "@tanstack/react-query-devtools": "^5.31.0",

View File

@ -0,0 +1,170 @@
import {
CheckboxFieldEntryFilter,
DateFieldEntryFilter,
DateRangeFieldEntryFilter,
EmailFieldEntryFilter,
FieldEntryFilter,
NumberFieldEntryFilter,
SelectFieldEntryFilter,
TextFieldEntryFilter
} from "@/components/formUtil/FromInput/types.ts";
import {FormSchemaField} from "@/components/formUtil/formBuilder/types.ts";
import {Autocomplete, Button, NumberInput, Popover, TextInput} from "@mantine/core";
import {IconFilterEdit} from "@tabler/icons-react";
import classes from "@/components/formUtil/FormFilter/index.module.css";
import {DateTimePicker} from "@mantine/dates";
import {CheckboxCard} from "@/components/input/CheckboxCard";
type FilterFieldProps<T extends FieldEntryFilter> = {
field: FormSchemaField
filter: T
setFilter: (value: T) => void,
}
export const FilterField = ({field, filter, setFilter}: FilterFieldProps<FieldEntryFilter>) => {
return <>
<Popover width={230} position="bottom" withArrow shadow="md">
<Popover.Target>
<Button
variant={"light"}
size={"xs"}
leftSection={<IconFilterEdit size={16}/>}
className={classes.filterButton}
>
<span className={classes.filterButtonLabel}>
{field.label}
</span>
</Button>
</Popover.Target>
<Popover.Dropdown>
{
(filter.dataType === "text" || filter.dataType === "email") &&
<FilterTextField
field={field}
filter={filter}
setFilter={setFilter}
/>
}
{
filter.dataType === "select" &&
<FilterSelectField
field={field}
filter={filter}
setFilter={setFilter}
/>
}
{
(filter.dataType === "date" || filter.dataType === "date-range") &&
<FilterDateField
field={field}
filter={filter}
setFilter={setFilter}
/>
}
{
filter.dataType === "number" &&
<FilterNumberField
field={field}
filter={filter}
setFilter={setFilter}
/>
}
{
filter.dataType === "checkbox" &&
<CheckboxFilterField
field={field}
filter={filter}
setFilter={setFilter}
/>
}
</Popover.Dropdown>
</Popover>
</>
}
export const FilterDateField = ({
filter,
setFilter
}: FilterFieldProps<DateFieldEntryFilter | DateRangeFieldEntryFilter>) => {
return <div className={"stack"}>
<DateTimePicker
clearable
placeholder={`Untergrenze`}
popoverProps={{withinPortal: false}}
value={filter.min ?? undefined}
onChange={(v) => setFilter({...filter, min: v})}
/>
<DateTimePicker
clearable
placeholder={`Obergrenze`}
popoverProps={{withinPortal: false}}
value={filter.max ?? undefined}
onChange={(v) => setFilter({...filter, max: v})}
/>
</div>
}
export const FilterNumberField = ({filter, setFilter}: FilterFieldProps<NumberFieldEntryFilter>) => {
return <div className={"stack"}>
<NumberInput
placeholder={`Untergrenze`}
value={filter.min ?? ""}
onChange={(v) => setFilter({...filter, min: v})}
/>
<NumberInput
placeholder={`Obergrenze`}
value={filter.max ?? ""}
onChange={(v) => setFilter({...filter, max: v})}
/>
</div>
}
export const FilterTextField = ({
filter,
setFilter
}: FilterFieldProps<TextFieldEntryFilter | EmailFieldEntryFilter>) => {
return <TextInput
placeholder={`Enthält`}
value={filter.value ?? ""}
onChange={(e) => setFilter({...filter, value: e.currentTarget.value})}
/>
}
export const FilterSelectField = ({
field,
filter,
setFilter
}: FilterFieldProps<SelectFieldEntryFilter>) => {
return <Autocomplete
placeholder={`Enthält`}
data={field.dataType === "select" ? field.options : []}
value={filter.value ?? ""}
onChange={(s) => setFilter({...filter, value: s})}
/>
}
export const CheckboxFilterField = ({filter, setFilter}: FilterFieldProps<CheckboxFieldEntryFilter>) => {
const checked = !!filter.value
const indeterminate = filter.value === null
return <CheckboxCard
label={`Wert ist ${indeterminate ? "beliebig" : checked ? "wahr" : "falsch"}`}
checked={checked}
indeterminate={indeterminate}
onChange={() => {
if (checked) {
setFilter({...filter, value: false})
} else if (filter.value === false) {
setFilter({...filter, value: null})
} else {
setFilter({...filter, value: true})
}
}}
/>
}

View File

@ -0,0 +1,11 @@
.filterButton {
max-width: 250px;
}
.filterButtonLabel {
display: inline-block;
max-width: calc(100%);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}

View File

@ -0,0 +1,134 @@
import {FormSchema, FormSchemaField} from "../formBuilder/types.ts";
import {useForm} from "@mantine/form";
import {FieldEntriesFilter, FieldEntryFilter} from "@/components/formUtil/FromInput/types.ts";
import {Button, Group, Popover, Switch, Text} from "@mantine/core";
import {IconFilter, IconFilterOff, IconFilterPlus} from "@tabler/icons-react";
import {FilterField} from "@/components/formUtil/FormFilter/FilterField.tsx";
/**
* todo
*
* @param field The field to create the default value for
*/
const createDefaultFilter = (field: FormSchemaField): FieldEntryFilter => {
switch (field.dataType) {
case "text":
return {
value: null,
dataType: field.dataType,
}
case "email":
return {
value: null,
dataType: field.dataType,
}
case "number":
return {
min: null,
max: null,
dataType: field.dataType,
}
case "checkbox":
return {
value: null,
dataType: field.dataType,
}
case "select":
return {
value: null,
dataType: field.dataType,
}
case "date":
return {
min: null,
max: null,
dataType: field.dataType,
}
case "date-range":
return {
min: null,
max: null,
dataType: field.dataType,
}
}
}
/**
* This renders a form based on a schema
*
* @see assembleFilter
* @see FormBuilder
*
* @param schema The schema to render
* @param label The label for the filter button
* @param defaultValue The default value for the filter
* @param onChange The function to call when the filter changes
*/
export default function FormFilter({schema, label, defaultValue, onChange}: {
schema: FormSchema,
label?: string,
defaultValue?: FieldEntriesFilter,
onChange?: (filter: FieldEntriesFilter) => void,
}) {
const formValues = useForm({
initialValues: defaultValue ?? {} as FieldEntriesFilter,
onValuesChange: (values) => {
onChange?.(values)
}
})
const toggleField = (field: FormSchemaField) => {
if (formValues.values[field.id] !== undefined) {
formValues.setValues({[field.id]: undefined})
} else {
formValues.setFieldValue(field.id, createDefaultFilter(field))
}
}
return <>
<Popover width={300} position="bottom" withArrow shadow="md">
<Popover.Target>
<Button size={"xs"} leftSection={<IconFilterPlus size={16}/>}>
{label || "Nach Werten filter"}
</Button>
</Popover.Target>
<Popover.Dropdown>
<div className={"stack"}>
{schema.fields.map(field => (
<div key={field.id}>
<Group align={"center"} wrap={"nowrap"}>
<Switch
checked={formValues.values[field.id] !== undefined}
onChange={() => toggleField(field)}
size={"sm"}
onLabel={<IconFilter size={10}/>}
offLabel={<IconFilterOff size={10}/>}
/>
<Text c={"dimmed"} size={"xs"} truncate="end">
{field.label}
</Text>
</Group>
</div>
))}
</div>
</Popover.Dropdown>
</Popover>
{schema
.fields
.filter(field => formValues.values[field.id] !== undefined)
.map(field => {
return (
<FilterField
key={field.id}
field={field}
filter={formValues.values[field.id]}
setFilter={(value) => formValues.setFieldValue(field.id, value)}
/>
)
})}
</>
}

View File

@ -0,0 +1,45 @@
import {FieldEntriesFilter} from "@/components/formUtil/FromInput/types.ts";
/**
* This function takes a filter object and assembles it into a filter string compatible with pocketbase
* @param values The filter object
* @param prefix The prefix to add to the field keys (e.g. questionData) this is useful for nested fields
*/
export const assembleFilter = (values: FieldEntriesFilter, prefix?: string) => {
const filter: string[] = []
Object.keys(values).forEach(key => {
const field = values[key]
if (field === undefined) return
// add prefix to key if it exists
if (prefix) key = `${prefix}.${key}`
switch (field.dataType) {
case "text":
case "email":
case "select":
if (field.value !== null && field.value !== '') filter.push(`${key}.value~'${field.value}'`)
break
case "number":
if (field.min !== null && field.min !== '') filter.push(`${key}.value>=${field.min}`)
if (field.max !== null && field.max !== '') filter.push(`${key}.value<=${field.max}`)
break
case "checkbox":
if (field.value) filter.push(`${key}.value=${field.value}`)
if (field.value === false) filter.push(`${key}.value!=true`)
break
case "date":
if (field.min !== null) filter.push(`${key}.value>='${field.min.toISOString()}'`)
if (field.max !== null) filter.push(`${key}.value<='${field.max.toISOString()}'`)
break
case "date-range":
if (field.min !== null) filter.push(`${key}.value.0>='${field.min.toISOString()}'`)
if (field.max !== null) filter.push(`${key}.value.1<='${field.max.toISOString()}'`)
break
}
})
return filter
}

View File

@ -135,10 +135,10 @@ const Wrapper = ({field, children}: {
children: ReactNode children: ReactNode
}) => { }) => {
return <Stack gap={5}> return <Stack gap={5}>
<Input.Label required>{field.label}</Input.Label> <Input.Label required component={"div"}>{field.label}</Input.Label>
{field.description && <> {field.description && <>
<Input.Description> <Input.Description component={"div"}>
<Spoiler <Spoiler
maxHeight={40} maxHeight={40}
showLabel={<Text span size={"xs"}>Mehr anzeigen</Text>} showLabel={<Text span size={"xs"}>Mehr anzeigen</Text>}

View File

@ -1,5 +1,5 @@
import {FormSchema} from "../formBuilder/types.ts"; import {FormSchema} from "../formBuilder/types.ts";
import {Button, Group,} from "@mantine/core"; import {ActionIcon, Button, Code, CopyButton, Group, Table, Text, Tooltip} from "@mantine/core";
import {useForm} from "@mantine/form"; import {useForm} from "@mantine/form";
import ShowDebug from "../../ShowDebug.tsx"; import ShowDebug from "../../ShowDebug.tsx";
@ -7,12 +7,16 @@ import {FieldEntries} from "@/components/formUtil/FromInput/types.ts";
import {createValidationFromSchema} from "@/components/formUtil/FromInput/validation.ts"; import {createValidationFromSchema} from "@/components/formUtil/FromInput/validation.ts";
import { import {
CheckboxField, CheckboxField,
DateField, DateRangeField, DateField,
DateRangeField,
EmailField, EmailField,
FormTextareaField, FormTextareaField,
FormTextField, FormTextField,
NumberField, SelectField NumberField,
SelectField
} from "@/components/formUtil/FromInput/formFieldComponents.tsx"; } from "@/components/formUtil/FromInput/formFieldComponents.tsx";
import {transformData} from "@/components/formUtil/formTable";
import {IconCheck, IconHash} from "@tabler/icons-react";
/** /**
* This function creates default values based on a data type (e.g. "" for text, false for checkbox) * This function creates default values based on a data type (e.g. "" for text, false for checkbox)
@ -24,7 +28,7 @@ import {
* @param initialEntries The already existing entries * @param initialEntries The already existing entries
*/ */
const createDefaultValuesFromSchema = (schema: FormSchema, initialEntries?: FieldEntries): FieldEntries => { const createDefaultValuesFromSchema = (schema: FormSchema, initialEntries?: FieldEntries): FieldEntries => {
const entries: FieldEntries = initialEntries ?? {} const entries: FieldEntries = transformData(initialEntries ?? {})
schema.fields.forEach(field => { schema.fields.forEach(field => {
// if the field already exists, skip it // if the field already exists, skip it
@ -166,9 +170,25 @@ export default function FormInput({schema, onAbort, onSubmit, disabled, initialD
</Group> </Group>
<ShowDebug> <ShowDebug>
<pre> <Table w={"500px"} data={{
{JSON.stringify(formValues, null, 2)} body: schema.fields.map(field => ([
</pre> <CopyButton value={field.id}>
{({copied, copy}) => (
<Tooltip label={`ID '${field.id}' kopieren`}>
<ActionIcon variant={"transparent"} onClick={copy}>
{copied ? <IconCheck/> : <IconHash/>}
</ActionIcon>
</Tooltip>
)}
</CopyButton>,
<Text c="dimmed" w={"100px"} truncate={"end"}>
{field.label}
</Text>,
<Code display={"block"} className={"wrapWords"}
maw={"300px"}>{JSON.stringify(formValues.values[field.id]?.value)}</Code>
])),
head: ["Field", "Value"]
}}/>
</ShowDebug> </ShowDebug>
</form> </form>
} }

View File

@ -61,8 +61,8 @@ export type EmailFieldEntry = {
} }
export type NumberFieldEntryFilter = { export type NumberFieldEntryFilter = {
min: number | null min: number | string | null
max: number | null max: number | string | null
dataType: "number" dataType: "number"
} }

View File

@ -35,7 +35,7 @@ const DateTimeCell = ({date}: { date: Date | string }) => {
const DateRangeCell = ({dateRange}: { dateRange: [Date | null, Date | null] }) => { const DateRangeCell = ({dateRange}: { dateRange: [Date | null, Date | null] }) => {
const [start, end] = dateRange const [start, end] = dateRange.map(date => date === null ? null : new Date(date))
if (start === null || end === null) { if (start === null || end === null) {
return <EmptyCell/> return <EmptyCell/>

View File

@ -1,5 +1,7 @@
import {FieldDataType, FormSchemaField} from "./formBuilder/types.ts"; import {FieldDataType, FormSchemaField} from "./formBuilder/types.ts";
import {nanoid} from "nanoid"; import {customAlphabet} from "nanoid";
const nanoid = customAlphabet('1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRTSUVWXYZ', 6)
export const humanReadableField = (fieldType: FieldDataType): string => { export const humanReadableField = (fieldType: FieldDataType): string => {
switch (fieldType) { switch (fieldType) {

View File

@ -11,7 +11,7 @@
background-color: light-dark(var(--mantine-color-white), var(--mantine-color-dark-8)); background-color: light-dark(var(--mantine-color-white), var(--mantine-color-dark-8));
&[aria-error="true"] { &[data-error="true"] {
border-color: var(--mantine-color-error); border-color: var(--mantine-color-error);
} }
} }

View File

@ -7,11 +7,12 @@ export type CheckboxCardProps = InputWrapperProps & CheckboxProps
export function CheckboxCard(props: CheckboxCardProps) { export function CheckboxCard(props: CheckboxCardProps) {
return ( return (
<div className={classes.container} aria-error={!!props.error}> <div className={classes.container} data-error={!!props.error}>
<Checkbox <Checkbox
checked={props.checked} checked={props.checked}
onChange={props.onChange} onChange={props.onChange}
required={props.required} required={props.required}
indeterminate={props.indeterminate}
size="md" size="md"
mr="xl" mr="xl"
styles={{input: {cursor: 'pointer'}}} styles={{input: {cursor: 'pointer'}}}
@ -19,9 +20,11 @@ export function CheckboxCard(props: CheckboxCardProps) {
/> />
<div className={classes.textContainer}> <div className={classes.textContainer}>
<Input.Label required={props.required}>{props.label}</Input.Label> <Input.Label required={props.required} component={"div"}>{props.label}</Input.Label>
{props.description && {props.description &&
<Input.Description><InnerHtml html={props.description.toString() ?? ""}/></Input.Description>} <Input.Description component={"div"}>
<InnerHtml html={props.description.toString() ?? ""}/>
</Input.Description>}
{props.error && <Input.Error>{props.error}</Input.Error>} {props.error && <Input.Error>{props.error}</Input.Error>}
</div> </div>
</div> </div>

View File

@ -1,7 +1,7 @@
import {RecordModel} from "pocketbase"; import {RecordModel} from "pocketbase";
import {UseMutationResult} from "@tanstack/react-query"; import {UseMutationResult} from "@tanstack/react-query";
import {CheckIcon, Combobox, Group, Pill, PillsInput, PillsInputProps, Stack, Text, useCombobox} from "@mantine/core"; import {CheckIcon, Combobox, Group, Pill, PillsInput, PillsInputProps, Stack, Text, useCombobox} from "@mantine/core";
import {useState} from "react"; import {useEffect, useState} from "react";
/* /*
* GenericRecordInputProps is a generic type that describes the props that are common to all Record Input Wrappers * GenericRecordInputProps is a generic type that describes the props that are common to all Record Input Wrappers
@ -57,6 +57,11 @@ export default function RecordSearchInput<T extends RecordModel>(props: {
props.setSelectedRecords(props.selectedRecords.filter((record) => record.id !== val)) props.setSelectedRecords(props.selectedRecords.filter((record) => record.id !== val))
} }
useEffect(() => {
props.recordSearchMutation.mutate('')
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const searchResults = (props.recordSearchMutation.data || []) const searchResults = (props.recordSearchMutation.data || [])
.map((recordView) => ( .map((recordView) => (
<Combobox.Option value={recordView.id} key={recordView.id} <Combobox.Option value={recordView.id} key={recordView.id}

View File

@ -10,6 +10,7 @@ import {
IconLogout, IconLogout,
IconMailCog, IconMailCog,
IconPassword, IconPassword,
IconRefresh,
IconServer, IconServer,
IconServerOff, IconServerOff,
IconUser IconUser
@ -24,7 +25,7 @@ export default function UserMenuModal() {
const {handler: changeEmailHandler} = useChangeEmail() const {handler: changeEmailHandler} = useChangeEmail()
const {logout, apiIsHealthy, user} = usePB() const {logout, apiIsHealthy, user, refreshUser} = usePB()
const {showHelp, toggleShowHelp} = useShowHelp() const {showHelp, toggleShowHelp} = useShowHelp()
@ -188,6 +189,19 @@ export default function UserMenuModal() {
</Tooltip> </Tooltip>
</>} </>}
<Tooltip label={"Anmeldedaten neu laden"}>
<ActionIcon
variant={"transparent"}
color={"orange"}
aria-label={"logout"}
onClick={() => {
refreshUser()
}}
>
<IconRefresh/>
</ActionIcon>
</Tooltip>
<Tooltip label={"Ausloggen"}> <Tooltip label={"Ausloggen"}>
<ActionIcon <ActionIcon
variant={"transparent"} variant={"transparent"}

View File

@ -1,7 +1,6 @@
import {createContext, DependencyList, ReactNode, useCallback, useContext, useEffect, useMemo, useState} from "react" import {createContext, DependencyList, ReactNode, useCallback, useContext, useEffect, useMemo, useState} from "react"
import PocketBase, {ClientResponseError, LocalAuthStore, RecordAuthResponse, RecordSubscription} from 'pocketbase' import PocketBase, {ClientResponseError, LocalAuthStore, RecordAuthResponse, RecordSubscription} from 'pocketbase'
import ms from "ms"; import ms from "ms";
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";
@ -73,20 +72,29 @@ const PocketData = () => {
refetchInterval: oneMinuteInMs refetchInterval: oneMinuteInMs
}) })
const refreshUserQuery = useQuery({
queryKey: ["refreshUser"],
queryFn: async () => {
try {
const {record, token} = await pb.collection(PB_USER_COLLECTION).authRefresh({
expand: "memberOf"
})
pb.authStore.save(token, record)
return record
} catch (e) {
pb.authStore.clear()
return null
}
},
refetchInterval: oneMinuteInMs
})
const [user, setUser] = useState(pb.authStore.model) const [user, setUser] = useState(pb.authStore.model)
pb.authStore.onChange((_, userRecord) => { pb.authStore.onChange((_, userRecord) => {
setUser(userRecord) setUser(userRecord)
}) })
const refreshUser = useCallback(async () => {
await pb.collection(PB_USER_COLLECTION).authRefresh({
expand: "memberOf"
}).catch(() => {
pb.authStore.clear()
})
}, [pb])
const ldapLogin = useCallback(async (usernameOrCN: string, password: string) => { const ldapLogin = useCallback(async (usernameOrCN: string, password: string) => {
await pb.send<RecordAuthResponse>("/api/ldap/login", { await pb.send<RecordAuthResponse>("/api/ldap/login", {
method: "POST", method: "POST",
@ -126,9 +134,7 @@ const PocketData = () => {
pb.collection(idOrName).unsubscribe(topic) pb.collection(idOrName).unsubscribe(topic)
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, deps ? deps : []); }, deps ? deps : [])
useInterval(refreshUser, oneMinuteInMs)
return { return {
ldapLogin, ldapLogin,
@ -136,7 +142,7 @@ const PocketData = () => {
logout, logout,
user: user as UserModal | null, user: user as UserModal | null,
pb, pb,
refreshUser, refreshUser: refreshUserQuery.refetch,
useSubscription, useSubscription,
apiIsHealthy: apiIsHealthyQuery.data ?? false apiIsHealthy: apiIsHealthyQuery.data ?? false
} }

View File

@ -39,6 +39,8 @@ export type EventListModel = {
event: string event: string
entryQuestionSchema: FormSchema | null entryQuestionSchema: FormSchema | null
entryStatusSchema: FormSchema | null; entryStatusSchema: FormSchema | null;
ignoreDefaultEntryQuestionScheme: boolean | null;
ignoreDefaultEntryStatusSchema: boolean | null;
expand?: { expand?: {
event: EventModel; event: EventModel;
} }
@ -49,7 +51,7 @@ export type EventListSlotModel = {
eventList: string; eventList: string;
startDate: string; startDate: string;
endDate: string; endDate: string;
maxEntries: number | null; maxEntries: number;
description: string | null; description: string | null;
expand?: { expand?: {
eventList: EventListModel; eventList: EventListModel;
@ -59,7 +61,7 @@ export type EventListSlotModel = {
export type EventListSlotsWithEntriesCountModel = EventListSlotModel export type EventListSlotsWithEntriesCountModel = EventListSlotModel
& { entriesCount: number } & { entriesCount: number }
& Pick<EventModel, "defaultEntryQuestionSchema" | "defaultEntryStatusSchema"> & Pick<EventModel, "defaultEntryQuestionSchema" | "defaultEntryStatusSchema">
& Pick<EventListModel, "entryQuestionSchema" | "entryStatusSchema"> & Pick<EventListModel, "entryQuestionSchema" | "entryStatusSchema" | "ignoreDefaultEntryStatusSchema" | "ignoreDefaultEntryQuestionScheme">
export type EventListSlotEntryModel = { export type EventListSlotEntryModel = {
entryQuestionData: FieldEntries; entryQuestionData: FieldEntries;
@ -91,4 +93,4 @@ export type EventListSlotEntriesWithUserModel =
} }
} }
& Pick<EventModel, "defaultEntryQuestionSchema" | "defaultEntryStatusSchema"> & Pick<EventModel, "defaultEntryQuestionSchema" | "defaultEntryStatusSchema">
& Pick<EventListModel, "entryQuestionSchema" | "entryStatusSchema"> & Pick<EventListModel, "entryQuestionSchema" | "entryStatusSchema" | "ignoreDefaultEntryQuestionScheme" | "ignoreDefaultEntryStatusSchema">

View File

@ -7,6 +7,7 @@ import {
Alert, Alert,
Anchor, Anchor,
Breadcrumbs, Breadcrumbs,
Code,
createPolymorphicComponent, createPolymorphicComponent,
Grid, Grid,
Group, Group,
@ -41,6 +42,8 @@ import {useEventRights} from "@/pages/events/util.ts";
import EventFavourites from "./EventComponents/EventFavourites.tsx"; import EventFavourites from "./EventComponents/EventFavourites.tsx";
import {forwardRef} from "react"; import {forwardRef} from "react";
import {APP_URL} from "../../../../../config.ts"; import {APP_URL} from "../../../../../config.ts";
import ShowDebug from "@/components/ShowDebug.tsx";
import {pprintDateTime} from "@/lib/datetime.ts";
type NavIconProps = { type NavIconProps = {
@ -148,6 +151,13 @@ export default function EditEventRouter() {
<Grid className={classes.grid} gutter={"sm"}> <Grid className={classes.grid} gutter={"sm"}>
<Grid.Col span={{base: 12, sm: 3}}> <Grid.Col span={{base: 12, sm: 3}}>
<div className={"stack"}> <div className={"stack"}>
<ShowDebug>
Event Id - <Code>{event.id}</Code>
<br/>
created - <Code>{pprintDateTime(event.created)}</Code>
<br/>
updated - <Code>{pprintDateTime(event.updated)}</Code>
</ShowDebug>
<EventData event={event}/> <EventData event={event}/>
<EventLinks event={event}/> <EventLinks event={event}/>
<EventFavourites event={event}/> <EventFavourites event={event}/>

View File

@ -90,7 +90,7 @@ export const EventSettingsMenu = ({event, target}: { event: EventModel, target:
<Menu.Dropdown> <Menu.Dropdown>
{ {
nav.map(({label, children,}, index, array) => <div key={index}> nav.map(({label, children,}, index) => <div key={index}>
<Menu.Label>{label}</Menu.Label> <Menu.Label>{label}</Menu.Label>
{children.map(({title, icon, to}) => ( {children.map(({title, icon, to}) => (
<Anchor component={NavLink} to={`/events/e/${event.id}/settings/${to}`} key={to}> <Anchor component={NavLink} to={`/events/e/${event.id}/settings/${to}`} key={to}>
@ -101,7 +101,6 @@ export const EventSettingsMenu = ({event, target}: { event: EventModel, target:
)} )}
</Anchor> </Anchor>
))} ))}
{index !== array.length - 1 && <Menu.Divider/>}
</div>) </div>)
} }
</Menu.Dropdown> </Menu.Dropdown>

View File

@ -1,5 +1,5 @@
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, Code, Group, LoadingOverlay, Title} from "@mantine/core";
import { import {
IconCheckupList, IconCheckupList,
IconClockCog, IconClockCog,
@ -13,13 +13,15 @@ import {Link, Navigate, NavLink, Route, Routes, useParams} from "react-router-do
import InnerHtml from "@/components/InnerHtml"; import InnerHtml from "@/components/InnerHtml";
import {EventModel} from "@/models/EventTypes.ts"; import {EventModel} from "@/models/EventTypes.ts";
import {PocketBaseErrorAlert, usePB} from "@/lib/pocketbase.tsx"; import {PocketBaseErrorAlert, usePB} from "@/lib/pocketbase.tsx";
import SlotsTable from "./EventListSlotsTable"; import ListSlots from "./ListSlots";
import EventListSettings from "@/pages/events/e/:eventId/EventLists/:listId/EventListSettings/EventListSettings.tsx";
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"; import TextWithIcon from "@/components/layout/TextWithIcon";
import ListSettings from "@/pages/events/e/:eventId/EventLists/:listId/ListSettings/ListSettings.tsx";
import ListEntryQuestionSettings
from "@/pages/events/e/:eventId/EventLists/:listId/ListSettings/ListEntryQuestionSettings.tsx";
import ListEntryStatusSettings
from "@/pages/events/e/:eventId/EventLists/:listId/ListSettings/ListEntryStatusSettings.tsx";
import ShowDebug from "@/components/ShowDebug.tsx";
import {pprintDateTime} from "@/lib/datetime.ts";
export default function EventListRouter({event}: { event: EventModel }) { export default function EventListRouter({event}: { event: EventModel }) {
@ -57,6 +59,14 @@ export default function EventListRouter({event}: { event: EventModel }) {
</Link> </Link>
</Breadcrumbs> </Breadcrumbs>
<ShowDebug>
List Id - <Code>{list.id}</Code>
<br/>
created - <Code>{pprintDateTime(list.created)}</Code>
<br/>
updated - <Code>{pprintDateTime(list.updated)}</Code>
</ShowDebug>
<Alert color={list.open ? "green" : "red"}> <Alert color={list.open ? "green" : "red"}>
<TextWithIcon icon={list.open ? <IconLockOpen size={16}/> : <IconLock size={16}/>}> <TextWithIcon icon={list.open ? <IconLockOpen size={16}/> : <IconLock size={16}/>}>
Liste ist für Anmeldungen <b>{list.open ? "geöffnet" : "geschlossen"}</b> Liste ist für Anmeldungen <b>{list.open ? "geöffnet" : "geschlossen"}</b>
@ -90,7 +100,7 @@ export default function EventListRouter({event}: { event: EventModel }) {
{[ {[
{ {
icon: <IconClockCog/>, icon: <IconClockCog/>,
to: `/events/e/${event.id}/lists/overview/${list.id}/entries`, to: `/events/e/${event.id}/lists/overview/${list.id}/slots`,
title: "Zeitslots" title: "Zeitslots"
}, },
{ {
@ -123,11 +133,11 @@ export default function EventListRouter({event}: { event: EventModel }) {
</Group> </Group>
<Routes> <Routes>
<Route index element={<Navigate to={"entries"} replace/>}/> <Route index element={<Navigate to={"slots"} replace/>}/>
<Route path={"entries"} element={<SlotsTable event={event} list={list}/>}/> <Route path={"slots"} element={<ListSlots list={list}/>}/>
<Route path={"settings"} element={<EventListSettings list={list} event={event}/>}/> <Route path={"settings"} element={<ListSettings list={list} event={event}/>}/>
<Route path={"questions"} element={<EventListEntryQuestionSettings list={list} event={event}/>}/> <Route path={"questions"} element={<ListEntryQuestionSettings list={list} event={event}/>}/>
<Route path={"status"} element={<EventListEntryStatusSettings list={list} event={event}/>}/> <Route path={"status"} element={<ListEntryStatusSettings list={list} event={event}/>}/>
</Routes> </Routes>
</div> </div>
} }

View File

@ -1,13 +0,0 @@
.bottomRow {
margin-left: var(--mantine-spacing-lg);
display: flex;
justify-content: space-between;
align-items: center;
gap: var(--gap);
}
.table {
display: flex;
flex-direction: column;
gap: calc(var(--gap) / 2);
}

View File

@ -1,86 +0,0 @@
import {EventListModel, EventListSlotsWithEntriesCountModel, EventModel} from "@/models/EventTypes.ts";
import {useQuery} from "@tanstack/react-query";
import {useState} from "react";
import {PocketBaseErrorAlert, usePB} from "@/lib/pocketbase.tsx";
import classes from "./EventListSlotEntriesTable.module.css";
import {Loader, Pagination, Stack, Text, ThemeIcon} from "@mantine/core";
import {transformData} from "@/components/formUtil/formTable";
import {EventListSlotEntryRow} from "./EventListSlotEntryRow.tsx";
import {IconDatabaseOff} from "@tabler/icons-react";
export const EventListSlotEntriesTable = ({slot, list, event, refetch, visible}:
{
visible: boolean,
refetch: () => void,
slot: EventListSlotsWithEntriesCountModel,
list: EventListModel,
event: EventModel
}) => {
const {pb} = usePB()
const [page, setPage] = useState(1)
const query = useQuery({
queryKey: ["event", event.id, "list", list.id, "slot", slot.id, "entriesWithUser", page],
queryFn: async () => {
const res = await pb.collection("eventListSlotEntriesWithUser").getList(page, 50, {
filter: `eventListsSlot='${slot.id}'`,
expand: "user"
})
return {
...res,
items: res.items.map(item => ({
...item,
questionData: transformData(item.questionData ?? {}),
statusData: transformData(item.statusData ?? {}),
}))
}
},
enabled: visible
})
if (query.isLoading) {
return <Stack align={"center"} gap={"xs"}>
<Loader size={"sm"}/>
<Text size={"xs"} c={"dimmed"}>Anmeldungen werden geladen</Text>
</Stack>
}
if (query.data?.totalItems === 0) {
return <Stack align={"center"} gap={"xs"}>
<ThemeIcon variant={"transparent"} color={"gray"} size={"md"}>
<IconDatabaseOff/>
</ThemeIcon>
<Text size={"xs"} c={"dimmed"}>Keine Anmeldungen vorhanden</Text>
</Stack>
}
if (query.isError) return <PocketBaseErrorAlert error={query.error}/>
return <div className={classes.table}>
{
query.data?.items.map(entry => (
<EventListSlotEntryRow
key={entry.id}
entry={entry}
slot={slot}
list={list}
event={event}
refetch={() => {
query.refetch()
refetch()
}}
/>
))
}
<div className={classes.bottomRow}>
<Text size={"xs"} c={"dimmed"}>
{query.data?.totalItems} Anmeldungen
</Text>
<Pagination size={"xs"} value={page} onChange={setPage} total={query.data?.totalPages ?? 1}/>
</div>
</div>
}

View File

@ -1,101 +0,0 @@
import {
EventListModel,
EventListSlotEntriesWithUserModel,
EventListSlotModel,
EventModel
} from "@/models/EventTypes.ts";
import {useDisclosure} from "@mantine/hooks";
import classes from "./EventListSlotEntryRow.module.css";
import {ActionIcon, Box, Collapse, Group, SimpleGrid, Tooltip} from "@mantine/core";
import {IconCheckupList, IconForms, IconUserMinus, IconUserPlus} from "@tabler/icons-react";
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/users/modals/util.tsx";
export const EventListSlotEntryDetails = ({entry}: {
entry: EventListSlotEntriesWithUserModel
}) => {
const questionSchemaFields = [
...entry.entryQuestionSchema?.fields ?? [],
...entry.defaultEntryQuestionSchema?.fields ?? [],
]
const statusSchemaFields = [
...entry.entryStatusSchema?.fields ?? [],
...entry.defaultEntryStatusSchema?.fields ?? [],
]
return <>
<SimpleGrid className={classes.dataGrid} cols={{base: 2, sm: 3, lg: 4}}>
{renderEntries(entry.entryQuestionData ?? {}, questionSchemaFields).map(({label, value}) => {
return <div className={classes.dataCell} key={label}>
<div className={classes.dataIcon}>
<IconForms size={16}/>
</div>
<div className={classes.dataTitle}>
{label}
</div>
<Box>
<RenderCell entry={value}/>
</Box>
</div>
})}
{renderEntries(entry.entryStatusData ?? {}, statusSchemaFields).map(({label, value}) => {
return <div className={classes.dataCell} key={label}>
<div className={classes.dataIcon}>
<IconCheckupList size={16}/>
</div>
<div className={classes.dataTitle}>
{label}
</div>
<Box>
<RenderCell entry={value}/>
</Box>
</div>
})}
</SimpleGrid>
</>
}
/**
* Renders a row with all entries for a slot
* @param entry - entry to render
* @param slot - slot the entry belongs to
* @param list - list the entry belongs to
* @param event - event the entry belongs to
* @param refetch - refetch function for the entries
* @constructor
*/
export const EventListSlotEntryRow = ({entry, refetch}: {
entry: EventListSlotEntriesWithUserModel,
slot: EventListSlotModel,
list: EventListModel,
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>
<ActionIcon variant={"transparent"} onClick={expandedHandler.toggle}>
{expanded ? <IconUserMinus size={16}/> : <IconUserPlus size={16}/>}
</ActionIcon>
</Tooltip>
<div><RenderUserName user={entry.expand?.user}/></div>
<Group gap={4} justify="right" wrap="nowrap">
<EditSlotEntryMenu entry={entry} refetch={refetch}/>
</Group>
</div>
<Collapse in={expanded}>
<EventListSlotEntryDetails entry={entry}/>
</Collapse>
</div>
}

View File

@ -8,18 +8,27 @@ import {FormSchema} from "@/components/formUtil/formBuilder/types.ts";
import FormBuilder from "@/components/formUtil/formBuilder"; import FormBuilder from "@/components/formUtil/formBuilder";
import FormFieldsPreview from "@/components/formUtil/formBuilder/FormFieldsPreview.tsx"; import FormFieldsPreview from "@/components/formUtil/formBuilder/FormFieldsPreview.tsx";
import {IconAlertTriangle} from "@tabler/icons-react"; import {IconAlertTriangle} from "@tabler/icons-react";
import {useForm} from "@mantine/form";
import {CheckboxCard} from "@/components/input/CheckboxCard";
export default function EventListEntryQuestionSettings({list, event}: { list: EventListModel, event: EventModel }) { export default function ListEntryQuestionSettings({list, event}: { list: EventListModel, event: EventModel }) {
const {pb} = usePB() const {pb} = usePB()
const formValues = useForm({
initialValues: {
ignoreDefaultEntryQuestionScheme: list.ignoreDefaultEntryQuestionScheme || false,
}
})
const editMutation = useMutation({ const editMutation = useMutation({
mutationFn: async (schema: FormSchema) => { mutationFn: async (schema: FormSchema) => {
return await pb.collection("eventLists").update(list.id, { return await pb.collection("eventLists").update(list.id, {
entryQuestionSchema: schema entryQuestionSchema: schema,
...formValues.values
}) })
}, },
onSuccess: () => { onSuccess: () => {
showSuccessNotification("Fragen gespeichert") showSuccessNotification("Fragen und Liste gespeichert")
return queryClient.invalidateQueries({queryKey: ["event", event.id, "list", list.id]}) return queryClient.invalidateQueries({queryKey: ["event", event.id, "list", list.id]})
} }
}) })
@ -29,7 +38,7 @@ export default function EventListEntryQuestionSettings({list, event}: { list: Ev
<PocketBaseErrorAlert error={editMutation.error}/> <PocketBaseErrorAlert error={editMutation.error}/>
{event.defaultEntryQuestionSchema && ( {(!list.ignoreDefaultEntryQuestionScheme && event.defaultEntryQuestionSchema) && (
<Alert color={"blue"} title={"Standard Fragen"} className={"stack"}> <Alert color={"blue"} title={"Standard Fragen"} className={"stack"}>
<Text c={"dimmed"} mb={"sm"} size={"xs"}> <Text c={"dimmed"} mb={"sm"} size={"xs"}>
Folgende Fragen sind standardmäßig für dieses Event vorgesehen Folgende Fragen sind standardmäßig für dieses Event vorgesehen
@ -39,6 +48,13 @@ export default function EventListEntryQuestionSettings({list, event}: { list: Ev
</Alert> </Alert>
)} )}
<CheckboxCard
label={"Event-Standardfragen ignorieren"}
description={"Wenn du diese Option aktivierst, werden die Standardfragen des Events nicht für diese Liste verwendet. " +
"Stattdessen werden nur die Fragen spezifisch für diese Liste verwendet."}
{...formValues.getInputProps("ignoreDefaultEntryQuestionScheme", {type: "checkbox"})}
/>
<Alert color={"orange"} icon={<IconAlertTriangle/>}> <Alert color={"orange"} icon={<IconAlertTriangle/>}>
Wenn du Felder entfernst, werden alle Daten, Wenn du Felder entfernst, werden alle Daten,
die für diese Felder gespeichert wurden nicht mehr angezeigt. die für diese Felder gespeichert wurden nicht mehr angezeigt.
@ -48,7 +64,7 @@ export default function EventListEntryQuestionSettings({list, event}: { list: Ev
defaultValue={list.entryQuestionSchema || {fields: []}} defaultValue={list.entryQuestionSchema || {fields: []}}
onSubmit={(schema) => editMutation.mutate(schema)} onSubmit={(schema) => editMutation.mutate(schema)}
withPreview withPreview
additionalSchemaToPreview={event.defaultEntryQuestionSchema || {fields: []}} additionalSchemaToPreview={!list.ignoreDefaultEntryQuestionScheme && event.defaultEntryQuestionSchema || {fields: []}}
/> />
</div> </div>
} }

View File

@ -8,18 +8,27 @@ import {FormSchema} from "@/components/formUtil/formBuilder/types.ts";
import FormBuilder from "@/components/formUtil/formBuilder"; import FormBuilder from "@/components/formUtil/formBuilder";
import FormFieldsPreview from "@/components/formUtil/formBuilder/FormFieldsPreview.tsx"; import FormFieldsPreview from "@/components/formUtil/formBuilder/FormFieldsPreview.tsx";
import {IconAlertTriangle} from "@tabler/icons-react"; import {IconAlertTriangle} from "@tabler/icons-react";
import {useForm} from "@mantine/form";
import {CheckboxCard} from "@/components/input/CheckboxCard";
export default function EventListEntryStatusSettings({list, event}: { list: EventListModel, event: EventModel }) { export default function ListEntryStatusSettings({list, event}: { list: EventListModel, event: EventModel }) {
const {pb} = usePB() const {pb} = usePB()
const editMutation = useMutation({ const formValues = useForm({
initialValues: {
ignoreDefaultEntryStatusSchema: list.ignoreDefaultEntryStatusSchema || false,
}
})
const editStatusMutation = useMutation({
mutationFn: async (schema: FormSchema) => { mutationFn: async (schema: FormSchema) => {
return await pb.collection("eventLists").update(list.id, { await pb.collection("eventLists").update(list.id, {
entryStatusSchema: schema entryStatusSchema: schema,
...formValues.values
}) })
}, },
onSuccess: () => { onSuccess: () => {
showSuccessNotification("Status Felder gespeichert") showSuccessNotification("Status Felder und Liste gespeichert")
return queryClient.invalidateQueries({queryKey: ["event", event.id, "list", list.id]}) return queryClient.invalidateQueries({queryKey: ["event", event.id, "list", list.id]})
} }
}) })
@ -27,9 +36,9 @@ export default function EventListEntryStatusSettings({list, event}: { list: Even
return <div className={"section stack"}> return <div className={"section stack"}>
<Title order={4} c={"blue"}>Eintrags-Status der Liste</Title> <Title order={4} c={"blue"}>Eintrags-Status der Liste</Title>
<PocketBaseErrorAlert error={editMutation.error}/> <PocketBaseErrorAlert error={editStatusMutation.error}/>
{event.defaultEntryStatusSchema && ( {(!list.ignoreDefaultEntryStatusSchema && event.defaultEntryStatusSchema) && (
<Alert color={"blue"} title={"Standard Status Felder"} className={"stack"}> <Alert color={"blue"} title={"Standard Status Felder"} className={"stack"}>
<Text c={"dimmed"} mb={"sm"} size={"xs"}> <Text c={"dimmed"} mb={"sm"} size={"xs"}>
Folgende Status-Felder sind standardmäßig für dieses Event vorgesehen Folgende Status-Felder sind standardmäßig für dieses Event vorgesehen
@ -39,6 +48,13 @@ export default function EventListEntryStatusSettings({list, event}: { list: Even
</Alert> </Alert>
)} )}
<CheckboxCard
label={"Event-Standardstatus ignorieren"}
description={"Wenn du diese Option aktivierst, werden die Standardstatus des Events nicht für diese Liste verwendet. " +
"Stattdessen wird nur der Status spezifisch für diese Liste verwendet."}
{...formValues.getInputProps("ignoreDefaultEntryStatusSchema", {type: "checkbox"})}
/>
<Alert color={"orange"} icon={<IconAlertTriangle/>}> <Alert color={"orange"} icon={<IconAlertTriangle/>}>
Wenn du Felder entfernst, werden alle Daten, Wenn du Felder entfernst, werden alle Daten,
die für diese Felder gespeichert wurden nicht mehr angezeigt. die für diese Felder gespeichert wurden nicht mehr angezeigt.
@ -46,9 +62,9 @@ export default function EventListEntryStatusSettings({list, event}: { list: Even
<FormBuilder <FormBuilder
defaultValue={list.entryStatusSchema || {fields: []}} defaultValue={list.entryStatusSchema || {fields: []}}
onSubmit={(schema) => editMutation.mutate(schema)} onSubmit={(schema) => editStatusMutation.mutate(schema)}
withPreview withPreview
additionalSchemaToPreview={event.defaultEntryStatusSchema || {fields: []}} additionalSchemaToPreview={!list.ignoreDefaultEntryStatusSchema && event.defaultEntryStatusSchema || {fields: []}}
/> />
</div> </div>
} }

View File

@ -11,7 +11,7 @@ import {useNavigate} from "react-router-dom";
import {useConfirmModal} from "@/components/ConfirmModal.tsx"; import {useConfirmModal} from "@/components/ConfirmModal.tsx";
import {CheckboxCard} from "@/components/input/CheckboxCard"; import {CheckboxCard} from "@/components/input/CheckboxCard";
export default function EventListSettings({list, event}: { list: EventListModel, event: EventModel }) { export default function ListSettings({list, event}: { list: EventListModel, event: EventModel }) {
const {pb} = usePB() const {pb} = usePB()
const navigate = useNavigate() const navigate = useNavigate()
@ -22,7 +22,7 @@ export default function EventListSettings({list, event}: { list: EventListModel,
allowOverlappingEntries: list.allowOverlappingEntries, allowOverlappingEntries: list.allowOverlappingEntries,
onlyStuVeAccounts: list.onlyStuVeAccounts, onlyStuVeAccounts: list.onlyStuVeAccounts,
favorite: list.favorite, favorite: list.favorite,
description: list.description || "" description: list.description || "",
} }
}) })
@ -99,7 +99,7 @@ export default function EventListSettings({list, event}: { list: EventListModel,
<CheckboxCard <CheckboxCard
label={"Überlappende Anmeldungen erlauben"} label={"Überlappende Anmeldungen erlauben"}
description={" Wenn diese Option aktiviert ist, kann sich eine Person in überlappende Zeitslots anmelden. " + description={"Wenn diese Option aktiviert ist, kann sich eine Person in überlappende Zeitslots anmelden. " +
"Dabei werden alle Zeitslots von diesem Event beachtet. " + "Dabei werden alle Zeitslots von diesem Event beachtet. " +
"Bei dem Verschieden von Anmeldungen wirds diese Regel nicht beachtet!"} "Bei dem Verschieden von Anmeldungen wirds diese Regel nicht beachtet!"}
{...formValues.getInputProps("allowOverlappingEntries", {type: "checkbox"})} {...formValues.getInputProps("allowOverlappingEntries", {type: "checkbox"})}

View File

@ -8,19 +8,18 @@
} }
.slotRow { .slotRow {
cursor: pointer;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
gap: var(--gap); gap: var(--gap);
width: 100%; width: 100%;
& > :nth-child(2) { & > :nth-child(1) {
width: 50%; width: 50%;
font-size: var(--mantine-font-size-sm); font-size: var(--mantine-font-size-sm);
} }
& > :nth-child(3) { & > :nth-child(2) {
flex: 1; flex: 1;
} }
} }

View File

@ -1,27 +1,24 @@
import {EventListModel, EventListSlotsWithEntriesCountModel, EventModel} from "@/models/EventTypes.ts"; import {EventListModel, EventListSlotsWithEntriesCountModel} from "@/models/EventTypes.ts";
import {useDisclosure} from "@mantine/hooks"; import {useDisclosure} from "@mantine/hooks";
import classes from "./EventListSlotRow.module.css"; import classes from "./ListSlotRow.module.css";
import {ActionIcon, Alert, Code, Collapse, Group, Modal, ThemeIcon} from "@mantine/core"; import {ActionIcon, Alert, Code, Group, Modal} from "@mantine/core";
import {IconAdjustments, IconAdjustmentsOff, IconInfoCircle, IconUsersMinus, IconUsersPlus} from "@tabler/icons-react"; import {IconAdjustments, IconAdjustmentsOff, IconInfoCircle} from "@tabler/icons-react";
import {RenderDateRange} from "../../components/RenderDateRange.tsx"; import {RenderDateRange} from "@/pages/events/e/:eventId/EventLists/EventListComponents/RenderDateRange.tsx";
import {modals} from "@mantine/modals"; import {modals} from "@mantine/modals";
import InnerHtml from "@/components/InnerHtml"; import InnerHtml from "@/components/InnerHtml";
import {EventListSlotEntriesTable} from "./EventListSlotEntriesTable.tsx";
import UpsertEventListSlot from "@/pages/events/e/:eventId/EventLists/components/UpsertEventListSlot.tsx";
import EventListSlotProgress from "@/pages/events/e/:eventId/EventLists/components/EventListSlotProgress.tsx";
import ShowDebug from "@/components/ShowDebug.tsx"; import ShowDebug from "@/components/ShowDebug.tsx";
import UpsertSlot from "@/pages/events/e/:eventId/EventLists/EventListComponents/UpsertSlot.tsx";
import SlotProgress from "@/pages/events/e/:eventId/EventLists/EventListComponents/SlotProgress.tsx";
import {pprintDateTime} from "@/lib/datetime.ts";
export const EventListSlotRow = ({slot, list, event, refetch}: { export const ListSlotRow = ({slot, list, refetch}: {
slot: EventListSlotsWithEntriesCountModel, slot: EventListSlotsWithEntriesCountModel,
list: EventListModel, list: EventListModel,
event: EventModel,
refetch: () => void refetch: () => void
}) => { }) => {
const [showEditModal, showEditModalHandler] = useDisclosure(false) const [showEditModal, showEditModalHandler] = useDisclosure(false)
const [expanded, expandedHandler] = useDisclosure(false)
return ( return (
<div className={`${classes.slotContainer}`}> <div className={`${classes.slotContainer}`}>
<Modal <Modal
@ -29,22 +26,15 @@ export const EventListSlotRow = ({slot, list, event, refetch}: {
title={`Zeitslot bearbeiten`} title={`Zeitslot bearbeiten`}
size={"lg"} size={"lg"}
> >
<UpsertEventListSlot slot={slot} list={list} onSuccess={() => { <UpsertSlot slot={slot} list={list} onSuccess={() => {
showEditModalHandler.close() showEditModalHandler.close()
refetch() refetch()
}} onAbort={showEditModalHandler.close}/> }} onAbort={showEditModalHandler.close}/>
</Modal> </Modal>
<div <div className={classes.slotRow}>
className={classes.slotRow}
onClick={expandedHandler.toggle}
>
<ThemeIcon variant={"transparent"}>
{
expanded ? <IconUsersMinus size={16}/> : <IconUsersPlus size={16}/>
}
</ThemeIcon>
<RenderDateRange start={new Date(slot.startDate)} end={new Date(slot.endDate)}/> <RenderDateRange start={new Date(slot.startDate)} end={new Date(slot.endDate)}/>
<EventListSlotProgress slot={slot}/> <SlotProgress slot={slot}/>
<Group gap={4} justify="right" wrap="nowrap"> <Group gap={4} justify="right" wrap="nowrap">
<ActionIcon <ActionIcon
size="sm" size="sm"
@ -67,9 +57,11 @@ export const EventListSlotRow = ({slot, list, event, refetch}: {
title: 'Beschreibung', title: 'Beschreibung',
children: <div className={"stack"}> children: <div className={"stack"}>
<ShowDebug> <ShowDebug>
Listen-ID <Code>{list.id}</Code> Slot Id - <Code>{slot.id}</Code>
<br/> <br/>
Slot-ID <Code>{slot.id}</Code> created - <Code>{pprintDateTime(slot.created)}</Code>
<br/>
updated - <Code>{pprintDateTime(slot.updated)}</Code>
</ShowDebug> </ShowDebug>
{ {
slot.description ? <InnerHtml html={slot.description}/> : slot.description ? <InnerHtml html={slot.description}/> :
@ -85,10 +77,6 @@ export const EventListSlotRow = ({slot, list, event, refetch}: {
</ActionIcon> </ActionIcon>
</Group> </Group>
</div> </div>
<Collapse in={expanded}>
<EventListSlotEntriesTable visible={expanded} slot={slot} list={list} refetch={refetch} event={event}/>
</Collapse>
</div> </div>
) )
} }

View File

@ -1,4 +1,4 @@
import {EventListModel, EventModel} from "@/models/EventTypes.ts"; import {EventListModel} from "@/models/EventTypes.ts";
import {PocketBaseErrorAlert, usePB} from "@/lib/pocketbase.tsx"; import {PocketBaseErrorAlert, usePB} from "@/lib/pocketbase.tsx";
import {useQuery} from "@tanstack/react-query"; import {useQuery} from "@tanstack/react-query";
import {Box, Button, Center, Pagination, Title} from "@mantine/core"; import {Box, Button, Center, Pagination, Title} from "@mantine/core";
@ -6,8 +6,8 @@ import {IconClockPlus} from "@tabler/icons-react";
import {useState} from "react"; import {useState} from "react";
import {useDisclosure} from "@mantine/hooks"; import {useDisclosure} from "@mantine/hooks";
import classes from './index.module.css' import classes from './index.module.css'
import {EventListSlotRow} from "./EventListSlotRow.tsx"; import {ListSlotRow} from "./ListSlotRow.tsx";
import UpsertEventListSlot from "@/pages/events/e/:eventId/EventLists/components/UpsertEventListSlot.tsx"; import UpsertSlot from "@/pages/events/e/:eventId/EventLists/EventListComponents/UpsertSlot.tsx";
/** /**
* Renders a table with all slots for a list * Renders a table with all slots for a list
@ -16,7 +16,7 @@ import UpsertEventListSlot from "@/pages/events/e/:eventId/EventLists/components
* @param event - event the list belongs to * @param event - event the list belongs to
* @constructor * @constructor
*/ */
export default function SlotsTable({list, event}: { event: EventModel, list: EventListModel }) { export default function ListSlots({list}: { list: EventListModel }) {
const {pb} = usePB() const {pb} = usePB()
@ -39,8 +39,8 @@ export default function SlotsTable({list, event}: { event: EventModel, list: Eve
<div className={classes.table}> <div className={classes.table}>
{query.data?.items.map(slot => ( {query.data?.items.map(slot => (
<EventListSlotRow <ListSlotRow
key={slot.id} event={event} list={list} slot={slot} key={slot.id} list={list} slot={slot}
refetch={query.refetch} refetch={query.refetch}
/> />
))} ))}
@ -53,7 +53,7 @@ export default function SlotsTable({list, event}: { event: EventModel, list: Eve
Neuen Slot hinzufügen Neuen Slot hinzufügen
</Title> </Title>
<UpsertEventListSlot list={list} onSuccess={() => { <UpsertSlot list={list} onSuccess={() => {
showNewSlotFormHandler.close() showNewSlotFormHandler.close()
query.refetch() query.refetch()
}} onAbort={showNewSlotFormHandler.close}/> }} onAbort={showNewSlotFormHandler.close}/>

View File

@ -4,19 +4,18 @@ import {showSuccessNotification} from "@/components/util.tsx";
import {useConfirmModal} from "@/components/ConfirmModal.tsx"; import {useConfirmModal} from "@/components/ConfirmModal.tsx";
import {usePB} from "@/lib/pocketbase.tsx"; import {usePB} from "@/lib/pocketbase.tsx";
import {useDisclosure} from "@mantine/hooks"; import {useDisclosure} from "@mantine/hooks";
import { import {ActionIcon, HoverCard, Menu} from "@mantine/core";
UpdateEventListSlotEntryStatusModal import {IconArrowsMove, IconCheckupList, IconSend, IconSettings, IconTrash} from "@tabler/icons-react";
} from "@/pages/events/e/:eventId/EventLists/components/UpdateEventListSlotEntryStatusModal.tsx";
import {
MoveEventListSlotEntryModal
} from "@/pages/events/e/:eventId/EventLists/components/MoveEventListSlotEntryModal.tsx";
import {
UpdateEventListSlotEntryFormModal
} 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/users/modals/util.tsx"; import {getUserName} from "@/components/users/modals/util.tsx";
import {
UpdateEntryStatusModal
} from "@/pages/events/e/:eventId/EventLists/EventListComponents/UpdateEntryStatusModal.tsx";
import {MoveEntryModal} from "@/pages/events/e/:eventId/EventLists/EventListComponents/MoveEntryModal.tsx";
import EntryStatusSpoiler from "@/pages/events/e/:eventId/EventLists/EventListComponents/EntryStatusSpoiler.tsx";
import {getListSchemas} from "@/pages/events/util.ts";
export default function EditSlotEntryMenu({entry, refetch}: { export default function EditSlotEntryMenu({entry, refetch}: {
refetch: () => void, refetch: () => void,
entry: EventListSlotEntriesWithUserModel entry: EventListSlotEntriesWithUserModel
@ -28,8 +27,6 @@ export default function EditSlotEntryMenu({entry, refetch}: {
const [showMoveEntryModal, showMoveEntryModalHandler] = useDisclosure(false) const [showMoveEntryModal, showMoveEntryModalHandler] = useDisclosure(false)
const [showEditFormModal, showEditFormModalHandler] = useDisclosure(false)
const deleteEntryMutation = useMutation({ const deleteEntryMutation = useMutation({
mutationFn: async () => { mutationFn: async () => {
await pb.collection("eventListSlotEntries").delete(entry.id) await pb.collection("eventListSlotEntries").delete(entry.id)
@ -46,62 +43,67 @@ export default function EditSlotEntryMenu({entry, refetch}: {
onConfirm: () => deleteEntryMutation.mutate() onConfirm: () => deleteEntryMutation.mutate()
}) })
const {statusSchema} = getListSchemas(entry)
const noStatusField = statusSchema.fields.length === 0
return <> return <>
<ConfirmModal/> <ConfirmModal/>
<UpdateEventListSlotEntryStatusModal <UpdateEntryStatusModal
opened={showStatusEditModal} opened={showStatusEditModal}
close={showStatusEditModalHandler.close} close={showStatusEditModalHandler.close}
entry={entry} entry={entry}
refetch={refetch} refetch={refetch}
/> />
<MoveEventListSlotEntryModal <MoveEntryModal
opened={showMoveEntryModal} opened={showMoveEntryModal}
close={showMoveEntryModalHandler.close} close={showMoveEntryModalHandler.close}
entry={entry} entry={entry}
refetch={refetch} refetch={refetch}
/> />
<UpdateEventListSlotEntryFormModal <HoverCard width={'350px'} shadow="md" position={"top"} closeDelay={0} disabled={noStatusField}>
opened={showEditFormModal} <HoverCard.Target>
close={showEditFormModalHandler.close} <ActionIcon
refetch={refetch} variant="light"
entry={entry} color="blue"
/>
<Button
leftSection={<IconCheckupList size={16}/>}
variant={"light"}
size={"compact-xs"}
onClick={showStatusEditModalHandler.toggle} onClick={showStatusEditModalHandler.toggle}
aria-label={"edit entry status"}
disabled={noStatusField}
> >
Status bearbeiten <IconCheckupList size={16}/>
</Button> </ActionIcon>
</HoverCard.Target>
<HoverCard.Dropdown p={0}>
<EntryStatusSpoiler entry={entry}/>
</HoverCard.Dropdown>
</HoverCard>
<Menu> <Menu>
<Menu.Target> <Menu.Target>
<ActionIcon <ActionIcon
size="sm"
variant="light" variant="light"
color="blue" color="blue"
aria-label={"edit entry"}
> >
<IconSettings size={16}/> <IconSettings size={16}/>
</ActionIcon> </ActionIcon>
</Menu.Target> </Menu.Target>
<Menu.Dropdown> <Menu.Dropdown>
<Menu.Item
leftSection={<IconSend size={16}/>}
disabled
>
Person benachrichtigen
</Menu.Item>
<Menu.Item <Menu.Item
leftSection={<IconArrowsMove size={16}/>} leftSection={<IconArrowsMove size={16}/>}
onClick={showMoveEntryModalHandler.toggle} onClick={showMoveEntryModalHandler.toggle}
> >
Eintrag verschieben Eintrag verschieben
</Menu.Item> </Menu.Item>
<Menu.Item
leftSection={<IconForms size={16}/>}
onClick={showEditFormModalHandler.toggle}
>
Formular
</Menu.Item>
<Menu.Item <Menu.Item
color={"red"} leftSection={<IconTrash size={16}/>} color={"red"} leftSection={<IconTrash size={16}/>}
onClick={toggleConfirmModal} onClick={toggleConfirmModal}

View File

@ -1,25 +1,3 @@
.entryContainer {
margin-left: var(--mantine-spacing-lg);
display: flex;
flex-direction: column;
}
.entryRow {
display: flex;
justify-content: space-between;
align-items: center;
gap: var(--gap);
width: 100%;
& > * {
font-size: var(--mantine-font-size-sm);
}
& > :nth-child(2) {
flex: 1;
}
}
.dataGrid { .dataGrid {
padding: var(--gap) 0; padding: var(--gap) 0;
} }
@ -43,4 +21,7 @@
color: var(--mantine-color-dimmed); color: var(--mantine-color-dimmed);
border-bottom: var(--border); border-bottom: var(--border);
margin-bottom: var(--mantine-spacing-xs); margin-bottom: var(--mantine-spacing-xs);
word-break: break-all;
overflow-wrap: break-word;
hyphens: auto;
} }

View File

@ -0,0 +1,53 @@
import {EventListSlotEntriesWithUserModel} from "@/models/EventTypes.ts";
import {getListSchemas} from "@/pages/events/util.ts";
import {Box, SimpleGrid} from "@mantine/core";
import classes from "./EntryQuestionAndStatusData.module.css";
import {renderEntries} from "@/components/formUtil/formTable";
import {IconCheckupList, IconForms} from "@tabler/icons-react";
import RenderCell from "@/components/formUtil/formTable/RenderCell.tsx";
/**
* This component renders the details of an event list slot entry
* It displays the question and status data of the entry
* @param entry - the entry to display
*/
export const EntryQuestionAndStatusData = ({entry}: {
entry: EventListSlotEntriesWithUserModel
}) => {
const {questionSchema, statusSchema} = getListSchemas(entry)
return <>
<SimpleGrid className={classes.dataGrid} cols={{base: 2, sm: 3, lg: 4}}>
{renderEntries(entry.entryQuestionData ?? {}, questionSchema.fields).map(({label, value}) => {
return <div className={classes.dataCell} key={label}>
<div className={classes.dataIcon}>
<IconForms size={16}/>
</div>
<div className={classes.dataTitle}>
{label}
</div>
<Box>
<RenderCell entry={value}/>
</Box>
</div>
})}
{renderEntries(entry.entryStatusData ?? {}, statusSchema.fields).map(({label, value}) => {
return <div className={classes.dataCell} key={label}>
<div className={classes.dataIcon}>
<IconCheckupList size={16}/>
</div>
<div className={classes.dataTitle}>
{label}
</div>
<Box>
<RenderCell entry={value}/>
</Box>
</div>
})}
</SimpleGrid>
</>
}

View File

@ -0,0 +1,30 @@
.statusCell {
display: flex;
align-items: center;
flex-direction: row;
justify-content: space-between;
flex-wrap: nowrap;
gap: var(--gap);
padding: 10px;
&:not(:last-of-type) {
border-bottom: 1px solid var(--border-color);
}
}
.statusTitle {
width: 40%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.statusValue {
width: 60%;
overflow: hidden;
display: flex;
align-items: center;
justify-content: start;
}

View File

@ -0,0 +1,23 @@
import {EventListSlotEntriesWithUserModel} from "@/models/EventTypes.ts";
import {getListSchemas} from "@/pages/events/util.ts";
import {renderEntries} from "@/components/formUtil/formTable";
import classes from "./EntryStatusSpoiler.module.css";
import RenderCell from "@/components/formUtil/formTable/RenderCell.tsx";
export default function EntryStatusSpoiler({entry}: { entry: EventListSlotEntriesWithUserModel }) {
const {statusSchema} = getListSchemas(entry)
return <div>
{renderEntries(entry.entryStatusData ?? {}, statusSchema.fields).map(({label, value}) => {
return <div className={`${classes.statusCell} hover`} key={label}>
<div className={classes.statusTitle}>
{label}
</div>
<div className={classes.statusValue}>
<RenderCell entry={value}/>
</div>
</div>
})}
</div>
}

View File

@ -2,12 +2,15 @@ import {EventListSlotEntriesWithUserModel} from "@/models/EventTypes.ts";
import {PocketBaseErrorAlert, usePB} from "@/lib/pocketbase.tsx"; import {PocketBaseErrorAlert, usePB} from "@/lib/pocketbase.tsx";
import {useMutation, useQuery} from "@tanstack/react-query"; import {useMutation, useQuery} from "@tanstack/react-query";
import {showSuccessNotification} from "@/components/util.tsx"; import {showSuccessNotification} from "@/components/util.tsx";
import {Alert, Button, Group, Modal, Select} from "@mantine/core"; import {Alert, Button, Group, Modal, Select, SelectProps, Text} 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/users/modals/util.tsx"; import {getUserName, RenderUserName} from "@/components/users/modals/util.tsx";
import {CheckboxCard} from "@/components/input/CheckboxCard";
import {IconCheck} from "@tabler/icons-react";
import SlotProgress from "@/pages/events/e/:eventId/EventLists/EventListComponents/SlotProgress.tsx";
export const MoveEventListSlotEntryModal = ({opened, close, refetch, entry}: { export const MoveEntryModal = ({opened, close, refetch, entry}: {
opened: boolean, opened: boolean,
close: () => void, close: () => void,
refetch: () => void, refetch: () => void,
@ -20,6 +23,8 @@ export const MoveEventListSlotEntryModal = ({opened, close, refetch, entry}: {
const [selectedList, setSelectedList] = useState<string | null>(entry.eventList) const [selectedList, setSelectedList] = useState<string | null>(entry.eventList)
const [showFullSlots, setShowFullSlots] = useState(false)
useEffect(() => { useEffect(() => {
setSelectedSlot(null) setSelectedSlot(null)
}, [selectedList]); }, [selectedList]);
@ -52,17 +57,34 @@ export const MoveEventListSlotEntryModal = ({opened, close, refetch, entry}: {
}) })
const slotsQuery = useQuery({ const slotsQuery = useQuery({
queryKey: ["eventListSlots", entry.eventList, "notFull", selectedList], queryKey: ["eventListSlots", entry.eventList, "notFull", selectedList, showFullSlots],
queryFn: async () => ( queryFn: async () => (
await pb.collection("eventListSlotsWithEntriesCount").getFullList({ await pb.collection("eventListSlotsWithEntriesCount").getFullList({
filter: `eventList='${selectedList}' filter: `eventList='${selectedList}'
&&(maxEntries=0 || entriesCount < maxEntries)`, ${showFullSlots ? "" : "&&(maxEntries=0 || entriesCount < maxEntries)"} `,
sort: "startDate" sort: "startDate"
}) })
), ),
enabled: selectedList !== null && opened enabled: selectedList !== null && opened
}) })
const renderSelectOption: SelectProps['renderOption'] = ({option, checked}) => {
const slot = slotsQuery.data?.find(slot => slot.id === option.value)
if (!slot) return <>Not Found</>
return (
<Group flex="1" gap="xs">
<Text size={"xs"} fw={checked ? 700 : undefined}>
{option.label}
</Text>
{<SlotProgress slot={slot} compact/>}
{checked && <IconCheck style={{marginInlineStart: 'auto'}} size={16}/>}
</Group>
)
}
return <Modal return <Modal
size={"lg"} size={"lg"}
opened={opened} opened={opened}
@ -103,10 +125,18 @@ export const MoveEventListSlotEntryModal = ({opened, close, refetch, entry}: {
value: slot.id, value: slot.id,
label: `${pprintDateTime(slot.startDate)} - ${pprintDateTime(slot.endDate)}` label: `${pprintDateTime(slot.startDate)} - ${pprintDateTime(slot.endDate)}`
})) ?? []} })) ?? []}
renderOption={renderSelectOption}
value={selectedSlot} value={selectedSlot}
onChange={(value) => setSelectedSlot(value)} onChange={(value) => setSelectedSlot(value)}
/> />
<CheckboxCard
label={"Zeitslots mit voller Belegung anzeigen"}
description={"Achtung: Wenn diese Option aktiviert ist, werden auch Zeitslots zum verschieben angezeigt, die bereits voll belegt sind."}
checked={showFullSlots}
onChange={() => setShowFullSlots(!showFullSlots)}
/>
{ {
selectedList !== entry.eventList && ( selectedList !== entry.eventList && (
<Alert color={"orange"} title={"Achtung!"}> <Alert color={"orange"} title={"Achtung!"}>

View File

@ -1,17 +1,41 @@
import {EventListSlotsWithEntriesCountModel} from "@/models/EventTypes.ts"; import {EventListSlotsWithEntriesCountModel} from "@/models/EventTypes.ts";
import {Badge, Group, Progress, Text, ThemeIcon, Tooltip} from "@mantine/core"; import {Badge, Group, Progress, Text, ThemeIcon, Tooltip} from "@mantine/core";
import {IconX} from "@tabler/icons-react"; import {IconX} from "@tabler/icons-react";
import classes from "./ListSlotProgress.module.css" import classes from "./SlotProgress.module.css"
/** /**
* Displays a progress status for the number of entries in a slot. * Displays a progress status for the number of entries in a slot.
* If the slot is unlimited or full, the message will reflect that. * If the slot is unlimited or full, the message will reflect that.
* @param slot The slot to display the progress for * @param slot The slot to display the progress for
*/ */
export default function EventListSlotProgress({slot}: { slot: EventListSlotsWithEntriesCountModel }) { export default function SlotProgress({slot, compact}: {
slot: EventListSlotsWithEntriesCountModel,
compact?: boolean
}) {
const slotIsUnlimited = slot.maxEntries === 0
const slotFull = !slotIsUnlimited && slot.entriesCount >= slot.maxEntries
const freeSlots = slotIsUnlimited ? 0 : slot.maxEntries - slot.entriesCount
const occupiedSlots = slot.entriesCount
if (compact) {
return <>
{slotIsUnlimited ? (
<Text c={"green"} size={"xs"}>{slot.entriesCount} Anmeldungen (unbegrenzte Plätze)</Text>) : (
slotFull ? (
<Text c={"red"} size={"xs"}>Voll - ({slot.maxEntries} Plätze)</Text>
) : (
<Text c={"green"} size={"xs"}>{freeSlots} Plätze frei
({slot.maxEntries} Plätze insgesamt)</Text>
)
)}
</>
}
// if there are no max entries, the slot is unlimited // if there are no max entries, the slot is unlimited
if (slot.maxEntries === 0 || slot.maxEntries === null) { if (slotIsUnlimited) {
return ( return (
<Group align={"center"} justify={"center"}> <Group align={"center"} justify={"center"}>
<Tooltip <Tooltip
@ -32,13 +56,12 @@ export default function EventListSlotProgress({slot}: { slot: EventListSlotsWith
<ThemeIcon size={"sm"} variant={"transparent"} color={"orange"}> <ThemeIcon size={"sm"} variant={"transparent"} color={"orange"}>
<IconX/> <IconX/>
</ThemeIcon> </ThemeIcon>
<Text mt={3} fz="sm" fw={700} c={"orange"}>Alle Plätze <u>{slot.entriesCount}/{slot.maxEntries}</u> belegt</Text> <Text mt={3} fz="sm" fw={700} c={"orange"}>Alle
Plätze <u>{slot.entriesCount}/{slot.maxEntries}</u> belegt</Text>
</Group> </Group>
) )
} }
const freeSlots = slot.maxEntries - slot.entriesCount
const occupiedSlots = slot.entriesCount
// if the slot is not full and has a max entry count // if the slot is not full and has a max entry count
return <> return <>

View File

@ -6,8 +6,9 @@ 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/users/modals/util.tsx"; import {getUserName, RenderUserName} from "@/components/users/modals/util.tsx";
import {getListSchemas} from "@/pages/events/util.ts";
export const UpdateEventListSlotEntryFormModal = ({opened, close, refetch, entry}: { export const UpdateEntryFormModal = ({opened, close, refetch, entry}: {
opened: boolean, opened: boolean,
close: () => void, close: () => void,
refetch: () => void, refetch: () => void,
@ -30,10 +31,7 @@ export const UpdateEventListSlotEntryFormModal = ({opened, close, refetch, entry
} }
}) })
const questionSchemaFields = [ const {questionSchema} = getListSchemas(entry)
...entry.entryQuestionSchema?.fields ?? [],
...entry.defaultEntryQuestionSchema?.fields ?? [],
]
return <Modal return <Modal
size={"lg"} size={"lg"}
@ -44,7 +42,7 @@ export const UpdateEventListSlotEntryFormModal = ({opened, close, refetch, entry
<PocketBaseErrorAlert error={mutation.error}/> <PocketBaseErrorAlert error={mutation.error}/>
<FormInput <FormInput
schema={{fields: questionSchemaFields}} schema={questionSchema}
onSubmit={mutation.mutateAsync} onSubmit={mutation.mutateAsync}
onAbort={close} onAbort={close}
initialData={entry.entryQuestionData ?? undefined} initialData={entry.entryQuestionData ?? undefined}

View File

@ -8,8 +8,9 @@ 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/users/modals/util.tsx"; import {getUserName, RenderUserName} from "@/components/users/modals/util.tsx";
import {getListSchemas} from "@/pages/events/util.ts";
export const UpdateEventListSlotEntryStatusModal = ({opened, close, refetch, entry}: { export const UpdateEntryStatusModal = ({opened, close, refetch, entry}: {
opened: boolean, opened: boolean,
close: () => void, close: () => void,
refetch: () => void, refetch: () => void,
@ -18,11 +19,6 @@ export const UpdateEventListSlotEntryStatusModal = ({opened, close, refetch, ent
const {pb} = usePB() const {pb} = usePB()
const statusSchemaFields = [
...entry.entryStatusSchema?.fields ?? [],
...entry.defaultEntryStatusSchema?.fields ?? [],
]
const mutation = useMutation({ const mutation = useMutation({
mutationFn: async (values: FieldEntries) => { mutationFn: async (values: FieldEntries) => {
return await pb.collection("eventListSlotEntries").update(entry.id, { return await pb.collection("eventListSlotEntries").update(entry.id, {
@ -36,6 +32,8 @@ export const UpdateEventListSlotEntryStatusModal = ({opened, close, refetch, ent
} }
}) })
const {statusSchema} = getListSchemas(entry)
return <Modal return <Modal
size={"lg"} size={"lg"}
opened={opened} opened={opened}
@ -47,24 +45,18 @@ export const UpdateEventListSlotEntryStatusModal = ({opened, close, refetch, ent
<Alert title={`${entry.listName}`} color={"blue"} mb={"sm"}> <Alert title={`${entry.listName}`} color={"blue"} mb={"sm"}>
<RenderDateRange start={new Date(entry.slotStartDate)} end={new Date(entry.slotEndDate)}/> <RenderDateRange start={new Date(entry.slotStartDate)} end={new Date(entry.slotEndDate)}/>
</Alert>
{(entry.listDescription || entry.slotDescription) && ( {(entry.listDescription || entry.slotDescription) && (
<Alert title={"Beschreibung"} color={"blue"} mb={"sm"}> <>
<br/>
{entry.listDescription && ( {entry.listDescription && <InnerHtml html={entry.listDescription}/>}
<InnerHtml html={entry.listDescription}/> <br/>
{entry.slotDescription && <InnerHtml html={entry.slotDescription}/>}
</>
)} )}
{entry.slotDescription && (
<InnerHtml html={entry.slotDescription}/>
)}
</Alert> </Alert>
)}
<FormInput <FormInput
schema={{fields: statusSchemaFields}} schema={statusSchema}
onSubmit={mutation.mutateAsync} onSubmit={mutation.mutateAsync}
onAbort={close} onAbort={close}
initialData={entry.entryStatusData ?? undefined} initialData={entry.entryStatusData ?? undefined}

View File

@ -9,7 +9,7 @@ import TextEditor from "@/components/input/Editor";
import ShowHelp from "@/components/ShowHelp.tsx"; import ShowHelp from "@/components/ShowHelp.tsx";
import {formatDuration} from "@/lib/datetime.ts"; import {formatDuration} from "@/lib/datetime.ts";
export default function UpsertEventListSlot({list, slot, onSuccess, onAbort}: { export default function UpsertSlot({list, slot, onSuccess, onAbort}: {
list: EventListModel, list: EventListModel,
slot?: EventListSlotModel, slot?: EventListSlotModel,
onSuccess: () => void, onSuccess: () => void,

View File

@ -1,27 +0,0 @@
.container {
background-color: var(--mantine-color-body);
border: var(--border);
border-radius: var(--border-radius);
padding: var(--mantine-spacing-xs);
}
.row {
display: flex;
justify-content: start;
align-items: center;
gap: var(--gap);
font-size: var(--mantine-font-size-sm);
& > :nth-child(2) {
width: 20%;
}
& > :nth-child(3) {
width: 20%;
}
& > :nth-child(4) {
flex: 1;
}
}

View File

@ -1,56 +0,0 @@
import {EventListSlotEntriesWithUserModel} from "@/models/EventTypes.ts";
import classes from "./EventListSearchResult.module.css";
import {ActionIcon, Collapse, ThemeIcon, Tooltip} from "@mantine/core";
import {IconChevronDown, IconChevronRight, IconList, IconUser} from "@tabler/icons-react";
import TextWithIcon from "@/components/layout/TextWithIcon";
import {
EventListSlotEntryDetails
} from "@/pages/events/e/:eventId/EventLists/:listId/EventListSlotsTable/EventListSlotEntryRow.tsx";
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/users/modals/util.tsx";
export default function EventListSearchResult({entry, refetch}: {
entry: EventListSlotEntriesWithUserModel,
refetch: () => void
}) {
const [expanded, expandedHandler] = useDisclosure(false)
return <div className={classes.container}>
<div className={classes.row}>
<Tooltip label={expanded ? "Details verbergen" : "Details anzeigen"} withArrow>
<ActionIcon onClick={expandedHandler.toggle} variant={"light"} size={"xs"}>
{expanded ? <IconChevronDown/> : <IconChevronRight/>}
</ActionIcon>
</Tooltip>
<TextWithIcon icon={
<ThemeIcon variant={"transparent"} size={"xs"} color={"gray"}>
<IconUser/>
</ThemeIcon>
}>
<RenderUserName user={entry.expand?.user}/>
</TextWithIcon>
<TextWithIcon icon={
<ThemeIcon variant={"transparent"} size={"xs"} color={"gray"}>
<IconList/>
</ThemeIcon>
}>
{entry.listName}
</TextWithIcon>
<RenderDateRange start={new Date(entry.slotStartDate)} end={new Date(entry.slotEndDate)}/>
<EditSlotEntryMenu entry={entry} refetch={refetch}/>
</div>
<Collapse in={expanded}>
<EventListSlotEntryDetails entry={entry}/>
</Collapse>
</div>
}

View File

@ -1,133 +0,0 @@
import {EventListModel, EventModel} from "@/models/EventTypes.ts";
import {
ActionIcon,
Autocomplete,
Center,
Collapse,
Group,
Loader,
Pagination,
Text,
Title,
Tooltip
} from "@mantine/core";
import {IconFilter, IconFilterOff, IconUserSearch} from "@tabler/icons-react";
import {useQuery} from "@tanstack/react-query";
import {useDebouncedValue, useDisclosure} from "@mantine/hooks";
import {usePB} from "@/lib/pocketbase.tsx";
import {useState} from "react";
import {onlyUnique} from "@/lib/util.ts";
import EventListSearchResult from "@/pages/events/e/:eventId/EventLists/EventListSearch/EventListSearchResult.tsx";
import ListSelect from "@/pages/events/e/:eventId/EventLists/ListSelect.tsx";
export default function ListSearch({event}: { event: EventModel }) {
const {pb} = usePB()
const [showFilter, showFilterHandler] = useDisclosure(true)
const [searchQueryString, setSearchQueryString] = useState('')
const [selectedLists, setSelectedLists] = useState<EventListModel[]>([])
const [debouncedSearchQueryString] = useDebouncedValue(searchQueryString, 200)
const [page, setPage] = useState(1)
const searchQuery = useQuery({
queryKey: ["eventListSearch", {event: event.id}, {debouncedSearchQueryString}, {page}, {selectedLists}],
queryFn: async () => {
const filter: string[] = [`event='${event.id}'`]
if (debouncedSearchQueryString) {
filter.push(`user.username ~ '${debouncedSearchQueryString.trim().replace(" ", ".")}'`)
}
if (selectedLists.length > 0) {
filter.push(`(${selectedLists.map(l => `eventList='${l.id}'`).join(" || ")})`)
}
return await pb.collection("eventListSlotEntriesWithUser").getList(1, 50, {
filter: filter.join(" && "),
expand: "user"
})
}
})
return <div className={"stack"}>
<Title order={4} c={"blue"}>Listen Durchsuchen</Title>
<Autocomplete
placeholder={"Nach Namen suchen ..."}
leftSection={<IconUserSearch/>}
rightSection={searchQuery.isLoading ? <Loader size={"xs"}/> : (
<Tooltip label={"Anmeldungen Filtern"} withArrow>
<ActionIcon
onClick={showFilterHandler.toggle}
variant={"transparent"}
aria-label={"toggle filter"}
>
{showFilter ? <IconFilterOff/> : <IconFilter/>}
</ActionIcon>
</Tooltip>
)}
data={
(searchQuery
.data
?.items
.map(e => e.expand?.user.username)
.filter(u => u !== undefined)
.filter(onlyUnique) ?? []) as string[]
}
value={searchQueryString} onChange={setSearchQueryString}
/>
<Collapse in={showFilter}>
<ListSelect
event={event}
selectedRecords={selectedLists}
setSelectedRecords={setSelectedLists}
placeholder={"Anmeldungen nur für bestimmte Listen anzeigen"}
/>
</Collapse>
<Group justify={"space-between"}>
<Text c={"dimmed"} size={"xs"}>
{searchQueryString ? `Suche nach Person '${searchQueryString}'` : "Alle Anmeldungen für dieses Event"}
</Text>
<Text c={"dimmed"} size={"xs"}>
{searchQuery.data?.totalItems ?? 0} {searchQueryString ? "Ergebnisse" : "Anmeldungen"}
</Text>
</Group>
{
/*
todo: more filter
<div className={"section stack"}>
<Title order={4} c={"blue"}>
Filter
</Title>
<Text> Formularfelder Auswahl </Text>
<Text> Statusfelder Auswahl </Text>
</div>
*/
}
{
searchQuery.data?.items.map((entry, index) => (
<EventListSearchResult
key={index}
entry={entry}
refetch={() => searchQuery.refetch()}
/>
))
}
<Center>
<Pagination size={"xs"} total={searchQuery.data?.totalPages ?? 0} value={page} onChange={setPage}/>
</Center>
</div>
}

View File

@ -5,12 +5,12 @@ import EventListRouter from "./:listId/EventListRouter.tsx";
import {IconCheckupList, IconForms, IconLink, IconList, IconUserSearch} from "@tabler/icons-react"; import {IconCheckupList, IconForms, IconLink, IconList, IconUserSearch} from "@tabler/icons-react";
import {ReactNode} from "react"; import {ReactNode} from "react";
import {Menu} from "@mantine/core"; import {Menu} from "@mantine/core";
import EventListSearch from "./EventListSearch/index.tsx"; import EventListSearch from "@/pages/events/e/:eventId/EventLists/Search/index.tsx";
import EventListShare from "@/pages/events/e/:eventId/EventLists/EventListShare.tsx"; import ShareEventLists from "@/pages/events/e/:eventId/EventLists/ShareEventLists.tsx";
import EditEventDefaultEntryStatusSchema import EditDefaultEntryStatusSchema
from "@/pages/events/e/:eventId/EventLists/EventListSettings/EditEventDefaultEntryStatusSchema.tsx"; from "@/pages/events/e/:eventId/EventLists/GeneralListSettings/EditDefaultEntryStatusSchema.tsx";
import EditEventDefaultEntryQuestionSchema import EditDefaultEntryQuestionSchema
from "@/pages/events/e/:eventId/EventLists/EventListSettings/EditEventDefaultEntryQuestionSchema.tsx"; from "@/pages/events/e/:eventId/EventLists/GeneralListSettings/EditDefaultEntryQuestionSchema.tsx";
const nav = [ const nav = [
@ -18,17 +18,17 @@ const nav = [
label: "Listenaktionen", label: "Listenaktionen",
children: [ children: [
{ {
title: "Übersicht", title: "Listen und Slots",
icon: <IconList size={16}/>, icon: <IconList size={16}/>,
to: "overview" to: "overview"
}, },
{ {
title: "Durchsuchen", title: "Anmeldungen",
icon: <IconUserSearch size={16}/>, icon: <IconUserSearch size={16}/>,
to: "search" to: "search"
}, },
{ {
title: "Teilen", title: "Listen Teilen",
icon: <IconLink size={16}/>, icon: <IconLink size={16}/>,
to: "share" to: "share"
} }
@ -69,7 +69,7 @@ export const EventListsMenu = ({event, target}: { event: EventModel, target: Rea
<Menu.Dropdown> <Menu.Dropdown>
{ {
nav.map(({label, children,}, index, array) => <div key={index}> nav.map(({label, children,}, index) => <div key={index}>
<Menu.Label>{label}</Menu.Label> <Menu.Label>{label}</Menu.Label>
{children.map(({title, icon, to}) => ( {children.map(({title, icon, to}) => (
<NavLink to={`/events/e/${event.id}/lists/${to}`} key={to}> <NavLink to={`/events/e/${event.id}/lists/${to}`} key={to}>
@ -80,7 +80,6 @@ export const EventListsMenu = ({event, target}: { event: EventModel, target: Rea
)} )}
</NavLink> </NavLink>
))} ))}
{index !== array.length - 1 && <Menu.Divider/>}
</div>) </div>)
} }
</Menu.Dropdown> </Menu.Dropdown>
@ -95,10 +94,10 @@ export default function EventListsRouter({event}: { event: EventModel }) {
<Route index element={<Navigate to={"overview"} replace/>}/> <Route index element={<Navigate to={"overview"} replace/>}/>
<Route path={"search"} element={<EventListSearch event={event}/>}/> <Route path={"search"} element={<EventListSearch event={event}/>}/>
<Route path={"share"} element={<EventListShare event={event}/>}/> <Route path={"share"} element={<ShareEventLists event={event}/>}/>
<Route path={"e/status"} element={<EditEventDefaultEntryStatusSchema event={event}/>}/> <Route path={"e/status"} element={<EditDefaultEntryStatusSchema event={event}/>}/>
<Route path={"e/questions"} element={<EditEventDefaultEntryQuestionSchema event={event}/>}/> <Route path={"e/questions"} element={<EditDefaultEntryQuestionSchema event={event}/>}/>
<Route path={"overview"} element={<EventListsOverview event={event}/>}/> <Route path={"overview"} element={<EventListsOverview event={event}/>}/>
<Route path={"overview/:listId/*"} element={<EventListRouter event={event}/>}/> <Route path={"overview/:listId/*"} element={<EventListRouter event={event}/>}/>

View File

@ -14,7 +14,7 @@ import {IconAlertTriangle, IconInfoCircle} from "@tabler/icons-react";
* This component allows the user to edit the default questions for all eventLists of an event. * This component allows the user to edit the default questions for all eventLists of an event.
* @param event The event to edit the default questions for. * @param event The event to edit the default questions for.
*/ */
export default function EditEventDefaultEntryQuestionSchema({event}: { event: EventModel }) { export default function EditDefaultEntryQuestionSchema({event}: { event: EventModel }) {
const {pb} = usePB() const {pb} = usePB()

View File

@ -14,7 +14,7 @@ import {IconAlertTriangle} from "@tabler/icons-react";
* This component allows the user to edit the default entry status fields for all eventLists of an event. * This component allows the user to edit the default entry status fields for all eventLists of an event.
* @param event The event to edit the default entry status fields for. * @param event The event to edit the default entry status fields for.
*/ */
export default function EditEventDefaultEntryStatusSchema({event}: { event: EventModel }) { export default function EditDefaultEntryStatusSchema({event}: { event: EventModel }) {
const {pb} = usePB() const {pb} = usePB()

View File

@ -19,7 +19,6 @@ export default function ListSelect(props: GenericRecordSearchInputProps<EventLis
recordSearchMutation={ recordSearchMutation={
useMutation({ useMutation({
mutationFn: async (search: string) => { mutationFn: async (search: string) => {
const filter: string[] = [] const filter: string[] = []
if (search) { if (search) {

View File

@ -0,0 +1,49 @@
.mainGrid {
display: grid;
grid-template-columns: auto auto auto 0.5fr;
gap: var(--gap);
}
.subgrid {
display: grid;
grid-template-columns: subgrid;
grid-column: span 4;
background-color: var(--mantine-color-body);
border: var(--border);
border-radius: var(--border-radius);
padding: var(--mantine-spacing-sm);
font-size: var(--mantine-font-size-sm);
@media (max-width: $mantine-breakpoint-sm) {
font-size: var(--mantine-font-size-sm);
display: flex;
flex-direction: column;
gap: var(--gap);
}
}
.child {
display: flex;
justify-content: start;
align-items: center;
text-align: start;
gap: var(--gap);
word-wrap: break-word; /* Ensures text wraps within the cell */
overflow-wrap: break-word; /* Ensures text wraps within the cell */
hyphens: auto;
@media (max-width: $mantine-breakpoint-sm) {
}
}
.alignEnd {
@media (min-width: $mantine-breakpoint-sm) {
justify-content: end;
}
}
.detailsContainer {
grid-column: span 4; /* Spans all columns of the main grid */
}

View File

@ -0,0 +1,91 @@
import {EventListSlotEntriesWithUserModel} from "@/models/EventTypes.ts";
import classes from "./EventEntries.module.css";
import {ActionIcon, Code, Collapse, ThemeIcon, Tooltip} from "@mantine/core";
import {IconEye, IconEyeOff, IconList, IconUser} from "@tabler/icons-react";
import TextWithIcon from "@/components/layout/TextWithIcon";
import {useDisclosure} from "@mantine/hooks";
import {
EntryQuestionAndStatusData
} from "@/pages/events/e/:eventId/EventLists/EventListComponents/EntryQuestionAndStatusData.tsx";
import ShowDebug from "@/components/ShowDebug.tsx";
import {pprintDateTime} from "@/lib/datetime.ts";
import {RenderUserName} from "@/components/users/modals/util.tsx";
import {RenderDateRange} from "@/pages/events/e/:eventId/EventLists/EventListComponents/RenderDateRange.tsx";
import EditSlotEntryMenu from "@/pages/events/e/:eventId/EventLists/EventListComponents/EditSlotEntryMenu.tsx";
import {Link} from "react-router-dom";
function EventEntry({entry, refetch}: {
entry: EventListSlotEntriesWithUserModel,
refetch: () => void
}) {
const [expanded, expandedHandler] = useDisclosure(false)
return <>
<div className={classes.subgrid}>
<div className={classes.child}>
<TextWithIcon icon={
<ThemeIcon variant={"transparent"} size={"xs"} color={"gray"}>
<IconUser/>
</ThemeIcon>
}>
<RenderUserName user={entry.expand?.user}/>
</TextWithIcon>
</div>
<Link to={`/events/e/${entry.event}/lists/overview/${entry.eventList}`} className={classes.child}>
<TextWithIcon icon={
<ThemeIcon variant={"transparent"} size={"xs"} color={"gray"}>
<IconList/>
</ThemeIcon>
}>
{entry.listName}
</TextWithIcon>
</Link>
<div className={classes.child}>
<RenderDateRange start={new Date(entry.slotStartDate)} end={new Date(entry.slotEndDate)}/>
</div>
<div className={`${classes.child} ${classes.alignEnd}`}>
<Tooltip label={expanded ? "Details verbergen" : "Details anzeigen"} withArrow>
<ActionIcon onClick={expandedHandler.toggle} variant={"light"} size={"md"}>
{expanded ? <IconEyeOff/> : <IconEye/>}
</ActionIcon>
</Tooltip>
<EditSlotEntryMenu entry={entry} refetch={refetch}/>
</div>
</div>
<Collapse in={expanded} className={classes.detailsContainer}>
<ShowDebug>
Entry Id - <Code>{entry.id}</Code>
<br/>
created - <Code>{pprintDateTime(entry.created)}</Code>
<br/>
updated - <Code>{pprintDateTime(entry.updated)}</Code>
<br/>
<br/>
Entry User Id - <Code>{entry.user}</Code>
<br/>
Entry Event Id - <Code>{entry.event}</Code>
<br/>
Entry List Id - <Code>{entry.eventList}</Code>
<br/>
Entry Slot Id - <Code>{entry.eventListsSlot}</Code>
</ShowDebug>
<EntryQuestionAndStatusData entry={entry}/>
</Collapse>
</>
}
export default function EventEntries({entries, refetch}: {
entries: EventListSlotEntriesWithUserModel[],
refetch: () => void
}) {
return <div className={classes.mainGrid}>
{entries.map(entry => <EventEntry key={entry.id} entry={entry} refetch={refetch}/>)}
</div>
}

View File

@ -0,0 +1,4 @@
.resultsContainer {
display: grid;
gap: var(--gap);
}

View File

@ -0,0 +1,226 @@
import {EventListModel, EventModel} from "@/models/EventTypes.ts";
import {
ActionIcon,
Autocomplete,
Button,
Center,
Collapse,
Group,
Loader,
Popover,
Text,
Title,
Tooltip
} from "@mantine/core";
import {IconArrowDown, IconFilter, IconFilterEdit, IconFilterOff, IconSend, IconUserSearch} from "@tabler/icons-react";
import {useInfiniteQuery} from "@tanstack/react-query";
import {useDebouncedValue, useDisclosure} from "@mantine/hooks";
import {PocketBaseErrorAlert, usePB} from "@/lib/pocketbase.tsx";
import {onlyUnique} from "@/lib/util.ts";
import ListSelect from "@/pages/events/e/:eventId/EventLists/ListSelect.tsx";
import FormFilter from "@/components/formUtil/FormFilter";
import {useForm} from "@mantine/form";
import {FieldEntriesFilter} from "@/components/formUtil/FromInput/types.ts";
import {assembleFilter} from "@/components/formUtil/FormFilter/util.ts";
import {DateTimePicker} from "@mantine/dates";
import EventEntries from "@/pages/events/e/:eventId/EventLists/Search/EventEntries.tsx";
export default function ListSearch({event}: { event: EventModel }) {
const {pb} = usePB()
const [showFilter, showFilterHandler] = useDisclosure(true)
const formValues = useForm({
initialValues: {
searchQueryString: '',
selectedLists: [] as EventListModel[],
questionFilter: {} as FieldEntriesFilter,
statusFilter: {} as FieldEntriesFilter,
createdByMin: null as Date | null,
createdByMax: null as Date | null,
}
})
const [debouncedFormValues] = useDebouncedValue(formValues.values, 200)
/**
* Assemble the filter string for the search query
* @param filterValues The filter values (debounced form values)
*/
const filterString = (filterValues: typeof debouncedFormValues) => {
// this array will hold all specified filter conditions
const filter: string[] = [`event='${event.id}'`]
// filter for user by name
filterValues.searchQueryString && filter.push(`user.username ~ '${filterValues.searchQueryString.trim().replace(" ", ".")}'`)
// filter for lists
filterValues.selectedLists.length > 0 && filter.push(`(${filterValues.selectedLists.map(l => `eventList='${l.id}'`).join(" || ")})`)
// filter for questions
const questionFilter = assembleFilter(filterValues.questionFilter, "entryQuestionData")
questionFilter.length !== 0 && filter.push(`(${questionFilter.join(" && ")})`)
// filter for status
const statusFilter = assembleFilter(filterValues.statusFilter, "entryStatusData")
statusFilter.length !== 0 && filter.push(`(${statusFilter.join(" && ")})`)
// filter for created date
filterValues.createdByMin && filter.push(`created>='${filterValues.createdByMin.toISOString()}'`)
filterValues.createdByMax && filter.push(`created<='${filterValues.createdByMax.toISOString()}'`)
// join all filter conditions with '&&' to create the final filter string
return filter.join(" && ")
}
const query = useInfiniteQuery({
queryKey: ["eventListSearch", {event: event.id}, {filter: filterString(debouncedFormValues)}],
queryFn: async ({pageParam}) => {
return await pb.collection("eventListSlotEntriesWithUser").getList(pageParam, 50, {
filter: filterString(debouncedFormValues),
expand: "user"
})
},
getNextPageParam: (lastPage) =>
lastPage.page >= lastPage.totalPages ? undefined : lastPage.page + 1,
initialPageParam: 1,
})
// this string informs the user about the current list selection
const listInfoString = `${formValues.values.selectedLists.length > 0 ? ` in den Listen ${formValues.values.selectedLists.map(l => `'${l.name}'`).join(", ")}` : ""}`
const entries = query.data?.pages.flatMap(p => p.items) ?? []
const entriesCount = query.data?.pages?.[0]?.totalItems ?? 0
return <div className={"stack"}>
<Title order={4} c={"blue"}>Listen Anmeldungen</Title>
<Autocomplete
placeholder={"Nach Namen suchen ..."}
leftSection={<IconUserSearch/>}
rightSection={query.isPending ? <Loader size={"xs"}/> : (
<Tooltip label={"Anmeldungen Filtern"} withArrow>
<ActionIcon
onClick={showFilterHandler.toggle}
variant={"transparent"}
aria-label={"toggle filter"}
>
{showFilter ? <IconFilterOff/> : <IconFilter/>}
</ActionIcon>
</Tooltip>
)}
data={
(entries
.map(e => e.expand?.user.username)
.filter(u => u !== undefined)
.filter(onlyUnique) ?? []) as string[]
}
value={formValues.values.searchQueryString}
onChange={(v) => formValues.setFieldValue("searchQueryString", v)}
/>
<Collapse in={showFilter}>
<div className={"stack"}>
<ListSelect
event={event}
selectedRecords={formValues.values.selectedLists}
setSelectedRecords={(ls) => formValues.setFieldValue("selectedLists", ls)}
placeholder={"Anmeldungen nur für bestimmte Listen anzeigen"}
/>
<Group>
<FormFilter
label={"Fragen"}
schema={{
fields: [
...event.defaultEntryQuestionSchema?.fields ?? [],
...formValues.values.selectedLists.map(l => ([
...l.entryQuestionSchema?.fields ?? [],
])).flat()
]
}}
defaultValue={formValues.values.questionFilter}
onChange={(qf) => formValues.setFieldValue("questionFilter", qf)}
/>
<FormFilter
label={"Status"}
schema={{
fields: [
...event.defaultEntryStatusSchema?.fields ?? [],
...formValues.values.selectedLists.map(l => ([
...l.entryStatusSchema?.fields ?? [],
])).flat()
]
}}
defaultValue={formValues.values.statusFilter}
onChange={(sf) => formValues.setFieldValue("statusFilter", sf)}
/>
<Popover width={300} position="bottom" withArrow shadow="md">
<Popover.Target>
<Button size={"xs"} leftSection={<IconFilterEdit size={16}/>}>
{"Anmeldungsdatum"}
</Button>
</Popover.Target>
<Popover.Dropdown>
<div className={"stack"}>
<DateTimePicker
clearable
placeholder={`Angemeldet ab ...`}
popoverProps={{withinPortal: false}}
value={formValues.values.createdByMin}
onChange={(v) => formValues.setFieldValue("createdByMin", v)}
/>
<DateTimePicker
clearable
placeholder={`Angemeldet bis ...`}
popoverProps={{withinPortal: false}}
value={formValues.values.createdByMax}
onChange={(v) => formValues.setFieldValue("createdByMax", v)}
/>
</div>
</Popover.Dropdown>
</Popover>
<Button size={"xs"} leftSection={<IconSend size={16}/>} disabled>
{entriesCount} Personen benachrichtigen
</Button>
</Group>
</div>
</Collapse>
<Group justify={"space-between"}>
<Text c={"dimmed"} size={"xs"}>
{formValues.values.searchQueryString ?
`Suche nach Person '${formValues.values.searchQueryString}'${listInfoString}` :
`Alle Anmeldungen für dieses Event${listInfoString}`}
</Text>
<Text c={"dimmed"} size={"xs"}>
{entriesCount} Ergebnisse
</Text>
</Group>
<PocketBaseErrorAlert error={query.error}/>
<EventEntries entries={entries} refetch={() => query.refetch()}/>
{query.hasNextPage && (
<Center p={"xs"}>
<Button
disabled={query.isFetchingNextPage || !query.hasNextPage}
loading={query.isFetchingNextPage}
variant={"transparent"}
size={"compact-xs"}
leftSection={<IconArrowDown/>}
onClick={() => query.fetchNextPage()}
>
Mehr Anmeldungen laden
</Button>
</Center>
)}
</div>
}

View File

@ -8,7 +8,7 @@ import {APP_URL} from "../../../../../../config.ts";
import {Link} from "react-router-dom"; import {Link} from "react-router-dom";
export default function EventListShare({event}: { event: EventModel }) { export default function ShareEventLists({event}: { event: EventModel }) {
const [selectedLists, setSelectedLists] = useState<EventListModel[]>([]) const [selectedLists, setSelectedLists] = useState<EventListModel[]>([])

View File

@ -2,22 +2,23 @@ import {EventListSlotEntriesWithUserModel} from "@/models/EventTypes.ts";
import {ActionIcon, Collapse, Group, Text, ThemeIcon, Tooltip} from "@mantine/core"; import {ActionIcon, Collapse, Group, Text, ThemeIcon, Tooltip} from "@mantine/core";
import {IconChevronDown, IconChevronRight, IconConfetti, IconForms, IconList, IconTrash} from "@tabler/icons-react"; import {IconChevronDown, IconChevronRight, IconConfetti, IconForms, IconList, IconTrash} from "@tabler/icons-react";
import TextWithIcon from "@/components/layout/TextWithIcon"; import TextWithIcon from "@/components/layout/TextWithIcon";
import {
EventListSlotEntryDetails
} from "@/pages/events/e/:eventId/EventLists/:listId/EventListSlotsTable/EventListSlotEntryRow.tsx";
import {useDisclosure} from "@mantine/hooks"; import {useDisclosure} from "@mantine/hooks";
import {RenderDateRange} from "@/pages/events/e/:eventId/EventLists/components/RenderDateRange.tsx";
import classes from "./UserEntryRow.module.css" import classes from "./UserEntryRow.module.css"
import {humanDeltaFromNow} from "@/lib/datetime.ts"; import {humanDeltaFromNow} from "@/lib/datetime.ts";
import {useMutation} from "@tanstack/react-query"; import {useMutation} from "@tanstack/react-query";
import {showSuccessNotification} from "@/components/util.tsx"; import {showSuccessNotification} from "@/components/util.tsx";
import {useConfirmModal} from "@/components/ConfirmModal.tsx"; import {useConfirmModal} from "@/components/ConfirmModal.tsx";
import {usePB} from "@/lib/pocketbase.tsx"; import {usePB} from "@/lib/pocketbase.tsx";
import {
UpdateEventListSlotEntryFormModal
} from "@/pages/events/e/:eventId/EventLists/components/UpdateEventListSlotEntryFormModal.tsx";
import {getUserName} from "@/components/users/modals/util.tsx"; import {getUserName} from "@/components/users/modals/util.tsx";
import {UpdateEntryFormModal} from "@/pages/events/e/:eventId/EventLists/EventListComponents/UpdateEntryFormModal.tsx";
import {RenderDateRange} from "@/pages/events/e/:eventId/EventLists/EventListComponents/RenderDateRange.tsx";
import {
EntryQuestionAndStatusData
} from "@/pages/events/e/:eventId/EventLists/EventListComponents/EntryQuestionAndStatusData.tsx";
export default function UserEntryRow({entry, refetch}: { export default function UserEntryRow({entry, refetch}: {
entry: EventListSlotEntriesWithUserModel, entry: EventListSlotEntriesWithUserModel,
@ -51,7 +52,7 @@ export default function UserEntryRow({entry, refetch}: {
<ConfirmModal/> <ConfirmModal/>
<UpdateEventListSlotEntryFormModal <UpdateEntryFormModal
opened={showEditFormModal} opened={showEditFormModal}
close={showEditFormModalHandler.close} close={showEditFormModalHandler.close}
refetch={refetch} refetch={refetch}
@ -123,7 +124,7 @@ export default function UserEntryRow({entry, refetch}: {
</div> </div>
<Collapse in={expanded}> <Collapse in={expanded}>
<EventListSlotEntryDetails entry={entry}/> <EntryQuestionAndStatusData entry={entry}/>
</Collapse> </Collapse>
</div> </div>
} }

View File

@ -1,7 +1,5 @@
import {EventListModel, 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 EventListSlotProgress from "@/pages/events/e/:eventId/EventLists/components/EventListSlotProgress.tsx";
import {useDisclosure} from "@mantine/hooks"; import {useDisclosure} from "@mantine/hooks";
import {Alert, Collapse, ThemeIcon, Tooltip, UnstyledButton} from "@mantine/core"; import {Alert, Collapse, ThemeIcon, Tooltip, UnstyledButton} from "@mantine/core";
import {IconChevronDown, IconChevronRight, IconInfoCircle} from "@tabler/icons-react"; import {IconChevronDown, IconChevronRight, IconInfoCircle} from "@tabler/icons-react";
@ -12,25 +10,22 @@ 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 {useNavigate} from "react-router-dom"; import {useNavigate} from "react-router-dom";
import {getListSchemas} from "@/pages/events/util.ts";
import {RenderDateRange} from "@/pages/events/e/:eventId/EventLists/EventListComponents/RenderDateRange.tsx";
import SlotProgress from "@/pages/events/e/:eventId/EventLists/EventListComponents/SlotProgress.tsx";
export default function EventListSlotView({slot, list, refetch}: { export default function EventListSlotView({slot, list, refetch}: {
list: EventListModel, list: EventListModel,
slot: EventListSlotsWithEntriesCountModel, slot: EventListSlotsWithEntriesCountModel,
refetch: () => void refetch: () => void
}) { }) {
const [expanded, expandedHandler] = useDisclosure(false) const [expanded, expandedHandler] = useDisclosure(false)
const slotIsFull = slot.maxEntries === 0 || slot.maxEntries === null ? false : slot.entriesCount >= slot.maxEntries const slotIsFull = slot.maxEntries === 0 || slot.maxEntries === null ? false : slot.entriesCount >= slot.maxEntries
const slotIsInPast = new Date(slot.endDate) < new Date() const slotIsInPast = new Date(slot.endDate) < new Date()
const schema = { const {questionSchema} = getListSchemas(slot)
fields: [
...slot.entryQuestionSchema?.fields ?? [],
...slot.defaultEntryQuestionSchema?.fields ?? [],
]
}
const {pb, user} = usePB() const {pb, user} = usePB()
@ -64,7 +59,7 @@ export default function EventListSlotView({slot, list, refetch}: {
<RenderDateRange start={new Date(slot.startDate)} end={new Date(slot.endDate)}/> <RenderDateRange start={new Date(slot.startDate)} end={new Date(slot.endDate)}/>
<EventListSlotProgress slot={slot}/> <SlotProgress slot={slot}/>
</div> </div>
</UnstyledButton> </UnstyledButton>
@ -97,7 +92,7 @@ export default function EventListSlotView({slot, list, refetch}: {
: :
<FormInput <FormInput
disabled={!user || slotIsFull} disabled={!user || slotIsFull}
schema={schema} schema={questionSchema}
onSubmit={createEntryMutation.mutateAsync} onSubmit={createEntryMutation.mutateAsync}
/> />
} }

View File

@ -1,4 +1,8 @@
import {EventModel} from "@/models/EventTypes.ts"; import {
EventListSlotEntriesWithUserModel,
EventListSlotsWithEntriesCountModel,
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"; import {LdapGroupModel} from "@/models/AuthTypes.ts";
@ -21,3 +25,27 @@ export const useEventRights = (event?: EventModel) => {
canEditEventList: false canEditEventList: false
} }
} }
/**
* Returns the schemas for the entry question and status
*
* @param slot The slot to get the schemas for
*/
export const getListSchemas = (slot: EventListSlotEntriesWithUserModel | EventListSlotsWithEntriesCountModel) => {
return {
questionSchema: {
fields: [
...slot.entryQuestionSchema?.fields ?? [],
...(!slot.ignoreDefaultEntryQuestionScheme ? slot.defaultEntryQuestionSchema?.fields ?? [] : []),
]
},
statusSchema: {
fields: [
...slot.entryStatusSchema?.fields ?? [],
...(!slot.ignoreDefaultEntryStatusSchema ? slot.defaultEntryStatusSchema?.fields ?? [] : []),
]
}
}
}

View File

@ -1,10 +1,12 @@
import {useShowDebug} from "@/components/ShowDebug.tsx"; import {useShowDebug} from "@/components/ShowDebug.tsx";
import {ActionIcon, Alert, Group, LoadingOverlay, Text, TextInput, Title} from "@mantine/core"; import {ActionIcon, Alert, Group, LoadingOverlay, Pagination, Text, TextInput, Title} from "@mantine/core";
import {useForm} from "@mantine/form"; import {useForm} from "@mantine/form";
import {useQuery} from "@tanstack/react-query"; import {useQuery} from "@tanstack/react-query";
import {PocketBaseErrorAlert, usePB} from "@/lib/pocketbase.tsx"; import {PocketBaseErrorAlert, usePB} from "@/lib/pocketbase.tsx";
import {CodeHighlight} from "@mantine/code-highlight"; import {CodeHighlight} from "@mantine/code-highlight";
import {IconRefresh} from "@tabler/icons-react"; import {IconRefresh} from "@tabler/icons-react";
import {RecordListOptions} from "pocketbase";
import {useState} from "react";
export default function DebugPage() { export default function DebugPage() {
@ -13,6 +15,8 @@ export default function DebugPage() {
const {pb} = usePB() const {pb} = usePB()
const [page, setPage] = useState(1)
const formValues = useForm({ const formValues = useForm({
initialValues: { initialValues: {
collectionName: "", collectionName: "",
@ -25,11 +29,16 @@ export default function DebugPage() {
const debugQuery = useQuery({ const debugQuery = useQuery({
queryKey: ["debug", formValues.values.collectionName, formValues.values.filter, formValues.values.sort, formValues.values.expand], queryKey: ["debug", formValues.values.collectionName, formValues.values.filter, formValues.values.sort, formValues.values.expand],
queryFn: async () => { queryFn: async () => {
return await pb.collection(formValues.values.collectionName).getList(1, 10, {
filter: formValues.values.filter, const options: RecordListOptions = {}
sort: formValues.values.sort,
expand: formValues.values.expand if (formValues.values.filter) options["filter"] = formValues.values.filter
})
if (formValues.values.sort) options["sort"] = formValues.values.sort
if (formValues.values.expand) options["expand"] = formValues.values.expand
return await pb.collection(formValues.values.collectionName).getList(page, 10, options)
}, },
enabled: formValues.values.collectionName !== "" enabled: formValues.values.collectionName !== ""
}) })
@ -79,9 +88,7 @@ export default function DebugPage() {
{debugQuery.data.totalItems} Ergebniss(e) {debugQuery.data.totalItems} Ergebniss(e)
</Text> </Text>
<Text c={"dimmed"}> <Pagination size={"xs"} value={page} onChange={setPage} total={debugQuery.data.totalPages ?? 1}/>
{debugQuery.data.page}/{debugQuery.data.totalPages} Seiten
</Text>
<ActionIcon <ActionIcon
onClick={() => debugQuery.refetch()} onClick={() => debugQuery.refetch()}
@ -96,6 +103,7 @@ export default function DebugPage() {
</Group> </Group>
<CodeHighlight code={JSON.stringify(debugQuery.data.items, null, 2)} language="json"/> <CodeHighlight code={JSON.stringify(debugQuery.data.items, null, 2)} language="json"/>
</div>} </div>}
</div> </div>
</> </>

View File

@ -14,7 +14,7 @@
} }
.tabler-icon { .tabler-icon {
stroke-width: 1.5; stroke-width: 1.2;
} }
.scrollable { .scrollable {
@ -145,6 +145,14 @@ a:hover, a:active, a:visited {
align-items: center; align-items: center;
} }
.wrapWords {
overflow: hidden;
white-space: pre-wrap;
word-wrap: break-word; /* Ensures text wraps within the cell */
overflow-wrap: break-word; /* Ensures text wraps within the cell */
hyphens: auto;
}
.monospace { .monospace {
font-family: var(--mantine-font-family-monospace); font-family: var(--mantine-font-family-monospace);
} }

View File

@ -461,68 +461,68 @@
"@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/resolve-uri" "^3.1.0"
"@jridgewell/sourcemap-codec" "^1.4.14" "@jridgewell/sourcemap-codec" "^1.4.14"
"@mantine/code-highlight@^7.8.0": "@mantine/code-highlight@^7.10.0":
version "7.8.0" version "7.10.0"
resolved "https://registry.yarnpkg.com/@mantine/code-highlight/-/code-highlight-7.8.0.tgz#de91ef30224ae95d40c4a1b3ecf68f8c89772d50" resolved "https://registry.yarnpkg.com/@mantine/code-highlight/-/code-highlight-7.10.0.tgz#189c0c109168a1b2aeab0069a6376b6538e827db"
integrity sha512-AeuOG5TuKPOv2ifHrvwlOfNCaFpDdSwXCKq336oFBUm3Jq3B3pK+pVn3ZnX+6ccMQHwsHf/Lyv2muWHG54IEDA== integrity sha512-7dep/kbqdDihCz3RYM5/q28I8M6aLgcZl0iQQN04zP12ZDgqWJ5NiyiNUodZCF4H34X/7XtQdz+cxVesJffXqw==
dependencies: dependencies:
clsx "2.1.0" clsx "^2.1.1"
highlight.js "^11.9.0" highlight.js "^11.9.0"
"@mantine/core@^7.8.0": "@mantine/core@^7.10.0":
version "7.8.0" version "7.10.0"
resolved "https://registry.yarnpkg.com/@mantine/core/-/core-7.8.0.tgz#b4bbd82ea2f1a25f5fb3d11ae5583cf80ecd8383" resolved "https://registry.yarnpkg.com/@mantine/core/-/core-7.10.0.tgz#bfaafc92cf2346e5a6cbb49289f577ce3f7c05f7"
integrity sha512-19RKuNdJ/s8pZjy2w2rvTsl4ybi/XM6vf+Kc0WY7kpLFCvdG+/UxNi1MuJF8t2Zs0QSFeb/H5yZQNe0XPbegHw== integrity sha512-hNqhdn/+4x8+FDWzR5fu1eMgnG1Mw4fZHw4WjIYjKrSv0NeKHY263RiesZz8RwcUQ8r7LlD95/2tUOMnKVTV5Q==
dependencies: dependencies:
"@floating-ui/react" "^0.26.9" "@floating-ui/react" "^0.26.9"
clsx "2.1.0" clsx "^2.1.1"
react-number-format "^5.3.1" react-number-format "^5.3.1"
react-remove-scroll "^2.5.7" react-remove-scroll "^2.5.7"
react-textarea-autosize "8.5.3" react-textarea-autosize "8.5.3"
type-fest "^4.12.0" type-fest "^4.12.0"
"@mantine/dates@^7.8.0": "@mantine/dates@^7.10.0":
version "7.8.0" version "7.10.0"
resolved "https://registry.yarnpkg.com/@mantine/dates/-/dates-7.8.0.tgz#a8785030000487158e1bd23655ea26245bbf299a" resolved "https://registry.yarnpkg.com/@mantine/dates/-/dates-7.10.0.tgz#0c2a02883d5fb4a36b40a578b26ef5a697c333e5"
integrity sha512-9jjiYMwP3jQOpOLKkjhp9uf2BGhtEbOnOzyAlpLOS0CJJlYtB0tO6dJ3JaogrOZ/Yfee7ZUBgouCG5EkR4/qtQ== integrity sha512-LBBh1U/RzxFQKGA6sSYxbCwYEMoM5lNIhwofY6g8zOTAZuRQqo5FIWItmB9I9ltT+M2o75SADeP6ZBLi4ec8ZA==
dependencies: dependencies:
clsx "2.1.0" clsx "^2.1.1"
"@mantine/form@^7.8.0": "@mantine/form@^7.10.0":
version "7.8.0" version "7.10.0"
resolved "https://registry.yarnpkg.com/@mantine/form/-/form-7.8.0.tgz#3f16b2e0124c65286892ed50181d192ae03d988b" resolved "https://registry.yarnpkg.com/@mantine/form/-/form-7.10.0.tgz#3e8e3fb836948becb13b89412c74016b50bac3d3"
integrity sha512-Qn3/69zGt/p3wyMwGz2V0+FbmvqC2/PvXaeyO0a4CnwhROeE7ObyCKXDcBmgapOSBHr/7wFvMeTDMaTMfe3DXw== integrity sha512-ChAtqdQCAZrnH6iiCivumyMuMsev+tFWIgsCCgAmbP2sOyMtjbNtypKrcwBwI/PzAH9N4jSJlsmJsnRdXNeEkQ==
dependencies: dependencies:
fast-deep-equal "^3.1.3" fast-deep-equal "^3.1.3"
klona "^2.0.6" klona "^2.0.6"
"@mantine/hooks@^7.8.0": "@mantine/hooks@^7.10.0":
version "7.8.0" version "7.10.0"
resolved "https://registry.yarnpkg.com/@mantine/hooks/-/hooks-7.8.0.tgz#fc32e07746689459c4b049dc581d1dbda5545686" resolved "https://registry.yarnpkg.com/@mantine/hooks/-/hooks-7.10.0.tgz#10a259e204a8af29df6aeeb24090c1e2c6debca0"
integrity sha512-+70fkgjhVJeJ+nJqnburIM3UAsfvxat1Low9HMPobLbv64FIdB4Nzu5ct3qojNQ58r5sK01tg5UoFIJYslaVrg== integrity sha512-fnalwYS2WQEFS4wmhmAetDZ/VdJPLNeUXPX9t+S21o3p/dRTX1xhU2mS7yWaQUKM0hPD1TcujqXGlP2M2g/A9A==
"@mantine/modals@^7.9.0": "@mantine/modals@^7.10.0":
version "7.9.0" version "7.10.0"
resolved "https://registry.yarnpkg.com/@mantine/modals/-/modals-7.9.0.tgz#aedfb29de1630fc0498aebf5d3c17acfae14321b" resolved "https://registry.yarnpkg.com/@mantine/modals/-/modals-7.10.0.tgz#c08789491bfbfb1d432818e0fc4b2eac71fd480e"
integrity sha512-shPoUmuAj6587DbGjc3AWpGRnxDvHftw7Ps7Xqcoy7ScxQ8+HpynBk9j7ojyXMolovK4hThW4oZdysPfpjQyaA== integrity sha512-UVtmRpTBWDqcJjdv97IUYLduYcZBrqteyDwnspHT453iFZlvCglHUXYR+LvN5ExE+kxUe2IUXL/pEaIRTjwtKQ==
"@mantine/notifications@^7.8.1": "@mantine/notifications@^7.10.0":
version "7.8.1" version "7.10.0"
resolved "https://registry.yarnpkg.com/@mantine/notifications/-/notifications-7.8.1.tgz#ce2a9e97753a4f18a5e7b1822c3be42bda43fd85" resolved "https://registry.yarnpkg.com/@mantine/notifications/-/notifications-7.10.0.tgz#aa638b8bb6c3d6bfb34d518a49ef8a8b6ab499e4"
integrity sha512-HpjZs45EmBYYmJj72+hbwLVxbO7nbZrkpjcrYc40/VIbcDjjJBXDZrAX4VLu7ZIjkbeoxxPgxp245XheC68ikA== integrity sha512-3a0mmM9Kr3nPP+8VHsIuly507nda6ciu2aB/xSxb7gFIKHw3GqSu77pxXa+5l4Y6AQKKvP9360K4KjH6+rOBWw==
dependencies: dependencies:
"@mantine/store" "7.8.1" "@mantine/store" "7.10.0"
react-transition-group "4.4.5" react-transition-group "4.4.5"
"@mantine/store@7.8.1": "@mantine/store@7.10.0":
version "7.8.1" version "7.10.0"
resolved "https://registry.yarnpkg.com/@mantine/store/-/store-7.8.1.tgz#75edadd8b42491466f681f1d23de528826b7cdff" resolved "https://registry.yarnpkg.com/@mantine/store/-/store-7.10.0.tgz#68368c6ca5b75cfb331220e06a3235be753df055"
integrity sha512-lX1l8zPd7LazenTKqmI/E17BssRLpf/c8aKYiwV1ZgNONWc+XlWr+6MW3GLKB5uM/DS1Zg0bAYAaRUtKGP1MIQ== integrity sha512-B6AyUX0cA97/hI9v0att7eJJnQTcUG7zBlTdWhOsptBV5UoDNrzdv3DDWIFxrA8h+nhNKGBh6Dif5HWh1+QLeA==
"@mantine/tiptap@^7.8.0": "@mantine/tiptap@^7.10.0":
version "7.8.0" version "7.10.0"
resolved "https://registry.yarnpkg.com/@mantine/tiptap/-/tiptap-7.8.0.tgz#390eb44659bf01d898335f6063c3d6231bb8e756" resolved "https://registry.yarnpkg.com/@mantine/tiptap/-/tiptap-7.10.0.tgz#77926f0a2d81c05e3f4084e65786778d5227fa56"
integrity sha512-0O766+DZ8/L2VX//7GKUCfOiMyC7lJXfLmMNqQdth2JtlGoDzWDAF7hhQos0hBor+rT7ci3gPLVMv39l2U0CQw== integrity sha512-C8wURpoh1dduWPgGgyknVc+E9/gDZVOMIyPxZXNx/r74/OoaE8tu8tgF/T21t8DtCQ4jter0PEGZFDB9hIXuag==
"@nodelib/fs.scandir@2.1.5": "@nodelib/fs.scandir@2.1.5":
version "2.1.5" version "2.1.5"
@ -1396,11 +1396,6 @@ character-reference-invalid@^2.0.0:
optionalDependencies: optionalDependencies:
fsevents "~2.3.2" fsevents "~2.3.2"
clsx@2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.0.tgz#e851283bcb5c80ee7608db18487433f7b23f77cb"
integrity sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==
clsx@^1.2.1: clsx@^1.2.1:
version "1.2.1" version "1.2.1"
resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12" resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12"