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
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_STORAGE_KEY = "stuve-it-login-record"
// general
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"

View File

@ -13,14 +13,14 @@
"@fontsource/fira-code": "^5.0.15",
"@fontsource/overpass": "^5.0.15",
"@hello-pangea/dnd": "^16.6.0",
"@mantine/code-highlight": "^7.8.0",
"@mantine/core": "^7.8.0",
"@mantine/dates": "^7.8.0",
"@mantine/form": "^7.8.0",
"@mantine/hooks": "^7.8.0",
"@mantine/modals": "^7.9.0",
"@mantine/notifications": "^7.8.1",
"@mantine/tiptap": "^7.8.0",
"@mantine/code-highlight": "^7.10.0",
"@mantine/core": "^7.10.0",
"@mantine/dates": "^7.10.0",
"@mantine/form": "^7.10.0",
"@mantine/hooks": "^7.10.0",
"@mantine/modals": "^7.10.0",
"@mantine/notifications": "^7.10.0",
"@mantine/tiptap": "^7.10.0",
"@tabler/icons-react": "^3.2.0",
"@tanstack/react-query": "^5.0.5",
"@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
}) => {
return <Stack gap={5}>
<Input.Label required>{field.label}</Input.Label>
<Input.Label required component={"div"}>{field.label}</Input.Label>
{field.description && <>
<Input.Description>
<Input.Description component={"div"}>
<Spoiler
maxHeight={40}
showLabel={<Text span size={"xs"}>Mehr anzeigen</Text>}

View File

@ -1,5 +1,5 @@
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 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 {
CheckboxField,
DateField, DateRangeField,
DateField,
DateRangeField,
EmailField,
FormTextareaField,
FormTextField,
NumberField, SelectField
NumberField,
SelectField
} 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)
@ -24,7 +28,7 @@ import {
* @param initialEntries The already existing entries
*/
const createDefaultValuesFromSchema = (schema: FormSchema, initialEntries?: FieldEntries): FieldEntries => {
const entries: FieldEntries = initialEntries ?? {}
const entries: FieldEntries = transformData(initialEntries ?? {})
schema.fields.forEach(field => {
// if the field already exists, skip it
@ -166,9 +170,25 @@ export default function FormInput({schema, onAbort, onSubmit, disabled, initialD
</Group>
<ShowDebug>
<pre>
{JSON.stringify(formValues, null, 2)}
</pre>
<Table w={"500px"} data={{
body: schema.fields.map(field => ([
<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>
</form>
}

View File

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

View File

@ -35,7 +35,7 @@ const DateTimeCell = ({date}: { date: Date | string }) => {
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) {
return <EmptyCell/>

View File

@ -1,5 +1,7 @@
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 => {
switch (fieldType) {

View File

@ -11,7 +11,7 @@
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);
}
}

View File

@ -7,11 +7,12 @@ export type CheckboxCardProps = InputWrapperProps & CheckboxProps
export function CheckboxCard(props: CheckboxCardProps) {
return (
<div className={classes.container} aria-error={!!props.error}>
<div className={classes.container} data-error={!!props.error}>
<Checkbox
checked={props.checked}
onChange={props.onChange}
required={props.required}
indeterminate={props.indeterminate}
size="md"
mr="xl"
styles={{input: {cursor: 'pointer'}}}
@ -19,9 +20,11 @@ export function CheckboxCard(props: CheckboxCardProps) {
/>
<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 &&
<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>}
</div>
</div>

View File

@ -1,7 +1,7 @@
import {RecordModel} from "pocketbase";
import {UseMutationResult} from "@tanstack/react-query";
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
@ -57,6 +57,11 @@ export default function RecordSearchInput<T extends RecordModel>(props: {
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 || [])
.map((recordView) => (
<Combobox.Option value={recordView.id} key={recordView.id}

View File

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

View File

@ -1,7 +1,6 @@
import {createContext, DependencyList, ReactNode, useCallback, useContext, useEffect, useMemo, useState} from "react"
import PocketBase, {ClientResponseError, LocalAuthStore, RecordAuthResponse, RecordSubscription} from 'pocketbase'
import ms from "ms";
import {useInterval} from "@mantine/hooks";
import {useQuery} from "@tanstack/react-query";
import {TypedPocketBase} from "@/models";
import {PB_BASE_URL, PB_STORAGE_KEY, PB_USER_COLLECTION} from "../../config.ts";
@ -73,20 +72,29 @@ const PocketData = () => {
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)
pb.authStore.onChange((_, 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) => {
await pb.send<RecordAuthResponse>("/api/ldap/login", {
method: "POST",
@ -126,9 +134,7 @@ const PocketData = () => {
pb.collection(idOrName).unsubscribe(topic)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, deps ? deps : []);
useInterval(refreshUser, oneMinuteInMs)
}, deps ? deps : [])
return {
ldapLogin,
@ -136,7 +142,7 @@ const PocketData = () => {
logout,
user: user as UserModal | null,
pb,
refreshUser,
refreshUser: refreshUserQuery.refetch,
useSubscription,
apiIsHealthy: apiIsHealthyQuery.data ?? false
}

View File

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

View File

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

View File

@ -90,7 +90,7 @@ export const EventSettingsMenu = ({event, target}: { event: EventModel, target:
<Menu.Dropdown>
{
nav.map(({label, children,}, index, array) => <div key={index}>
nav.map(({label, children,}, index) => <div key={index}>
<Menu.Label>{label}</Menu.Label>
{children.map(({title, icon, 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>
))}
{index !== array.length - 1 && <Menu.Divider/>}
</div>)
}
</Menu.Dropdown>

View File

@ -1,5 +1,5 @@
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 {
IconCheckupList,
IconClockCog,
@ -13,13 +13,15 @@ import {Link, Navigate, NavLink, Route, Routes, useParams} from "react-router-do
import InnerHtml from "@/components/InnerHtml";
import {EventModel} from "@/models/EventTypes.ts";
import {PocketBaseErrorAlert, usePB} from "@/lib/pocketbase.tsx";
import SlotsTable from "./EventListSlotsTable";
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 ListSlots from "./ListSlots";
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 }) {
@ -57,6 +59,14 @@ export default function EventListRouter({event}: { event: EventModel }) {
</Link>
</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"}>
<TextWithIcon icon={list.open ? <IconLockOpen size={16}/> : <IconLock size={16}/>}>
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/>,
to: `/events/e/${event.id}/lists/overview/${list.id}/entries`,
to: `/events/e/${event.id}/lists/overview/${list.id}/slots`,
title: "Zeitslots"
},
{
@ -123,11 +133,11 @@ export default function EventListRouter({event}: { event: EventModel }) {
</Group>
<Routes>
<Route index element={<Navigate to={"entries"} replace/>}/>
<Route path={"entries"} element={<SlotsTable event={event} list={list}/>}/>
<Route path={"settings"} element={<EventListSettings list={list} event={event}/>}/>
<Route path={"questions"} element={<EventListEntryQuestionSettings list={list} event={event}/>}/>
<Route path={"status"} element={<EventListEntryStatusSettings list={list} event={event}/>}/>
<Route index element={<Navigate to={"slots"} replace/>}/>
<Route path={"slots"} element={<ListSlots list={list}/>}/>
<Route path={"settings"} element={<ListSettings list={list} event={event}/>}/>
<Route path={"questions"} element={<ListEntryQuestionSettings list={list} event={event}/>}/>
<Route path={"status"} element={<ListEntryStatusSettings list={list} event={event}/>}/>
</Routes>
</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 FormFieldsPreview from "@/components/formUtil/formBuilder/FormFieldsPreview.tsx";
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 formValues = useForm({
initialValues: {
ignoreDefaultEntryQuestionScheme: list.ignoreDefaultEntryQuestionScheme || false,
}
})
const editMutation = useMutation({
mutationFn: async (schema: FormSchema) => {
return await pb.collection("eventLists").update(list.id, {
entryQuestionSchema: schema
entryQuestionSchema: schema,
...formValues.values
})
},
onSuccess: () => {
showSuccessNotification("Fragen gespeichert")
showSuccessNotification("Fragen und Liste gespeichert")
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}/>
{event.defaultEntryQuestionSchema && (
{(!list.ignoreDefaultEntryQuestionScheme && event.defaultEntryQuestionSchema) && (
<Alert color={"blue"} title={"Standard Fragen"} className={"stack"}>
<Text c={"dimmed"} mb={"sm"} size={"xs"}>
Folgende Fragen sind standardmäßig für dieses Event vorgesehen
@ -39,6 +48,13 @@ export default function EventListEntryQuestionSettings({list, event}: { list: Ev
</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/>}>
Wenn du Felder entfernst, werden alle Daten,
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: []}}
onSubmit={(schema) => editMutation.mutate(schema)}
withPreview
additionalSchemaToPreview={event.defaultEntryQuestionSchema || {fields: []}}
additionalSchemaToPreview={!list.ignoreDefaultEntryQuestionScheme && event.defaultEntryQuestionSchema || {fields: []}}
/>
</div>
}

View File

@ -8,18 +8,27 @@ import {FormSchema} from "@/components/formUtil/formBuilder/types.ts";
import FormBuilder from "@/components/formUtil/formBuilder";
import FormFieldsPreview from "@/components/formUtil/formBuilder/FormFieldsPreview.tsx";
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 editMutation = useMutation({
const formValues = useForm({
initialValues: {
ignoreDefaultEntryStatusSchema: list.ignoreDefaultEntryStatusSchema || false,
}
})
const editStatusMutation = useMutation({
mutationFn: async (schema: FormSchema) => {
return await pb.collection("eventLists").update(list.id, {
entryStatusSchema: schema
await pb.collection("eventLists").update(list.id, {
entryStatusSchema: schema,
...formValues.values
})
},
onSuccess: () => {
showSuccessNotification("Status Felder gespeichert")
showSuccessNotification("Status Felder und Liste gespeichert")
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"}>
<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"}>
<Text c={"dimmed"} mb={"sm"} size={"xs"}>
Folgende Status-Felder sind standardmäßig für dieses Event vorgesehen
@ -39,6 +48,13 @@ export default function EventListEntryStatusSettings({list, event}: { list: Even
</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/>}>
Wenn du Felder entfernst, werden alle Daten,
die für diese Felder gespeichert wurden nicht mehr angezeigt.
@ -46,9 +62,9 @@ export default function EventListEntryStatusSettings({list, event}: { list: Even
<FormBuilder
defaultValue={list.entryStatusSchema || {fields: []}}
onSubmit={(schema) => editMutation.mutate(schema)}
onSubmit={(schema) => editStatusMutation.mutate(schema)}
withPreview
additionalSchemaToPreview={event.defaultEntryStatusSchema || {fields: []}}
additionalSchemaToPreview={!list.ignoreDefaultEntryStatusSchema && event.defaultEntryStatusSchema || {fields: []}}
/>
</div>
}

View File

@ -11,7 +11,7 @@ import {useNavigate} from "react-router-dom";
import {useConfirmModal} from "@/components/ConfirmModal.tsx";
import {CheckboxCard} from "@/components/input/CheckboxCard";
export default function EventListSettings({list, event}: { list: EventListModel, event: EventModel }) {
export default function ListSettings({list, event}: { list: EventListModel, event: EventModel }) {
const {pb} = usePB()
const navigate = useNavigate()
@ -22,7 +22,7 @@ export default function EventListSettings({list, event}: { list: EventListModel,
allowOverlappingEntries: list.allowOverlappingEntries,
onlyStuVeAccounts: list.onlyStuVeAccounts,
favorite: list.favorite,
description: list.description || ""
description: list.description || "",
}
})
@ -99,7 +99,7 @@ export default function EventListSettings({list, event}: { list: EventListModel,
<CheckboxCard
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. " +
"Bei dem Verschieden von Anmeldungen wirds diese Regel nicht beachtet!"}
{...formValues.getInputProps("allowOverlappingEntries", {type: "checkbox"})}

View File

@ -8,19 +8,18 @@
}
.slotRow {
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
gap: var(--gap);
width: 100%;
& > :nth-child(2) {
& > :nth-child(1) {
width: 50%;
font-size: var(--mantine-font-size-sm);
}
& > :nth-child(3) {
& > :nth-child(2) {
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 classes from "./EventListSlotRow.module.css";
import {ActionIcon, Alert, Code, Collapse, Group, Modal, ThemeIcon} from "@mantine/core";
import {IconAdjustments, IconAdjustmentsOff, IconInfoCircle, IconUsersMinus, IconUsersPlus} from "@tabler/icons-react";
import {RenderDateRange} from "../../components/RenderDateRange.tsx";
import classes from "./ListSlotRow.module.css";
import {ActionIcon, Alert, Code, Group, Modal} from "@mantine/core";
import {IconAdjustments, IconAdjustmentsOff, IconInfoCircle} from "@tabler/icons-react";
import {RenderDateRange} from "@/pages/events/e/:eventId/EventLists/EventListComponents/RenderDateRange.tsx";
import {modals} from "@mantine/modals";
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 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,
list: EventListModel,
event: EventModel,
refetch: () => void
}) => {
const [showEditModal, showEditModalHandler] = useDisclosure(false)
const [expanded, expandedHandler] = useDisclosure(false)
return (
<div className={`${classes.slotContainer}`}>
<Modal
@ -29,22 +26,15 @@ export const EventListSlotRow = ({slot, list, event, refetch}: {
title={`Zeitslot bearbeiten`}
size={"lg"}
>
<UpsertEventListSlot slot={slot} list={list} onSuccess={() => {
<UpsertSlot slot={slot} list={list} onSuccess={() => {
showEditModalHandler.close()
refetch()
}} onAbort={showEditModalHandler.close}/>
</Modal>
<div
className={classes.slotRow}
onClick={expandedHandler.toggle}
>
<ThemeIcon variant={"transparent"}>
{
expanded ? <IconUsersMinus size={16}/> : <IconUsersPlus size={16}/>
}
</ThemeIcon>
<div className={classes.slotRow}>
<RenderDateRange start={new Date(slot.startDate)} end={new Date(slot.endDate)}/>
<EventListSlotProgress slot={slot}/>
<SlotProgress slot={slot}/>
<Group gap={4} justify="right" wrap="nowrap">
<ActionIcon
size="sm"
@ -67,9 +57,11 @@ export const EventListSlotRow = ({slot, list, event, refetch}: {
title: 'Beschreibung',
children: <div className={"stack"}>
<ShowDebug>
Listen-ID <Code>{list.id}</Code>
Slot Id - <Code>{slot.id}</Code>
<br/>
Slot-ID <Code>{slot.id}</Code>
created - <Code>{pprintDateTime(slot.created)}</Code>
<br/>
updated - <Code>{pprintDateTime(slot.updated)}</Code>
</ShowDebug>
{
slot.description ? <InnerHtml html={slot.description}/> :
@ -85,10 +77,6 @@ export const EventListSlotRow = ({slot, list, event, refetch}: {
</ActionIcon>
</Group>
</div>
<Collapse in={expanded}>
<EventListSlotEntriesTable visible={expanded} slot={slot} list={list} refetch={refetch} event={event}/>
</Collapse>
</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 {useQuery} from "@tanstack/react-query";
import {Box, Button, Center, Pagination, Title} from "@mantine/core";
@ -6,8 +6,8 @@ import {IconClockPlus} from "@tabler/icons-react";
import {useState} from "react";
import {useDisclosure} from "@mantine/hooks";
import classes from './index.module.css'
import {EventListSlotRow} from "./EventListSlotRow.tsx";
import UpsertEventListSlot from "@/pages/events/e/:eventId/EventLists/components/UpsertEventListSlot.tsx";
import {ListSlotRow} from "./ListSlotRow.tsx";
import UpsertSlot from "@/pages/events/e/:eventId/EventLists/EventListComponents/UpsertSlot.tsx";
/**
* 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
* @constructor
*/
export default function SlotsTable({list, event}: { event: EventModel, list: EventListModel }) {
export default function ListSlots({list}: { list: EventListModel }) {
const {pb} = usePB()
@ -39,8 +39,8 @@ export default function SlotsTable({list, event}: { event: EventModel, list: Eve
<div className={classes.table}>
{query.data?.items.map(slot => (
<EventListSlotRow
key={slot.id} event={event} list={list} slot={slot}
<ListSlotRow
key={slot.id} list={list} slot={slot}
refetch={query.refetch}
/>
))}
@ -53,7 +53,7 @@ export default function SlotsTable({list, event}: { event: EventModel, list: Eve
Neuen Slot hinzufügen
</Title>
<UpsertEventListSlot list={list} onSuccess={() => {
<UpsertSlot list={list} onSuccess={() => {
showNewSlotFormHandler.close()
query.refetch()
}} onAbort={showNewSlotFormHandler.close}/>

View File

@ -4,19 +4,18 @@ import {showSuccessNotification} from "@/components/util.tsx";
import {useConfirmModal} from "@/components/ConfirmModal.tsx";
import {usePB} from "@/lib/pocketbase.tsx";
import {useDisclosure} from "@mantine/hooks";
import {
UpdateEventListSlotEntryStatusModal
} 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 {ActionIcon, HoverCard, Menu} from "@mantine/core";
import {IconArrowsMove, IconCheckupList, IconSend, IconSettings, IconTrash} from "@tabler/icons-react";
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}: {
refetch: () => void,
entry: EventListSlotEntriesWithUserModel
@ -28,8 +27,6 @@ export default function EditSlotEntryMenu({entry, refetch}: {
const [showMoveEntryModal, showMoveEntryModalHandler] = useDisclosure(false)
const [showEditFormModal, showEditFormModalHandler] = useDisclosure(false)
const deleteEntryMutation = useMutation({
mutationFn: async () => {
await pb.collection("eventListSlotEntries").delete(entry.id)
@ -46,62 +43,67 @@ export default function EditSlotEntryMenu({entry, refetch}: {
onConfirm: () => deleteEntryMutation.mutate()
})
const {statusSchema} = getListSchemas(entry)
const noStatusField = statusSchema.fields.length === 0
return <>
<ConfirmModal/>
<UpdateEventListSlotEntryStatusModal
<UpdateEntryStatusModal
opened={showStatusEditModal}
close={showStatusEditModalHandler.close}
entry={entry}
refetch={refetch}
/>
<MoveEventListSlotEntryModal
<MoveEntryModal
opened={showMoveEntryModal}
close={showMoveEntryModalHandler.close}
entry={entry}
refetch={refetch}
/>
<UpdateEventListSlotEntryFormModal
opened={showEditFormModal}
close={showEditFormModalHandler.close}
refetch={refetch}
entry={entry}
/>
<Button
leftSection={<IconCheckupList size={16}/>}
variant={"light"}
size={"compact-xs"}
onClick={showStatusEditModalHandler.toggle}
>
Status bearbeiten
</Button>
<HoverCard width={'350px'} shadow="md" position={"top"} closeDelay={0} disabled={noStatusField}>
<HoverCard.Target>
<ActionIcon
variant="light"
color="blue"
onClick={showStatusEditModalHandler.toggle}
aria-label={"edit entry status"}
disabled={noStatusField}
>
<IconCheckupList size={16}/>
</ActionIcon>
</HoverCard.Target>
<HoverCard.Dropdown p={0}>
<EntryStatusSpoiler entry={entry}/>
</HoverCard.Dropdown>
</HoverCard>
<Menu>
<Menu.Target>
<ActionIcon
size="sm"
variant="light"
color="blue"
aria-label={"edit entry"}
>
<IconSettings size={16}/>
</ActionIcon>
</Menu.Target>
<Menu.Dropdown>
<Menu.Item
leftSection={<IconSend size={16}/>}
disabled
>
Person benachrichtigen
</Menu.Item>
<Menu.Item
leftSection={<IconArrowsMove size={16}/>}
onClick={showMoveEntryModalHandler.toggle}
>
Eintrag verschieben
</Menu.Item>
<Menu.Item
leftSection={<IconForms size={16}/>}
onClick={showEditFormModalHandler.toggle}
>
Formular
</Menu.Item>
<Menu.Item
color={"red"} leftSection={<IconTrash size={16}/>}
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 {
padding: var(--gap) 0;
}
@ -43,4 +21,7 @@
color: var(--mantine-color-dimmed);
border-bottom: var(--border);
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 {useMutation, useQuery} from "@tanstack/react-query";
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 {pprintDateTime} from "@/lib/datetime.ts";
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,
close: () => void,
refetch: () => void,
@ -20,6 +23,8 @@ export const MoveEventListSlotEntryModal = ({opened, close, refetch, entry}: {
const [selectedList, setSelectedList] = useState<string | null>(entry.eventList)
const [showFullSlots, setShowFullSlots] = useState(false)
useEffect(() => {
setSelectedSlot(null)
}, [selectedList]);
@ -52,17 +57,34 @@ export const MoveEventListSlotEntryModal = ({opened, close, refetch, entry}: {
})
const slotsQuery = useQuery({
queryKey: ["eventListSlots", entry.eventList, "notFull", selectedList],
queryKey: ["eventListSlots", entry.eventList, "notFull", selectedList, showFullSlots],
queryFn: async () => (
await pb.collection("eventListSlotsWithEntriesCount").getFullList({
filter: `eventList='${selectedList}'
&&(maxEntries=0 || entriesCount < maxEntries)`,
${showFullSlots ? "" : "&&(maxEntries=0 || entriesCount < maxEntries)"} `,
sort: "startDate"
})
),
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
size={"lg"}
opened={opened}
@ -103,10 +125,18 @@ export const MoveEventListSlotEntryModal = ({opened, close, refetch, entry}: {
value: slot.id,
label: `${pprintDateTime(slot.startDate)} - ${pprintDateTime(slot.endDate)}`
})) ?? []}
renderOption={renderSelectOption}
value={selectedSlot}
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 && (
<Alert color={"orange"} title={"Achtung!"}>

View File

@ -1,17 +1,41 @@
import {EventListSlotsWithEntriesCountModel} from "@/models/EventTypes.ts";
import {Badge, Group, Progress, Text, ThemeIcon, Tooltip} from "@mantine/core";
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.
* If the slot is unlimited or full, the message will reflect that.
* @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 (slot.maxEntries === 0 || slot.maxEntries === null) {
if (slotIsUnlimited) {
return (
<Group align={"center"} justify={"center"}>
<Tooltip
@ -32,13 +56,12 @@ export default function EventListSlotProgress({slot}: { slot: EventListSlotsWith
<ThemeIcon size={"sm"} variant={"transparent"} color={"orange"}>
<IconX/>
</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>
)
}
const freeSlots = slot.maxEntries - slot.entriesCount
const occupiedSlots = slot.entriesCount
// if the slot is not full and has a max entry count
return <>

View File

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

View File

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

View File

@ -9,7 +9,7 @@ import TextEditor from "@/components/input/Editor";
import ShowHelp from "@/components/ShowHelp.tsx";
import {formatDuration} from "@/lib/datetime.ts";
export default function UpsertEventListSlot({list, slot, onSuccess, onAbort}: {
export default function UpsertSlot({list, slot, onSuccess, onAbort}: {
list: EventListModel,
slot?: EventListSlotModel,
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 {ReactNode} from "react";
import {Menu} from "@mantine/core";
import EventListSearch from "./EventListSearch/index.tsx";
import EventListShare from "@/pages/events/e/:eventId/EventLists/EventListShare.tsx";
import EditEventDefaultEntryStatusSchema
from "@/pages/events/e/:eventId/EventLists/EventListSettings/EditEventDefaultEntryStatusSchema.tsx";
import EditEventDefaultEntryQuestionSchema
from "@/pages/events/e/:eventId/EventLists/EventListSettings/EditEventDefaultEntryQuestionSchema.tsx";
import EventListSearch from "@/pages/events/e/:eventId/EventLists/Search/index.tsx";
import ShareEventLists from "@/pages/events/e/:eventId/EventLists/ShareEventLists.tsx";
import EditDefaultEntryStatusSchema
from "@/pages/events/e/:eventId/EventLists/GeneralListSettings/EditDefaultEntryStatusSchema.tsx";
import EditDefaultEntryQuestionSchema
from "@/pages/events/e/:eventId/EventLists/GeneralListSettings/EditDefaultEntryQuestionSchema.tsx";
const nav = [
@ -18,17 +18,17 @@ const nav = [
label: "Listenaktionen",
children: [
{
title: "Übersicht",
title: "Listen und Slots",
icon: <IconList size={16}/>,
to: "overview"
},
{
title: "Durchsuchen",
title: "Anmeldungen",
icon: <IconUserSearch size={16}/>,
to: "search"
},
{
title: "Teilen",
title: "Listen Teilen",
icon: <IconLink size={16}/>,
to: "share"
}
@ -69,7 +69,7 @@ export const EventListsMenu = ({event, target}: { event: EventModel, target: Rea
<Menu.Dropdown>
{
nav.map(({label, children,}, index, array) => <div key={index}>
nav.map(({label, children,}, index) => <div key={index}>
<Menu.Label>{label}</Menu.Label>
{children.map(({title, icon, 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>
))}
{index !== array.length - 1 && <Menu.Divider/>}
</div>)
}
</Menu.Dropdown>
@ -95,10 +94,10 @@ export default function EventListsRouter({event}: { event: EventModel }) {
<Route index element={<Navigate to={"overview"} replace/>}/>
<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/questions"} element={<EditEventDefaultEntryQuestionSchema event={event}/>}/>
<Route path={"e/status"} element={<EditDefaultEntryStatusSchema event={event}/>}/>
<Route path={"e/questions"} element={<EditDefaultEntryQuestionSchema event={event}/>}/>
<Route path={"overview"} element={<EventListsOverview 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.
* @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()

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.
* @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()

View File

@ -19,7 +19,6 @@ export default function ListSelect(props: GenericRecordSearchInputProps<EventLis
recordSearchMutation={
useMutation({
mutationFn: async (search: string) => {
const filter: string[] = []
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";
export default function EventListShare({event}: { event: EventModel }) {
export default function ShareEventLists({event}: { event: EventModel }) {
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 {IconChevronDown, IconChevronRight, IconConfetti, IconForms, IconList, IconTrash} 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 classes from "./UserEntryRow.module.css"
import {humanDeltaFromNow} from "@/lib/datetime.ts";
import {useMutation} from "@tanstack/react-query";
import {showSuccessNotification} from "@/components/util.tsx";
import {useConfirmModal} from "@/components/ConfirmModal.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 {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}: {
entry: EventListSlotEntriesWithUserModel,
@ -51,7 +52,7 @@ export default function UserEntryRow({entry, refetch}: {
<ConfirmModal/>
<UpdateEventListSlotEntryFormModal
<UpdateEntryFormModal
opened={showEditFormModal}
close={showEditFormModalHandler.close}
refetch={refetch}
@ -123,7 +124,7 @@ export default function UserEntryRow({entry, refetch}: {
</div>
<Collapse in={expanded}>
<EventListSlotEntryDetails entry={entry}/>
<EntryQuestionAndStatusData entry={entry}/>
</Collapse>
</div>
}

View File

@ -1,7 +1,5 @@
import {EventListModel, EventListSlotsWithEntriesCountModel} from "@/models/EventTypes.ts";
import classes from "./EventListSlotView.module.css";
import {RenderDateRange} from "@/pages/events/e/:eventId/EventLists/components/RenderDateRange.tsx";
import EventListSlotProgress from "@/pages/events/e/:eventId/EventLists/components/EventListSlotProgress.tsx";
import {useDisclosure} from "@mantine/hooks";
import {Alert, Collapse, ThemeIcon, Tooltip, UnstyledButton} from "@mantine/core";
import {IconChevronDown, IconChevronRight, IconInfoCircle} from "@tabler/icons-react";
@ -12,25 +10,22 @@ import {showSuccessNotification} from "@/components/util.tsx";
import InnerHtml from "@/components/InnerHtml";
import FormInput from "@/components/formUtil/FromInput";
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}: {
list: EventListModel,
slot: EventListSlotsWithEntriesCountModel,
refetch: () => void
}) {
const [expanded, expandedHandler] = useDisclosure(false)
const slotIsFull = slot.maxEntries === 0 || slot.maxEntries === null ? false : slot.entriesCount >= slot.maxEntries
const slotIsInPast = new Date(slot.endDate) < new Date()
const schema = {
fields: [
...slot.entryQuestionSchema?.fields ?? [],
...slot.defaultEntryQuestionSchema?.fields ?? [],
]
}
const {questionSchema} = getListSchemas(slot)
const {pb, user} = usePB()
@ -62,9 +57,9 @@ export default function EventListSlotView({slot, list, refetch}: {
<div className={classes.slotInfo}>
<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>
</UnstyledButton>
@ -97,7 +92,7 @@ export default function EventListSlotView({slot, list, refetch}: {
:
<FormInput
disabled={!user || slotIsFull}
schema={schema}
schema={questionSchema}
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 {useSettings} from "@/lib/settings.ts";
import {LdapGroupModel} from "@/models/AuthTypes.ts";
@ -20,4 +24,28 @@ export const useEventRights = (event?: EventModel) => {
canEditEvent: isEventAdmin || isStex,
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 {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 {useQuery} from "@tanstack/react-query";
import {PocketBaseErrorAlert, usePB} from "@/lib/pocketbase.tsx";
import {CodeHighlight} from "@mantine/code-highlight";
import {IconRefresh} from "@tabler/icons-react";
import {RecordListOptions} from "pocketbase";
import {useState} from "react";
export default function DebugPage() {
@ -13,6 +15,8 @@ export default function DebugPage() {
const {pb} = usePB()
const [page, setPage] = useState(1)
const formValues = useForm({
initialValues: {
collectionName: "",
@ -25,11 +29,16 @@ export default function DebugPage() {
const debugQuery = useQuery({
queryKey: ["debug", formValues.values.collectionName, formValues.values.filter, formValues.values.sort, formValues.values.expand],
queryFn: async () => {
return await pb.collection(formValues.values.collectionName).getList(1, 10, {
filter: formValues.values.filter,
sort: formValues.values.sort,
expand: formValues.values.expand
})
const options: RecordListOptions = {}
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 !== ""
})
@ -79,9 +88,7 @@ export default function DebugPage() {
{debugQuery.data.totalItems} Ergebniss(e)
</Text>
<Text c={"dimmed"}>
{debugQuery.data.page}/{debugQuery.data.totalPages} Seiten
</Text>
<Pagination size={"xs"} value={page} onChange={setPage} total={debugQuery.data.totalPages ?? 1}/>
<ActionIcon
onClick={() => debugQuery.refetch()}
@ -96,6 +103,7 @@ export default function DebugPage() {
</Group>
<CodeHighlight code={JSON.stringify(debugQuery.data.items, null, 2)} language="json"/>
</div>}
</div>
</>

View File

@ -14,7 +14,7 @@
}
.tabler-icon {
stroke-width: 1.5;
stroke-width: 1.2;
}
.scrollable {
@ -145,6 +145,14 @@ a:hover, a:active, a:visited {
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 {
font-family: var(--mantine-font-family-monospace);
}

View File

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