feat(entries filter): added filter for entries
Build and Push Docker image / build-and-push (push) Successful in 4m29s
Details
Build and Push Docker image / build-and-push (push) Successful in 4m29s
Details
This commit is contained in:
parent
4f53566b61
commit
d1569bce55
|
@ -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"
|
16
package.json
16
package.json
|
@ -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",
|
||||
|
|
|
@ -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})
|
||||
}
|
||||
}}
|
||||
/>
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
.filterButton {
|
||||
max-width: 250px;
|
||||
}
|
||||
|
||||
.filterButtonLabel {
|
||||
display: inline-block;
|
||||
max-width: calc(100%);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
|
@ -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)}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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>}
|
||||
|
|
|
@ -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>
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
|
||||
|
|
|
@ -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/>
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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"}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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">
|
|
@ -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}/>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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>
|
||||
}
|
|
@ -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>
|
||||
}
|
|
@ -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>
|
||||
}
|
|
@ -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>
|
||||
}
|
|
@ -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"})}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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}/>
|
|
@ -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}
|
|
@ -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;
|
||||
}
|
|
@ -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>
|
||||
</>
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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>
|
||||
}
|
|
@ -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!"}>
|
|
@ -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 <>
|
|
@ -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}
|
|
@ -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}
|
|
@ -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,
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
}
|
|
@ -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>
|
||||
}
|
|
@ -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}/>}/>
|
||||
|
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -19,7 +19,6 @@ export default function ListSelect(props: GenericRecordSearchInputProps<EventLis
|
|||
recordSearchMutation={
|
||||
useMutation({
|
||||
mutationFn: async (search: string) => {
|
||||
|
||||
const filter: string[] = []
|
||||
|
||||
if (search) {
|
||||
|
|
|
@ -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 */
|
||||
}
|
||||
|
|
@ -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>
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
.resultsContainer {
|
||||
display: grid;
|
||||
gap: var(--gap);
|
||||
}
|
|
@ -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>
|
||||
}
|
|
@ -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[]>([])
|
||||
|
|
@ -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>
|
||||
}
|
|
@ -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}
|
||||
/>
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
|
@ -21,3 +25,27 @@ export const useEventRights = (event?: EventModel) => {
|
|||
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 ?? [] : []),
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
</>
|
||||
|
|
|
@ -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);
|
||||
}
|
85
yarn.lock
85
yarn.lock
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue