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
|
// POCKETBASE
|
||||||
export const PB_USER_COLLECTION = "ldap_users"
|
export const PB_USER_COLLECTION = "users"
|
||||||
export const PB_BASE_URL = "https://backend.stuve-it.de"
|
export const PB_BASE_URL = "https://backend.stuve-it.de"
|
||||||
export const PB_STORAGE_KEY = "stuve-it-login-record"
|
export const PB_STORAGE_KEY = "stuve-it-login-record"
|
||||||
|
|
||||||
// general
|
// general
|
||||||
export const APP_NAME = "StuVe IT"
|
export const APP_NAME = "StuVe IT"
|
||||||
export const APP_VERSION = "0.7.0 (beta)"
|
export const APP_VERSION = "0.8.0 (beta)"
|
||||||
export const APP_URL = "https://it.stuve.uni-ulm.de"
|
export const APP_URL = "https://it.stuve.uni-ulm.de"
|
16
package.json
16
package.json
|
@ -13,14 +13,14 @@
|
||||||
"@fontsource/fira-code": "^5.0.15",
|
"@fontsource/fira-code": "^5.0.15",
|
||||||
"@fontsource/overpass": "^5.0.15",
|
"@fontsource/overpass": "^5.0.15",
|
||||||
"@hello-pangea/dnd": "^16.6.0",
|
"@hello-pangea/dnd": "^16.6.0",
|
||||||
"@mantine/code-highlight": "^7.8.0",
|
"@mantine/code-highlight": "^7.10.0",
|
||||||
"@mantine/core": "^7.8.0",
|
"@mantine/core": "^7.10.0",
|
||||||
"@mantine/dates": "^7.8.0",
|
"@mantine/dates": "^7.10.0",
|
||||||
"@mantine/form": "^7.8.0",
|
"@mantine/form": "^7.10.0",
|
||||||
"@mantine/hooks": "^7.8.0",
|
"@mantine/hooks": "^7.10.0",
|
||||||
"@mantine/modals": "^7.9.0",
|
"@mantine/modals": "^7.10.0",
|
||||||
"@mantine/notifications": "^7.8.1",
|
"@mantine/notifications": "^7.10.0",
|
||||||
"@mantine/tiptap": "^7.8.0",
|
"@mantine/tiptap": "^7.10.0",
|
||||||
"@tabler/icons-react": "^3.2.0",
|
"@tabler/icons-react": "^3.2.0",
|
||||||
"@tanstack/react-query": "^5.0.5",
|
"@tanstack/react-query": "^5.0.5",
|
||||||
"@tanstack/react-query-devtools": "^5.31.0",
|
"@tanstack/react-query-devtools": "^5.31.0",
|
||||||
|
|
|
@ -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
|
children: ReactNode
|
||||||
}) => {
|
}) => {
|
||||||
return <Stack gap={5}>
|
return <Stack gap={5}>
|
||||||
<Input.Label required>{field.label}</Input.Label>
|
<Input.Label required component={"div"}>{field.label}</Input.Label>
|
||||||
|
|
||||||
{field.description && <>
|
{field.description && <>
|
||||||
<Input.Description>
|
<Input.Description component={"div"}>
|
||||||
<Spoiler
|
<Spoiler
|
||||||
maxHeight={40}
|
maxHeight={40}
|
||||||
showLabel={<Text span size={"xs"}>Mehr anzeigen</Text>}
|
showLabel={<Text span size={"xs"}>Mehr anzeigen</Text>}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import {FormSchema} from "../formBuilder/types.ts";
|
import {FormSchema} from "../formBuilder/types.ts";
|
||||||
import {Button, Group,} from "@mantine/core";
|
import {ActionIcon, Button, Code, CopyButton, Group, Table, Text, Tooltip} from "@mantine/core";
|
||||||
import {useForm} from "@mantine/form";
|
import {useForm} from "@mantine/form";
|
||||||
|
|
||||||
import ShowDebug from "../../ShowDebug.tsx";
|
import ShowDebug from "../../ShowDebug.tsx";
|
||||||
|
@ -7,12 +7,16 @@ import {FieldEntries} from "@/components/formUtil/FromInput/types.ts";
|
||||||
import {createValidationFromSchema} from "@/components/formUtil/FromInput/validation.ts";
|
import {createValidationFromSchema} from "@/components/formUtil/FromInput/validation.ts";
|
||||||
import {
|
import {
|
||||||
CheckboxField,
|
CheckboxField,
|
||||||
DateField, DateRangeField,
|
DateField,
|
||||||
|
DateRangeField,
|
||||||
EmailField,
|
EmailField,
|
||||||
FormTextareaField,
|
FormTextareaField,
|
||||||
FormTextField,
|
FormTextField,
|
||||||
NumberField, SelectField
|
NumberField,
|
||||||
|
SelectField
|
||||||
} from "@/components/formUtil/FromInput/formFieldComponents.tsx";
|
} from "@/components/formUtil/FromInput/formFieldComponents.tsx";
|
||||||
|
import {transformData} from "@/components/formUtil/formTable";
|
||||||
|
import {IconCheck, IconHash} from "@tabler/icons-react";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This function creates default values based on a data type (e.g. "" for text, false for checkbox)
|
* This function creates default values based on a data type (e.g. "" for text, false for checkbox)
|
||||||
|
@ -24,7 +28,7 @@ import {
|
||||||
* @param initialEntries The already existing entries
|
* @param initialEntries The already existing entries
|
||||||
*/
|
*/
|
||||||
const createDefaultValuesFromSchema = (schema: FormSchema, initialEntries?: FieldEntries): FieldEntries => {
|
const createDefaultValuesFromSchema = (schema: FormSchema, initialEntries?: FieldEntries): FieldEntries => {
|
||||||
const entries: FieldEntries = initialEntries ?? {}
|
const entries: FieldEntries = transformData(initialEntries ?? {})
|
||||||
|
|
||||||
schema.fields.forEach(field => {
|
schema.fields.forEach(field => {
|
||||||
// if the field already exists, skip it
|
// if the field already exists, skip it
|
||||||
|
@ -166,9 +170,25 @@ export default function FormInput({schema, onAbort, onSubmit, disabled, initialD
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<ShowDebug>
|
<ShowDebug>
|
||||||
<pre>
|
<Table w={"500px"} data={{
|
||||||
{JSON.stringify(formValues, null, 2)}
|
body: schema.fields.map(field => ([
|
||||||
</pre>
|
<CopyButton value={field.id}>
|
||||||
|
{({copied, copy}) => (
|
||||||
|
<Tooltip label={`ID '${field.id}' kopieren`}>
|
||||||
|
<ActionIcon variant={"transparent"} onClick={copy}>
|
||||||
|
{copied ? <IconCheck/> : <IconHash/>}
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</CopyButton>,
|
||||||
|
<Text c="dimmed" w={"100px"} truncate={"end"}>
|
||||||
|
{field.label}
|
||||||
|
</Text>,
|
||||||
|
<Code display={"block"} className={"wrapWords"}
|
||||||
|
maw={"300px"}>{JSON.stringify(formValues.values[field.id]?.value)}</Code>
|
||||||
|
])),
|
||||||
|
head: ["Field", "Value"]
|
||||||
|
}}/>
|
||||||
</ShowDebug>
|
</ShowDebug>
|
||||||
</form>
|
</form>
|
||||||
}
|
}
|
|
@ -61,8 +61,8 @@ export type EmailFieldEntry = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type NumberFieldEntryFilter = {
|
export type NumberFieldEntryFilter = {
|
||||||
min: number | null
|
min: number | string | null
|
||||||
max: number | null
|
max: number | string | null
|
||||||
dataType: "number"
|
dataType: "number"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -35,7 +35,7 @@ const DateTimeCell = ({date}: { date: Date | string }) => {
|
||||||
|
|
||||||
const DateRangeCell = ({dateRange}: { dateRange: [Date | null, Date | null] }) => {
|
const DateRangeCell = ({dateRange}: { dateRange: [Date | null, Date | null] }) => {
|
||||||
|
|
||||||
const [start, end] = dateRange
|
const [start, end] = dateRange.map(date => date === null ? null : new Date(date))
|
||||||
|
|
||||||
if (start === null || end === null) {
|
if (start === null || end === null) {
|
||||||
return <EmptyCell/>
|
return <EmptyCell/>
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import {FieldDataType, FormSchemaField} from "./formBuilder/types.ts";
|
import {FieldDataType, FormSchemaField} from "./formBuilder/types.ts";
|
||||||
import {nanoid} from "nanoid";
|
import {customAlphabet} from "nanoid";
|
||||||
|
|
||||||
|
const nanoid = customAlphabet('1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRTSUVWXYZ', 6)
|
||||||
|
|
||||||
export const humanReadableField = (fieldType: FieldDataType): string => {
|
export const humanReadableField = (fieldType: FieldDataType): string => {
|
||||||
switch (fieldType) {
|
switch (fieldType) {
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
background-color: light-dark(var(--mantine-color-white), var(--mantine-color-dark-8));
|
background-color: light-dark(var(--mantine-color-white), var(--mantine-color-dark-8));
|
||||||
|
|
||||||
|
|
||||||
&[aria-error="true"] {
|
&[data-error="true"] {
|
||||||
border-color: var(--mantine-color-error);
|
border-color: var(--mantine-color-error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,11 +7,12 @@ export type CheckboxCardProps = InputWrapperProps & CheckboxProps
|
||||||
export function CheckboxCard(props: CheckboxCardProps) {
|
export function CheckboxCard(props: CheckboxCardProps) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classes.container} aria-error={!!props.error}>
|
<div className={classes.container} data-error={!!props.error}>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={props.checked}
|
checked={props.checked}
|
||||||
onChange={props.onChange}
|
onChange={props.onChange}
|
||||||
required={props.required}
|
required={props.required}
|
||||||
|
indeterminate={props.indeterminate}
|
||||||
size="md"
|
size="md"
|
||||||
mr="xl"
|
mr="xl"
|
||||||
styles={{input: {cursor: 'pointer'}}}
|
styles={{input: {cursor: 'pointer'}}}
|
||||||
|
@ -19,9 +20,11 @@ export function CheckboxCard(props: CheckboxCardProps) {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className={classes.textContainer}>
|
<div className={classes.textContainer}>
|
||||||
<Input.Label required={props.required}>{props.label}</Input.Label>
|
<Input.Label required={props.required} component={"div"}>{props.label}</Input.Label>
|
||||||
{props.description &&
|
{props.description &&
|
||||||
<Input.Description><InnerHtml html={props.description.toString() ?? ""}/></Input.Description>}
|
<Input.Description component={"div"}>
|
||||||
|
<InnerHtml html={props.description.toString() ?? ""}/>
|
||||||
|
</Input.Description>}
|
||||||
{props.error && <Input.Error>{props.error}</Input.Error>}
|
{props.error && <Input.Error>{props.error}</Input.Error>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import {RecordModel} from "pocketbase";
|
import {RecordModel} from "pocketbase";
|
||||||
import {UseMutationResult} from "@tanstack/react-query";
|
import {UseMutationResult} from "@tanstack/react-query";
|
||||||
import {CheckIcon, Combobox, Group, Pill, PillsInput, PillsInputProps, Stack, Text, useCombobox} from "@mantine/core";
|
import {CheckIcon, Combobox, Group, Pill, PillsInput, PillsInputProps, Stack, Text, useCombobox} from "@mantine/core";
|
||||||
import {useState} from "react";
|
import {useEffect, useState} from "react";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* GenericRecordInputProps is a generic type that describes the props that are common to all Record Input Wrappers
|
* GenericRecordInputProps is a generic type that describes the props that are common to all Record Input Wrappers
|
||||||
|
@ -57,6 +57,11 @@ export default function RecordSearchInput<T extends RecordModel>(props: {
|
||||||
props.setSelectedRecords(props.selectedRecords.filter((record) => record.id !== val))
|
props.setSelectedRecords(props.selectedRecords.filter((record) => record.id !== val))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
props.recordSearchMutation.mutate('')
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
const searchResults = (props.recordSearchMutation.data || [])
|
const searchResults = (props.recordSearchMutation.data || [])
|
||||||
.map((recordView) => (
|
.map((recordView) => (
|
||||||
<Combobox.Option value={recordView.id} key={recordView.id}
|
<Combobox.Option value={recordView.id} key={recordView.id}
|
||||||
|
|
|
@ -10,6 +10,7 @@ import {
|
||||||
IconLogout,
|
IconLogout,
|
||||||
IconMailCog,
|
IconMailCog,
|
||||||
IconPassword,
|
IconPassword,
|
||||||
|
IconRefresh,
|
||||||
IconServer,
|
IconServer,
|
||||||
IconServerOff,
|
IconServerOff,
|
||||||
IconUser
|
IconUser
|
||||||
|
@ -24,7 +25,7 @@ export default function UserMenuModal() {
|
||||||
|
|
||||||
const {handler: changeEmailHandler} = useChangeEmail()
|
const {handler: changeEmailHandler} = useChangeEmail()
|
||||||
|
|
||||||
const {logout, apiIsHealthy, user} = usePB()
|
const {logout, apiIsHealthy, user, refreshUser} = usePB()
|
||||||
|
|
||||||
const {showHelp, toggleShowHelp} = useShowHelp()
|
const {showHelp, toggleShowHelp} = useShowHelp()
|
||||||
|
|
||||||
|
@ -188,6 +189,19 @@ export default function UserMenuModal() {
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</>}
|
</>}
|
||||||
|
|
||||||
|
<Tooltip label={"Anmeldedaten neu laden"}>
|
||||||
|
<ActionIcon
|
||||||
|
variant={"transparent"}
|
||||||
|
color={"orange"}
|
||||||
|
aria-label={"logout"}
|
||||||
|
onClick={() => {
|
||||||
|
refreshUser()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconRefresh/>
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip label={"Ausloggen"}>
|
<Tooltip label={"Ausloggen"}>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
variant={"transparent"}
|
variant={"transparent"}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import {createContext, DependencyList, ReactNode, useCallback, useContext, useEffect, useMemo, useState} from "react"
|
import {createContext, DependencyList, ReactNode, useCallback, useContext, useEffect, useMemo, useState} from "react"
|
||||||
import PocketBase, {ClientResponseError, LocalAuthStore, RecordAuthResponse, RecordSubscription} from 'pocketbase'
|
import PocketBase, {ClientResponseError, LocalAuthStore, RecordAuthResponse, RecordSubscription} from 'pocketbase'
|
||||||
import ms from "ms";
|
import ms from "ms";
|
||||||
import {useInterval} from "@mantine/hooks";
|
|
||||||
import {useQuery} from "@tanstack/react-query";
|
import {useQuery} from "@tanstack/react-query";
|
||||||
import {TypedPocketBase} from "@/models";
|
import {TypedPocketBase} from "@/models";
|
||||||
import {PB_BASE_URL, PB_STORAGE_KEY, PB_USER_COLLECTION} from "../../config.ts";
|
import {PB_BASE_URL, PB_STORAGE_KEY, PB_USER_COLLECTION} from "../../config.ts";
|
||||||
|
@ -73,20 +72,29 @@ const PocketData = () => {
|
||||||
refetchInterval: oneMinuteInMs
|
refetchInterval: oneMinuteInMs
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const refreshUserQuery = useQuery({
|
||||||
|
queryKey: ["refreshUser"],
|
||||||
|
queryFn: async () => {
|
||||||
|
try {
|
||||||
|
const {record, token} = await pb.collection(PB_USER_COLLECTION).authRefresh({
|
||||||
|
expand: "memberOf"
|
||||||
|
})
|
||||||
|
pb.authStore.save(token, record)
|
||||||
|
return record
|
||||||
|
} catch (e) {
|
||||||
|
pb.authStore.clear()
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
refetchInterval: oneMinuteInMs
|
||||||
|
})
|
||||||
|
|
||||||
const [user, setUser] = useState(pb.authStore.model)
|
const [user, setUser] = useState(pb.authStore.model)
|
||||||
|
|
||||||
pb.authStore.onChange((_, userRecord) => {
|
pb.authStore.onChange((_, userRecord) => {
|
||||||
setUser(userRecord)
|
setUser(userRecord)
|
||||||
})
|
})
|
||||||
|
|
||||||
const refreshUser = useCallback(async () => {
|
|
||||||
await pb.collection(PB_USER_COLLECTION).authRefresh({
|
|
||||||
expand: "memberOf"
|
|
||||||
}).catch(() => {
|
|
||||||
pb.authStore.clear()
|
|
||||||
})
|
|
||||||
}, [pb])
|
|
||||||
|
|
||||||
const ldapLogin = useCallback(async (usernameOrCN: string, password: string) => {
|
const ldapLogin = useCallback(async (usernameOrCN: string, password: string) => {
|
||||||
await pb.send<RecordAuthResponse>("/api/ldap/login", {
|
await pb.send<RecordAuthResponse>("/api/ldap/login", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
@ -126,9 +134,7 @@ const PocketData = () => {
|
||||||
pb.collection(idOrName).unsubscribe(topic)
|
pb.collection(idOrName).unsubscribe(topic)
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, deps ? deps : []);
|
}, deps ? deps : [])
|
||||||
|
|
||||||
useInterval(refreshUser, oneMinuteInMs)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ldapLogin,
|
ldapLogin,
|
||||||
|
@ -136,7 +142,7 @@ const PocketData = () => {
|
||||||
logout,
|
logout,
|
||||||
user: user as UserModal | null,
|
user: user as UserModal | null,
|
||||||
pb,
|
pb,
|
||||||
refreshUser,
|
refreshUser: refreshUserQuery.refetch,
|
||||||
useSubscription,
|
useSubscription,
|
||||||
apiIsHealthy: apiIsHealthyQuery.data ?? false
|
apiIsHealthy: apiIsHealthyQuery.data ?? false
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,6 +39,8 @@ export type EventListModel = {
|
||||||
event: string
|
event: string
|
||||||
entryQuestionSchema: FormSchema | null
|
entryQuestionSchema: FormSchema | null
|
||||||
entryStatusSchema: FormSchema | null;
|
entryStatusSchema: FormSchema | null;
|
||||||
|
ignoreDefaultEntryQuestionScheme: boolean | null;
|
||||||
|
ignoreDefaultEntryStatusSchema: boolean | null;
|
||||||
expand?: {
|
expand?: {
|
||||||
event: EventModel;
|
event: EventModel;
|
||||||
}
|
}
|
||||||
|
@ -49,7 +51,7 @@ export type EventListSlotModel = {
|
||||||
eventList: string;
|
eventList: string;
|
||||||
startDate: string;
|
startDate: string;
|
||||||
endDate: string;
|
endDate: string;
|
||||||
maxEntries: number | null;
|
maxEntries: number;
|
||||||
description: string | null;
|
description: string | null;
|
||||||
expand?: {
|
expand?: {
|
||||||
eventList: EventListModel;
|
eventList: EventListModel;
|
||||||
|
@ -59,7 +61,7 @@ export type EventListSlotModel = {
|
||||||
export type EventListSlotsWithEntriesCountModel = EventListSlotModel
|
export type EventListSlotsWithEntriesCountModel = EventListSlotModel
|
||||||
& { entriesCount: number }
|
& { entriesCount: number }
|
||||||
& Pick<EventModel, "defaultEntryQuestionSchema" | "defaultEntryStatusSchema">
|
& Pick<EventModel, "defaultEntryQuestionSchema" | "defaultEntryStatusSchema">
|
||||||
& Pick<EventListModel, "entryQuestionSchema" | "entryStatusSchema">
|
& Pick<EventListModel, "entryQuestionSchema" | "entryStatusSchema" | "ignoreDefaultEntryStatusSchema" | "ignoreDefaultEntryQuestionScheme">
|
||||||
|
|
||||||
export type EventListSlotEntryModel = {
|
export type EventListSlotEntryModel = {
|
||||||
entryQuestionData: FieldEntries;
|
entryQuestionData: FieldEntries;
|
||||||
|
@ -91,4 +93,4 @@ export type EventListSlotEntriesWithUserModel =
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
& Pick<EventModel, "defaultEntryQuestionSchema" | "defaultEntryStatusSchema">
|
& Pick<EventModel, "defaultEntryQuestionSchema" | "defaultEntryStatusSchema">
|
||||||
& Pick<EventListModel, "entryQuestionSchema" | "entryStatusSchema">
|
& Pick<EventListModel, "entryQuestionSchema" | "entryStatusSchema" | "ignoreDefaultEntryQuestionScheme" | "ignoreDefaultEntryStatusSchema">
|
|
@ -7,6 +7,7 @@ import {
|
||||||
Alert,
|
Alert,
|
||||||
Anchor,
|
Anchor,
|
||||||
Breadcrumbs,
|
Breadcrumbs,
|
||||||
|
Code,
|
||||||
createPolymorphicComponent,
|
createPolymorphicComponent,
|
||||||
Grid,
|
Grid,
|
||||||
Group,
|
Group,
|
||||||
|
@ -41,6 +42,8 @@ import {useEventRights} from "@/pages/events/util.ts";
|
||||||
import EventFavourites from "./EventComponents/EventFavourites.tsx";
|
import EventFavourites from "./EventComponents/EventFavourites.tsx";
|
||||||
import {forwardRef} from "react";
|
import {forwardRef} from "react";
|
||||||
import {APP_URL} from "../../../../../config.ts";
|
import {APP_URL} from "../../../../../config.ts";
|
||||||
|
import ShowDebug from "@/components/ShowDebug.tsx";
|
||||||
|
import {pprintDateTime} from "@/lib/datetime.ts";
|
||||||
|
|
||||||
|
|
||||||
type NavIconProps = {
|
type NavIconProps = {
|
||||||
|
@ -148,6 +151,13 @@ export default function EditEventRouter() {
|
||||||
<Grid className={classes.grid} gutter={"sm"}>
|
<Grid className={classes.grid} gutter={"sm"}>
|
||||||
<Grid.Col span={{base: 12, sm: 3}}>
|
<Grid.Col span={{base: 12, sm: 3}}>
|
||||||
<div className={"stack"}>
|
<div className={"stack"}>
|
||||||
|
<ShowDebug>
|
||||||
|
Event Id - <Code>{event.id}</Code>
|
||||||
|
<br/>
|
||||||
|
created - <Code>{pprintDateTime(event.created)}</Code>
|
||||||
|
<br/>
|
||||||
|
updated - <Code>{pprintDateTime(event.updated)}</Code>
|
||||||
|
</ShowDebug>
|
||||||
<EventData event={event}/>
|
<EventData event={event}/>
|
||||||
<EventLinks event={event}/>
|
<EventLinks event={event}/>
|
||||||
<EventFavourites event={event}/>
|
<EventFavourites event={event}/>
|
||||||
|
|
|
@ -90,7 +90,7 @@ export const EventSettingsMenu = ({event, target}: { event: EventModel, target:
|
||||||
|
|
||||||
<Menu.Dropdown>
|
<Menu.Dropdown>
|
||||||
{
|
{
|
||||||
nav.map(({label, children,}, index, array) => <div key={index}>
|
nav.map(({label, children,}, index) => <div key={index}>
|
||||||
<Menu.Label>{label}</Menu.Label>
|
<Menu.Label>{label}</Menu.Label>
|
||||||
{children.map(({title, icon, to}) => (
|
{children.map(({title, icon, to}) => (
|
||||||
<Anchor component={NavLink} to={`/events/e/${event.id}/settings/${to}`} key={to}>
|
<Anchor component={NavLink} to={`/events/e/${event.id}/settings/${to}`} key={to}>
|
||||||
|
@ -101,7 +101,6 @@ export const EventSettingsMenu = ({event, target}: { event: EventModel, target:
|
||||||
)}
|
)}
|
||||||
</Anchor>
|
</Anchor>
|
||||||
))}
|
))}
|
||||||
{index !== array.length - 1 && <Menu.Divider/>}
|
|
||||||
</div>)
|
</div>)
|
||||||
}
|
}
|
||||||
</Menu.Dropdown>
|
</Menu.Dropdown>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import {useQuery} from "@tanstack/react-query";
|
import {useQuery} from "@tanstack/react-query";
|
||||||
import {Alert, Breadcrumbs, Button, Group, LoadingOverlay, Title} from "@mantine/core";
|
import {Alert, Breadcrumbs, Button, Code, Group, LoadingOverlay, Title} from "@mantine/core";
|
||||||
import {
|
import {
|
||||||
IconCheckupList,
|
IconCheckupList,
|
||||||
IconClockCog,
|
IconClockCog,
|
||||||
|
@ -13,13 +13,15 @@ import {Link, Navigate, NavLink, Route, Routes, useParams} from "react-router-do
|
||||||
import InnerHtml from "@/components/InnerHtml";
|
import InnerHtml from "@/components/InnerHtml";
|
||||||
import {EventModel} from "@/models/EventTypes.ts";
|
import {EventModel} from "@/models/EventTypes.ts";
|
||||||
import {PocketBaseErrorAlert, usePB} from "@/lib/pocketbase.tsx";
|
import {PocketBaseErrorAlert, usePB} from "@/lib/pocketbase.tsx";
|
||||||
import SlotsTable from "./EventListSlotsTable";
|
import ListSlots from "./ListSlots";
|
||||||
import EventListSettings from "@/pages/events/e/:eventId/EventLists/:listId/EventListSettings/EventListSettings.tsx";
|
|
||||||
import EventListEntryQuestionSettings
|
|
||||||
from "@/pages/events/e/:eventId/EventLists/:listId/EventListSettings/EventListEntryQuestionSettings.tsx";
|
|
||||||
import EventListEntryStatusSettings
|
|
||||||
from "@/pages/events/e/:eventId/EventLists/:listId/EventListSettings/EventListEntryStatusSettings.tsx";
|
|
||||||
import TextWithIcon from "@/components/layout/TextWithIcon";
|
import TextWithIcon from "@/components/layout/TextWithIcon";
|
||||||
|
import ListSettings from "@/pages/events/e/:eventId/EventLists/:listId/ListSettings/ListSettings.tsx";
|
||||||
|
import ListEntryQuestionSettings
|
||||||
|
from "@/pages/events/e/:eventId/EventLists/:listId/ListSettings/ListEntryQuestionSettings.tsx";
|
||||||
|
import ListEntryStatusSettings
|
||||||
|
from "@/pages/events/e/:eventId/EventLists/:listId/ListSettings/ListEntryStatusSettings.tsx";
|
||||||
|
import ShowDebug from "@/components/ShowDebug.tsx";
|
||||||
|
import {pprintDateTime} from "@/lib/datetime.ts";
|
||||||
|
|
||||||
|
|
||||||
export default function EventListRouter({event}: { event: EventModel }) {
|
export default function EventListRouter({event}: { event: EventModel }) {
|
||||||
|
@ -57,6 +59,14 @@ export default function EventListRouter({event}: { event: EventModel }) {
|
||||||
</Link>
|
</Link>
|
||||||
</Breadcrumbs>
|
</Breadcrumbs>
|
||||||
|
|
||||||
|
<ShowDebug>
|
||||||
|
List Id - <Code>{list.id}</Code>
|
||||||
|
<br/>
|
||||||
|
created - <Code>{pprintDateTime(list.created)}</Code>
|
||||||
|
<br/>
|
||||||
|
updated - <Code>{pprintDateTime(list.updated)}</Code>
|
||||||
|
</ShowDebug>
|
||||||
|
|
||||||
<Alert color={list.open ? "green" : "red"}>
|
<Alert color={list.open ? "green" : "red"}>
|
||||||
<TextWithIcon icon={list.open ? <IconLockOpen size={16}/> : <IconLock size={16}/>}>
|
<TextWithIcon icon={list.open ? <IconLockOpen size={16}/> : <IconLock size={16}/>}>
|
||||||
Liste ist für Anmeldungen <b>{list.open ? "geöffnet" : "geschlossen"}</b>
|
Liste ist für Anmeldungen <b>{list.open ? "geöffnet" : "geschlossen"}</b>
|
||||||
|
@ -90,7 +100,7 @@ export default function EventListRouter({event}: { event: EventModel }) {
|
||||||
{[
|
{[
|
||||||
{
|
{
|
||||||
icon: <IconClockCog/>,
|
icon: <IconClockCog/>,
|
||||||
to: `/events/e/${event.id}/lists/overview/${list.id}/entries`,
|
to: `/events/e/${event.id}/lists/overview/${list.id}/slots`,
|
||||||
title: "Zeitslots"
|
title: "Zeitslots"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -123,11 +133,11 @@ export default function EventListRouter({event}: { event: EventModel }) {
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route index element={<Navigate to={"entries"} replace/>}/>
|
<Route index element={<Navigate to={"slots"} replace/>}/>
|
||||||
<Route path={"entries"} element={<SlotsTable event={event} list={list}/>}/>
|
<Route path={"slots"} element={<ListSlots list={list}/>}/>
|
||||||
<Route path={"settings"} element={<EventListSettings list={list} event={event}/>}/>
|
<Route path={"settings"} element={<ListSettings list={list} event={event}/>}/>
|
||||||
<Route path={"questions"} element={<EventListEntryQuestionSettings list={list} event={event}/>}/>
|
<Route path={"questions"} element={<ListEntryQuestionSettings list={list} event={event}/>}/>
|
||||||
<Route path={"status"} element={<EventListEntryStatusSettings list={list} event={event}/>}/>
|
<Route path={"status"} element={<ListEntryStatusSettings list={list} event={event}/>}/>
|
||||||
</Routes>
|
</Routes>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
|
@ -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 FormBuilder from "@/components/formUtil/formBuilder";
|
||||||
import FormFieldsPreview from "@/components/formUtil/formBuilder/FormFieldsPreview.tsx";
|
import FormFieldsPreview from "@/components/formUtil/formBuilder/FormFieldsPreview.tsx";
|
||||||
import {IconAlertTriangle} from "@tabler/icons-react";
|
import {IconAlertTriangle} from "@tabler/icons-react";
|
||||||
|
import {useForm} from "@mantine/form";
|
||||||
|
import {CheckboxCard} from "@/components/input/CheckboxCard";
|
||||||
|
|
||||||
export default function EventListEntryQuestionSettings({list, event}: { list: EventListModel, event: EventModel }) {
|
export default function ListEntryQuestionSettings({list, event}: { list: EventListModel, event: EventModel }) {
|
||||||
const {pb} = usePB()
|
const {pb} = usePB()
|
||||||
|
|
||||||
|
const formValues = useForm({
|
||||||
|
initialValues: {
|
||||||
|
ignoreDefaultEntryQuestionScheme: list.ignoreDefaultEntryQuestionScheme || false,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const editMutation = useMutation({
|
const editMutation = useMutation({
|
||||||
mutationFn: async (schema: FormSchema) => {
|
mutationFn: async (schema: FormSchema) => {
|
||||||
return await pb.collection("eventLists").update(list.id, {
|
return await pb.collection("eventLists").update(list.id, {
|
||||||
entryQuestionSchema: schema
|
entryQuestionSchema: schema,
|
||||||
|
...formValues.values
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
showSuccessNotification("Fragen gespeichert")
|
showSuccessNotification("Fragen und Liste gespeichert")
|
||||||
return queryClient.invalidateQueries({queryKey: ["event", event.id, "list", list.id]})
|
return queryClient.invalidateQueries({queryKey: ["event", event.id, "list", list.id]})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -29,7 +38,7 @@ export default function EventListEntryQuestionSettings({list, event}: { list: Ev
|
||||||
|
|
||||||
<PocketBaseErrorAlert error={editMutation.error}/>
|
<PocketBaseErrorAlert error={editMutation.error}/>
|
||||||
|
|
||||||
{event.defaultEntryQuestionSchema && (
|
{(!list.ignoreDefaultEntryQuestionScheme && event.defaultEntryQuestionSchema) && (
|
||||||
<Alert color={"blue"} title={"Standard Fragen"} className={"stack"}>
|
<Alert color={"blue"} title={"Standard Fragen"} className={"stack"}>
|
||||||
<Text c={"dimmed"} mb={"sm"} size={"xs"}>
|
<Text c={"dimmed"} mb={"sm"} size={"xs"}>
|
||||||
Folgende Fragen sind standardmäßig für dieses Event vorgesehen
|
Folgende Fragen sind standardmäßig für dieses Event vorgesehen
|
||||||
|
@ -39,6 +48,13 @@ export default function EventListEntryQuestionSettings({list, event}: { list: Ev
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<CheckboxCard
|
||||||
|
label={"Event-Standardfragen ignorieren"}
|
||||||
|
description={"Wenn du diese Option aktivierst, werden die Standardfragen des Events nicht für diese Liste verwendet. " +
|
||||||
|
"Stattdessen werden nur die Fragen spezifisch für diese Liste verwendet."}
|
||||||
|
{...formValues.getInputProps("ignoreDefaultEntryQuestionScheme", {type: "checkbox"})}
|
||||||
|
/>
|
||||||
|
|
||||||
<Alert color={"orange"} icon={<IconAlertTriangle/>}>
|
<Alert color={"orange"} icon={<IconAlertTriangle/>}>
|
||||||
Wenn du Felder entfernst, werden alle Daten,
|
Wenn du Felder entfernst, werden alle Daten,
|
||||||
die für diese Felder gespeichert wurden nicht mehr angezeigt.
|
die für diese Felder gespeichert wurden nicht mehr angezeigt.
|
||||||
|
@ -48,7 +64,7 @@ export default function EventListEntryQuestionSettings({list, event}: { list: Ev
|
||||||
defaultValue={list.entryQuestionSchema || {fields: []}}
|
defaultValue={list.entryQuestionSchema || {fields: []}}
|
||||||
onSubmit={(schema) => editMutation.mutate(schema)}
|
onSubmit={(schema) => editMutation.mutate(schema)}
|
||||||
withPreview
|
withPreview
|
||||||
additionalSchemaToPreview={event.defaultEntryQuestionSchema || {fields: []}}
|
additionalSchemaToPreview={!list.ignoreDefaultEntryQuestionScheme && event.defaultEntryQuestionSchema || {fields: []}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
|
@ -8,18 +8,27 @@ import {FormSchema} from "@/components/formUtil/formBuilder/types.ts";
|
||||||
import FormBuilder from "@/components/formUtil/formBuilder";
|
import FormBuilder from "@/components/formUtil/formBuilder";
|
||||||
import FormFieldsPreview from "@/components/formUtil/formBuilder/FormFieldsPreview.tsx";
|
import FormFieldsPreview from "@/components/formUtil/formBuilder/FormFieldsPreview.tsx";
|
||||||
import {IconAlertTriangle} from "@tabler/icons-react";
|
import {IconAlertTriangle} from "@tabler/icons-react";
|
||||||
|
import {useForm} from "@mantine/form";
|
||||||
|
import {CheckboxCard} from "@/components/input/CheckboxCard";
|
||||||
|
|
||||||
export default function EventListEntryStatusSettings({list, event}: { list: EventListModel, event: EventModel }) {
|
export default function ListEntryStatusSettings({list, event}: { list: EventListModel, event: EventModel }) {
|
||||||
const {pb} = usePB()
|
const {pb} = usePB()
|
||||||
|
|
||||||
const editMutation = useMutation({
|
const formValues = useForm({
|
||||||
|
initialValues: {
|
||||||
|
ignoreDefaultEntryStatusSchema: list.ignoreDefaultEntryStatusSchema || false,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const editStatusMutation = useMutation({
|
||||||
mutationFn: async (schema: FormSchema) => {
|
mutationFn: async (schema: FormSchema) => {
|
||||||
return await pb.collection("eventLists").update(list.id, {
|
await pb.collection("eventLists").update(list.id, {
|
||||||
entryStatusSchema: schema
|
entryStatusSchema: schema,
|
||||||
|
...formValues.values
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
showSuccessNotification("Status Felder gespeichert")
|
showSuccessNotification("Status Felder und Liste gespeichert")
|
||||||
return queryClient.invalidateQueries({queryKey: ["event", event.id, "list", list.id]})
|
return queryClient.invalidateQueries({queryKey: ["event", event.id, "list", list.id]})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -27,9 +36,9 @@ export default function EventListEntryStatusSettings({list, event}: { list: Even
|
||||||
return <div className={"section stack"}>
|
return <div className={"section stack"}>
|
||||||
<Title order={4} c={"blue"}>Eintrags-Status der Liste</Title>
|
<Title order={4} c={"blue"}>Eintrags-Status der Liste</Title>
|
||||||
|
|
||||||
<PocketBaseErrorAlert error={editMutation.error}/>
|
<PocketBaseErrorAlert error={editStatusMutation.error}/>
|
||||||
|
|
||||||
{event.defaultEntryStatusSchema && (
|
{(!list.ignoreDefaultEntryStatusSchema && event.defaultEntryStatusSchema) && (
|
||||||
<Alert color={"blue"} title={"Standard Status Felder"} className={"stack"}>
|
<Alert color={"blue"} title={"Standard Status Felder"} className={"stack"}>
|
||||||
<Text c={"dimmed"} mb={"sm"} size={"xs"}>
|
<Text c={"dimmed"} mb={"sm"} size={"xs"}>
|
||||||
Folgende Status-Felder sind standardmäßig für dieses Event vorgesehen
|
Folgende Status-Felder sind standardmäßig für dieses Event vorgesehen
|
||||||
|
@ -39,6 +48,13 @@ export default function EventListEntryStatusSettings({list, event}: { list: Even
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<CheckboxCard
|
||||||
|
label={"Event-Standardstatus ignorieren"}
|
||||||
|
description={"Wenn du diese Option aktivierst, werden die Standardstatus des Events nicht für diese Liste verwendet. " +
|
||||||
|
"Stattdessen wird nur der Status spezifisch für diese Liste verwendet."}
|
||||||
|
{...formValues.getInputProps("ignoreDefaultEntryStatusSchema", {type: "checkbox"})}
|
||||||
|
/>
|
||||||
|
|
||||||
<Alert color={"orange"} icon={<IconAlertTriangle/>}>
|
<Alert color={"orange"} icon={<IconAlertTriangle/>}>
|
||||||
Wenn du Felder entfernst, werden alle Daten,
|
Wenn du Felder entfernst, werden alle Daten,
|
||||||
die für diese Felder gespeichert wurden nicht mehr angezeigt.
|
die für diese Felder gespeichert wurden nicht mehr angezeigt.
|
||||||
|
@ -46,9 +62,9 @@ export default function EventListEntryStatusSettings({list, event}: { list: Even
|
||||||
|
|
||||||
<FormBuilder
|
<FormBuilder
|
||||||
defaultValue={list.entryStatusSchema || {fields: []}}
|
defaultValue={list.entryStatusSchema || {fields: []}}
|
||||||
onSubmit={(schema) => editMutation.mutate(schema)}
|
onSubmit={(schema) => editStatusMutation.mutate(schema)}
|
||||||
withPreview
|
withPreview
|
||||||
additionalSchemaToPreview={event.defaultEntryStatusSchema || {fields: []}}
|
additionalSchemaToPreview={!list.ignoreDefaultEntryStatusSchema && event.defaultEntryStatusSchema || {fields: []}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
|
@ -11,7 +11,7 @@ import {useNavigate} from "react-router-dom";
|
||||||
import {useConfirmModal} from "@/components/ConfirmModal.tsx";
|
import {useConfirmModal} from "@/components/ConfirmModal.tsx";
|
||||||
import {CheckboxCard} from "@/components/input/CheckboxCard";
|
import {CheckboxCard} from "@/components/input/CheckboxCard";
|
||||||
|
|
||||||
export default function EventListSettings({list, event}: { list: EventListModel, event: EventModel }) {
|
export default function ListSettings({list, event}: { list: EventListModel, event: EventModel }) {
|
||||||
const {pb} = usePB()
|
const {pb} = usePB()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
@ -22,7 +22,7 @@ export default function EventListSettings({list, event}: { list: EventListModel,
|
||||||
allowOverlappingEntries: list.allowOverlappingEntries,
|
allowOverlappingEntries: list.allowOverlappingEntries,
|
||||||
onlyStuVeAccounts: list.onlyStuVeAccounts,
|
onlyStuVeAccounts: list.onlyStuVeAccounts,
|
||||||
favorite: list.favorite,
|
favorite: list.favorite,
|
||||||
description: list.description || ""
|
description: list.description || "",
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -99,7 +99,7 @@ export default function EventListSettings({list, event}: { list: EventListModel,
|
||||||
|
|
||||||
<CheckboxCard
|
<CheckboxCard
|
||||||
label={"Überlappende Anmeldungen erlauben"}
|
label={"Überlappende Anmeldungen erlauben"}
|
||||||
description={" Wenn diese Option aktiviert ist, kann sich eine Person in überlappende Zeitslots anmelden. " +
|
description={"Wenn diese Option aktiviert ist, kann sich eine Person in überlappende Zeitslots anmelden. " +
|
||||||
"Dabei werden alle Zeitslots von diesem Event beachtet. " +
|
"Dabei werden alle Zeitslots von diesem Event beachtet. " +
|
||||||
"Bei dem Verschieden von Anmeldungen wirds diese Regel nicht beachtet!"}
|
"Bei dem Verschieden von Anmeldungen wirds diese Regel nicht beachtet!"}
|
||||||
{...formValues.getInputProps("allowOverlappingEntries", {type: "checkbox"})}
|
{...formValues.getInputProps("allowOverlappingEntries", {type: "checkbox"})}
|
|
@ -8,19 +8,18 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.slotRow {
|
.slotRow {
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--gap);
|
gap: var(--gap);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
& > :nth-child(2) {
|
& > :nth-child(1) {
|
||||||
width: 50%;
|
width: 50%;
|
||||||
font-size: var(--mantine-font-size-sm);
|
font-size: var(--mantine-font-size-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
& > :nth-child(3) {
|
& > :nth-child(2) {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,27 +1,24 @@
|
||||||
import {EventListModel, EventListSlotsWithEntriesCountModel, EventModel} from "@/models/EventTypes.ts";
|
import {EventListModel, EventListSlotsWithEntriesCountModel} from "@/models/EventTypes.ts";
|
||||||
import {useDisclosure} from "@mantine/hooks";
|
import {useDisclosure} from "@mantine/hooks";
|
||||||
import classes from "./EventListSlotRow.module.css";
|
import classes from "./ListSlotRow.module.css";
|
||||||
import {ActionIcon, Alert, Code, Collapse, Group, Modal, ThemeIcon} from "@mantine/core";
|
import {ActionIcon, Alert, Code, Group, Modal} from "@mantine/core";
|
||||||
import {IconAdjustments, IconAdjustmentsOff, IconInfoCircle, IconUsersMinus, IconUsersPlus} from "@tabler/icons-react";
|
import {IconAdjustments, IconAdjustmentsOff, IconInfoCircle} from "@tabler/icons-react";
|
||||||
import {RenderDateRange} from "../../components/RenderDateRange.tsx";
|
import {RenderDateRange} from "@/pages/events/e/:eventId/EventLists/EventListComponents/RenderDateRange.tsx";
|
||||||
import {modals} from "@mantine/modals";
|
import {modals} from "@mantine/modals";
|
||||||
import InnerHtml from "@/components/InnerHtml";
|
import InnerHtml from "@/components/InnerHtml";
|
||||||
import {EventListSlotEntriesTable} from "./EventListSlotEntriesTable.tsx";
|
|
||||||
import UpsertEventListSlot from "@/pages/events/e/:eventId/EventLists/components/UpsertEventListSlot.tsx";
|
|
||||||
import EventListSlotProgress from "@/pages/events/e/:eventId/EventLists/components/EventListSlotProgress.tsx";
|
|
||||||
import ShowDebug from "@/components/ShowDebug.tsx";
|
import ShowDebug from "@/components/ShowDebug.tsx";
|
||||||
|
import UpsertSlot from "@/pages/events/e/:eventId/EventLists/EventListComponents/UpsertSlot.tsx";
|
||||||
|
import SlotProgress from "@/pages/events/e/:eventId/EventLists/EventListComponents/SlotProgress.tsx";
|
||||||
|
import {pprintDateTime} from "@/lib/datetime.ts";
|
||||||
|
|
||||||
export const EventListSlotRow = ({slot, list, event, refetch}: {
|
export const ListSlotRow = ({slot, list, refetch}: {
|
||||||
slot: EventListSlotsWithEntriesCountModel,
|
slot: EventListSlotsWithEntriesCountModel,
|
||||||
list: EventListModel,
|
list: EventListModel,
|
||||||
event: EventModel,
|
|
||||||
refetch: () => void
|
refetch: () => void
|
||||||
}) => {
|
}) => {
|
||||||
|
|
||||||
const [showEditModal, showEditModalHandler] = useDisclosure(false)
|
const [showEditModal, showEditModalHandler] = useDisclosure(false)
|
||||||
|
|
||||||
const [expanded, expandedHandler] = useDisclosure(false)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`${classes.slotContainer}`}>
|
<div className={`${classes.slotContainer}`}>
|
||||||
<Modal
|
<Modal
|
||||||
|
@ -29,22 +26,15 @@ export const EventListSlotRow = ({slot, list, event, refetch}: {
|
||||||
title={`Zeitslot bearbeiten`}
|
title={`Zeitslot bearbeiten`}
|
||||||
size={"lg"}
|
size={"lg"}
|
||||||
>
|
>
|
||||||
<UpsertEventListSlot slot={slot} list={list} onSuccess={() => {
|
<UpsertSlot slot={slot} list={list} onSuccess={() => {
|
||||||
showEditModalHandler.close()
|
showEditModalHandler.close()
|
||||||
refetch()
|
refetch()
|
||||||
}} onAbort={showEditModalHandler.close}/>
|
}} onAbort={showEditModalHandler.close}/>
|
||||||
</Modal>
|
</Modal>
|
||||||
<div
|
<div className={classes.slotRow}>
|
||||||
className={classes.slotRow}
|
|
||||||
onClick={expandedHandler.toggle}
|
|
||||||
>
|
|
||||||
<ThemeIcon variant={"transparent"}>
|
|
||||||
{
|
|
||||||
expanded ? <IconUsersMinus size={16}/> : <IconUsersPlus size={16}/>
|
|
||||||
}
|
|
||||||
</ThemeIcon>
|
|
||||||
<RenderDateRange start={new Date(slot.startDate)} end={new Date(slot.endDate)}/>
|
<RenderDateRange start={new Date(slot.startDate)} end={new Date(slot.endDate)}/>
|
||||||
<EventListSlotProgress slot={slot}/>
|
<SlotProgress slot={slot}/>
|
||||||
<Group gap={4} justify="right" wrap="nowrap">
|
<Group gap={4} justify="right" wrap="nowrap">
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
size="sm"
|
size="sm"
|
||||||
|
@ -67,9 +57,11 @@ export const EventListSlotRow = ({slot, list, event, refetch}: {
|
||||||
title: 'Beschreibung',
|
title: 'Beschreibung',
|
||||||
children: <div className={"stack"}>
|
children: <div className={"stack"}>
|
||||||
<ShowDebug>
|
<ShowDebug>
|
||||||
Listen-ID <Code>{list.id}</Code>
|
Slot Id - <Code>{slot.id}</Code>
|
||||||
<br/>
|
<br/>
|
||||||
Slot-ID <Code>{slot.id}</Code>
|
created - <Code>{pprintDateTime(slot.created)}</Code>
|
||||||
|
<br/>
|
||||||
|
updated - <Code>{pprintDateTime(slot.updated)}</Code>
|
||||||
</ShowDebug>
|
</ShowDebug>
|
||||||
{
|
{
|
||||||
slot.description ? <InnerHtml html={slot.description}/> :
|
slot.description ? <InnerHtml html={slot.description}/> :
|
||||||
|
@ -85,10 +77,6 @@ export const EventListSlotRow = ({slot, list, event, refetch}: {
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Group>
|
</Group>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Collapse in={expanded}>
|
|
||||||
<EventListSlotEntriesTable visible={expanded} slot={slot} list={list} refetch={refetch} event={event}/>
|
|
||||||
</Collapse>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
import {EventListModel, EventModel} from "@/models/EventTypes.ts";
|
import {EventListModel} from "@/models/EventTypes.ts";
|
||||||
import {PocketBaseErrorAlert, usePB} from "@/lib/pocketbase.tsx";
|
import {PocketBaseErrorAlert, usePB} from "@/lib/pocketbase.tsx";
|
||||||
import {useQuery} from "@tanstack/react-query";
|
import {useQuery} from "@tanstack/react-query";
|
||||||
import {Box, Button, Center, Pagination, Title} from "@mantine/core";
|
import {Box, Button, Center, Pagination, Title} from "@mantine/core";
|
||||||
|
@ -6,8 +6,8 @@ import {IconClockPlus} from "@tabler/icons-react";
|
||||||
import {useState} from "react";
|
import {useState} from "react";
|
||||||
import {useDisclosure} from "@mantine/hooks";
|
import {useDisclosure} from "@mantine/hooks";
|
||||||
import classes from './index.module.css'
|
import classes from './index.module.css'
|
||||||
import {EventListSlotRow} from "./EventListSlotRow.tsx";
|
import {ListSlotRow} from "./ListSlotRow.tsx";
|
||||||
import UpsertEventListSlot from "@/pages/events/e/:eventId/EventLists/components/UpsertEventListSlot.tsx";
|
import UpsertSlot from "@/pages/events/e/:eventId/EventLists/EventListComponents/UpsertSlot.tsx";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders a table with all slots for a list
|
* Renders a table with all slots for a list
|
||||||
|
@ -16,7 +16,7 @@ import UpsertEventListSlot from "@/pages/events/e/:eventId/EventLists/components
|
||||||
* @param event - event the list belongs to
|
* @param event - event the list belongs to
|
||||||
* @constructor
|
* @constructor
|
||||||
*/
|
*/
|
||||||
export default function SlotsTable({list, event}: { event: EventModel, list: EventListModel }) {
|
export default function ListSlots({list}: { list: EventListModel }) {
|
||||||
|
|
||||||
const {pb} = usePB()
|
const {pb} = usePB()
|
||||||
|
|
||||||
|
@ -39,8 +39,8 @@ export default function SlotsTable({list, event}: { event: EventModel, list: Eve
|
||||||
|
|
||||||
<div className={classes.table}>
|
<div className={classes.table}>
|
||||||
{query.data?.items.map(slot => (
|
{query.data?.items.map(slot => (
|
||||||
<EventListSlotRow
|
<ListSlotRow
|
||||||
key={slot.id} event={event} list={list} slot={slot}
|
key={slot.id} list={list} slot={slot}
|
||||||
refetch={query.refetch}
|
refetch={query.refetch}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
@ -53,7 +53,7 @@ export default function SlotsTable({list, event}: { event: EventModel, list: Eve
|
||||||
Neuen Slot hinzufügen
|
Neuen Slot hinzufügen
|
||||||
</Title>
|
</Title>
|
||||||
|
|
||||||
<UpsertEventListSlot list={list} onSuccess={() => {
|
<UpsertSlot list={list} onSuccess={() => {
|
||||||
showNewSlotFormHandler.close()
|
showNewSlotFormHandler.close()
|
||||||
query.refetch()
|
query.refetch()
|
||||||
}} onAbort={showNewSlotFormHandler.close}/>
|
}} onAbort={showNewSlotFormHandler.close}/>
|
|
@ -4,19 +4,18 @@ import {showSuccessNotification} from "@/components/util.tsx";
|
||||||
import {useConfirmModal} from "@/components/ConfirmModal.tsx";
|
import {useConfirmModal} from "@/components/ConfirmModal.tsx";
|
||||||
import {usePB} from "@/lib/pocketbase.tsx";
|
import {usePB} from "@/lib/pocketbase.tsx";
|
||||||
import {useDisclosure} from "@mantine/hooks";
|
import {useDisclosure} from "@mantine/hooks";
|
||||||
import {
|
import {ActionIcon, HoverCard, Menu} from "@mantine/core";
|
||||||
UpdateEventListSlotEntryStatusModal
|
import {IconArrowsMove, IconCheckupList, IconSend, IconSettings, IconTrash} from "@tabler/icons-react";
|
||||||
} from "@/pages/events/e/:eventId/EventLists/components/UpdateEventListSlotEntryStatusModal.tsx";
|
|
||||||
import {
|
|
||||||
MoveEventListSlotEntryModal
|
|
||||||
} from "@/pages/events/e/:eventId/EventLists/components/MoveEventListSlotEntryModal.tsx";
|
|
||||||
import {
|
|
||||||
UpdateEventListSlotEntryFormModal
|
|
||||||
} from "@/pages/events/e/:eventId/EventLists/components/UpdateEventListSlotEntryFormModal.tsx";
|
|
||||||
import {ActionIcon, Button, Menu} from "@mantine/core";
|
|
||||||
import {IconArrowsMove, IconCheckupList, IconForms, IconSettings, IconTrash} from "@tabler/icons-react";
|
|
||||||
import {getUserName} from "@/components/users/modals/util.tsx";
|
import {getUserName} from "@/components/users/modals/util.tsx";
|
||||||
|
|
||||||
|
import {
|
||||||
|
UpdateEntryStatusModal
|
||||||
|
} from "@/pages/events/e/:eventId/EventLists/EventListComponents/UpdateEntryStatusModal.tsx";
|
||||||
|
import {MoveEntryModal} from "@/pages/events/e/:eventId/EventLists/EventListComponents/MoveEntryModal.tsx";
|
||||||
|
import EntryStatusSpoiler from "@/pages/events/e/:eventId/EventLists/EventListComponents/EntryStatusSpoiler.tsx";
|
||||||
|
import {getListSchemas} from "@/pages/events/util.ts";
|
||||||
|
|
||||||
|
|
||||||
export default function EditSlotEntryMenu({entry, refetch}: {
|
export default function EditSlotEntryMenu({entry, refetch}: {
|
||||||
refetch: () => void,
|
refetch: () => void,
|
||||||
entry: EventListSlotEntriesWithUserModel
|
entry: EventListSlotEntriesWithUserModel
|
||||||
|
@ -28,8 +27,6 @@ export default function EditSlotEntryMenu({entry, refetch}: {
|
||||||
|
|
||||||
const [showMoveEntryModal, showMoveEntryModalHandler] = useDisclosure(false)
|
const [showMoveEntryModal, showMoveEntryModalHandler] = useDisclosure(false)
|
||||||
|
|
||||||
const [showEditFormModal, showEditFormModalHandler] = useDisclosure(false)
|
|
||||||
|
|
||||||
const deleteEntryMutation = useMutation({
|
const deleteEntryMutation = useMutation({
|
||||||
mutationFn: async () => {
|
mutationFn: async () => {
|
||||||
await pb.collection("eventListSlotEntries").delete(entry.id)
|
await pb.collection("eventListSlotEntries").delete(entry.id)
|
||||||
|
@ -46,62 +43,67 @@ export default function EditSlotEntryMenu({entry, refetch}: {
|
||||||
onConfirm: () => deleteEntryMutation.mutate()
|
onConfirm: () => deleteEntryMutation.mutate()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const {statusSchema} = getListSchemas(entry)
|
||||||
|
|
||||||
|
const noStatusField = statusSchema.fields.length === 0
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<ConfirmModal/>
|
<ConfirmModal/>
|
||||||
|
|
||||||
<UpdateEventListSlotEntryStatusModal
|
<UpdateEntryStatusModal
|
||||||
opened={showStatusEditModal}
|
opened={showStatusEditModal}
|
||||||
close={showStatusEditModalHandler.close}
|
close={showStatusEditModalHandler.close}
|
||||||
entry={entry}
|
entry={entry}
|
||||||
refetch={refetch}
|
refetch={refetch}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<MoveEventListSlotEntryModal
|
<MoveEntryModal
|
||||||
opened={showMoveEntryModal}
|
opened={showMoveEntryModal}
|
||||||
close={showMoveEntryModalHandler.close}
|
close={showMoveEntryModalHandler.close}
|
||||||
entry={entry}
|
entry={entry}
|
||||||
refetch={refetch}
|
refetch={refetch}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<UpdateEventListSlotEntryFormModal
|
<HoverCard width={'350px'} shadow="md" position={"top"} closeDelay={0} disabled={noStatusField}>
|
||||||
opened={showEditFormModal}
|
<HoverCard.Target>
|
||||||
close={showEditFormModalHandler.close}
|
<ActionIcon
|
||||||
refetch={refetch}
|
variant="light"
|
||||||
entry={entry}
|
color="blue"
|
||||||
/>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
leftSection={<IconCheckupList size={16}/>}
|
|
||||||
variant={"light"}
|
|
||||||
size={"compact-xs"}
|
|
||||||
onClick={showStatusEditModalHandler.toggle}
|
onClick={showStatusEditModalHandler.toggle}
|
||||||
|
aria-label={"edit entry status"}
|
||||||
|
disabled={noStatusField}
|
||||||
>
|
>
|
||||||
Status bearbeiten
|
<IconCheckupList size={16}/>
|
||||||
</Button>
|
</ActionIcon>
|
||||||
|
</HoverCard.Target>
|
||||||
|
<HoverCard.Dropdown p={0}>
|
||||||
|
<EntryStatusSpoiler entry={entry}/>
|
||||||
|
</HoverCard.Dropdown>
|
||||||
|
</HoverCard>
|
||||||
|
|
||||||
<Menu>
|
<Menu>
|
||||||
<Menu.Target>
|
<Menu.Target>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
size="sm"
|
|
||||||
variant="light"
|
variant="light"
|
||||||
color="blue"
|
color="blue"
|
||||||
|
aria-label={"edit entry"}
|
||||||
>
|
>
|
||||||
<IconSettings size={16}/>
|
<IconSettings size={16}/>
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Menu.Target>
|
</Menu.Target>
|
||||||
<Menu.Dropdown>
|
<Menu.Dropdown>
|
||||||
|
<Menu.Item
|
||||||
|
leftSection={<IconSend size={16}/>}
|
||||||
|
disabled
|
||||||
|
>
|
||||||
|
Person benachrichtigen
|
||||||
|
</Menu.Item>
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
leftSection={<IconArrowsMove size={16}/>}
|
leftSection={<IconArrowsMove size={16}/>}
|
||||||
onClick={showMoveEntryModalHandler.toggle}
|
onClick={showMoveEntryModalHandler.toggle}
|
||||||
>
|
>
|
||||||
Eintrag verschieben
|
Eintrag verschieben
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
<Menu.Item
|
|
||||||
leftSection={<IconForms size={16}/>}
|
|
||||||
onClick={showEditFormModalHandler.toggle}
|
|
||||||
>
|
|
||||||
Formular
|
|
||||||
</Menu.Item>
|
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
color={"red"} leftSection={<IconTrash size={16}/>}
|
color={"red"} leftSection={<IconTrash size={16}/>}
|
||||||
onClick={toggleConfirmModal}
|
onClick={toggleConfirmModal}
|
|
@ -1,25 +1,3 @@
|
||||||
.entryContainer {
|
|
||||||
margin-left: var(--mantine-spacing-lg);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.entryRow {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--gap);
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
& > * {
|
|
||||||
font-size: var(--mantine-font-size-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
& > :nth-child(2) {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.dataGrid {
|
.dataGrid {
|
||||||
padding: var(--gap) 0;
|
padding: var(--gap) 0;
|
||||||
}
|
}
|
||||||
|
@ -43,4 +21,7 @@
|
||||||
color: var(--mantine-color-dimmed);
|
color: var(--mantine-color-dimmed);
|
||||||
border-bottom: var(--border);
|
border-bottom: var(--border);
|
||||||
margin-bottom: var(--mantine-spacing-xs);
|
margin-bottom: var(--mantine-spacing-xs);
|
||||||
|
word-break: break-all;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
hyphens: auto;
|
||||||
}
|
}
|
|
@ -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 {PocketBaseErrorAlert, usePB} from "@/lib/pocketbase.tsx";
|
||||||
import {useMutation, useQuery} from "@tanstack/react-query";
|
import {useMutation, useQuery} from "@tanstack/react-query";
|
||||||
import {showSuccessNotification} from "@/components/util.tsx";
|
import {showSuccessNotification} from "@/components/util.tsx";
|
||||||
import {Alert, Button, Group, Modal, Select} from "@mantine/core";
|
import {Alert, Button, Group, Modal, Select, SelectProps, Text} from "@mantine/core";
|
||||||
import {useEffect, useState} from "react";
|
import {useEffect, useState} from "react";
|
||||||
import {pprintDateTime} from "@/lib/datetime.ts";
|
import {pprintDateTime} from "@/lib/datetime.ts";
|
||||||
import {getUserName, RenderUserName} from "@/components/users/modals/util.tsx";
|
import {getUserName, RenderUserName} from "@/components/users/modals/util.tsx";
|
||||||
|
import {CheckboxCard} from "@/components/input/CheckboxCard";
|
||||||
|
import {IconCheck} from "@tabler/icons-react";
|
||||||
|
import SlotProgress from "@/pages/events/e/:eventId/EventLists/EventListComponents/SlotProgress.tsx";
|
||||||
|
|
||||||
export const MoveEventListSlotEntryModal = ({opened, close, refetch, entry}: {
|
export const MoveEntryModal = ({opened, close, refetch, entry}: {
|
||||||
opened: boolean,
|
opened: boolean,
|
||||||
close: () => void,
|
close: () => void,
|
||||||
refetch: () => void,
|
refetch: () => void,
|
||||||
|
@ -20,6 +23,8 @@ export const MoveEventListSlotEntryModal = ({opened, close, refetch, entry}: {
|
||||||
|
|
||||||
const [selectedList, setSelectedList] = useState<string | null>(entry.eventList)
|
const [selectedList, setSelectedList] = useState<string | null>(entry.eventList)
|
||||||
|
|
||||||
|
const [showFullSlots, setShowFullSlots] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSelectedSlot(null)
|
setSelectedSlot(null)
|
||||||
}, [selectedList]);
|
}, [selectedList]);
|
||||||
|
@ -52,17 +57,34 @@ export const MoveEventListSlotEntryModal = ({opened, close, refetch, entry}: {
|
||||||
})
|
})
|
||||||
|
|
||||||
const slotsQuery = useQuery({
|
const slotsQuery = useQuery({
|
||||||
queryKey: ["eventListSlots", entry.eventList, "notFull", selectedList],
|
queryKey: ["eventListSlots", entry.eventList, "notFull", selectedList, showFullSlots],
|
||||||
queryFn: async () => (
|
queryFn: async () => (
|
||||||
await pb.collection("eventListSlotsWithEntriesCount").getFullList({
|
await pb.collection("eventListSlotsWithEntriesCount").getFullList({
|
||||||
filter: `eventList='${selectedList}'
|
filter: `eventList='${selectedList}'
|
||||||
&&(maxEntries=0 || entriesCount < maxEntries)`,
|
${showFullSlots ? "" : "&&(maxEntries=0 || entriesCount < maxEntries)"} `,
|
||||||
sort: "startDate"
|
sort: "startDate"
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
enabled: selectedList !== null && opened
|
enabled: selectedList !== null && opened
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const renderSelectOption: SelectProps['renderOption'] = ({option, checked}) => {
|
||||||
|
|
||||||
|
const slot = slotsQuery.data?.find(slot => slot.id === option.value)
|
||||||
|
|
||||||
|
if (!slot) return <>Not Found</>
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Group flex="1" gap="xs">
|
||||||
|
<Text size={"xs"} fw={checked ? 700 : undefined}>
|
||||||
|
{option.label}
|
||||||
|
</Text>
|
||||||
|
{<SlotProgress slot={slot} compact/>}
|
||||||
|
{checked && <IconCheck style={{marginInlineStart: 'auto'}} size={16}/>}
|
||||||
|
</Group>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return <Modal
|
return <Modal
|
||||||
size={"lg"}
|
size={"lg"}
|
||||||
opened={opened}
|
opened={opened}
|
||||||
|
@ -103,10 +125,18 @@ export const MoveEventListSlotEntryModal = ({opened, close, refetch, entry}: {
|
||||||
value: slot.id,
|
value: slot.id,
|
||||||
label: `${pprintDateTime(slot.startDate)} - ${pprintDateTime(slot.endDate)}`
|
label: `${pprintDateTime(slot.startDate)} - ${pprintDateTime(slot.endDate)}`
|
||||||
})) ?? []}
|
})) ?? []}
|
||||||
|
renderOption={renderSelectOption}
|
||||||
value={selectedSlot}
|
value={selectedSlot}
|
||||||
onChange={(value) => setSelectedSlot(value)}
|
onChange={(value) => setSelectedSlot(value)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<CheckboxCard
|
||||||
|
label={"Zeitslots mit voller Belegung anzeigen"}
|
||||||
|
description={"Achtung: Wenn diese Option aktiviert ist, werden auch Zeitslots zum verschieben angezeigt, die bereits voll belegt sind."}
|
||||||
|
checked={showFullSlots}
|
||||||
|
onChange={() => setShowFullSlots(!showFullSlots)}
|
||||||
|
/>
|
||||||
|
|
||||||
{
|
{
|
||||||
selectedList !== entry.eventList && (
|
selectedList !== entry.eventList && (
|
||||||
<Alert color={"orange"} title={"Achtung!"}>
|
<Alert color={"orange"} title={"Achtung!"}>
|
|
@ -1,17 +1,41 @@
|
||||||
import {EventListSlotsWithEntriesCountModel} from "@/models/EventTypes.ts";
|
import {EventListSlotsWithEntriesCountModel} from "@/models/EventTypes.ts";
|
||||||
import {Badge, Group, Progress, Text, ThemeIcon, Tooltip} from "@mantine/core";
|
import {Badge, Group, Progress, Text, ThemeIcon, Tooltip} from "@mantine/core";
|
||||||
import {IconX} from "@tabler/icons-react";
|
import {IconX} from "@tabler/icons-react";
|
||||||
import classes from "./ListSlotProgress.module.css"
|
import classes from "./SlotProgress.module.css"
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Displays a progress status for the number of entries in a slot.
|
* Displays a progress status for the number of entries in a slot.
|
||||||
* If the slot is unlimited or full, the message will reflect that.
|
* If the slot is unlimited or full, the message will reflect that.
|
||||||
* @param slot The slot to display the progress for
|
* @param slot The slot to display the progress for
|
||||||
*/
|
*/
|
||||||
export default function EventListSlotProgress({slot}: { slot: EventListSlotsWithEntriesCountModel }) {
|
export default function SlotProgress({slot, compact}: {
|
||||||
|
slot: EventListSlotsWithEntriesCountModel,
|
||||||
|
compact?: boolean
|
||||||
|
}) {
|
||||||
|
|
||||||
|
const slotIsUnlimited = slot.maxEntries === 0
|
||||||
|
const slotFull = !slotIsUnlimited && slot.entriesCount >= slot.maxEntries
|
||||||
|
const freeSlots = slotIsUnlimited ? 0 : slot.maxEntries - slot.entriesCount
|
||||||
|
const occupiedSlots = slot.entriesCount
|
||||||
|
|
||||||
|
if (compact) {
|
||||||
|
return <>
|
||||||
|
{slotIsUnlimited ? (
|
||||||
|
<Text c={"green"} size={"xs"}>{slot.entriesCount} Anmeldungen (unbegrenzte Plätze)</Text>) : (
|
||||||
|
slotFull ? (
|
||||||
|
<Text c={"red"} size={"xs"}>Voll - ({slot.maxEntries} Plätze)</Text>
|
||||||
|
) : (
|
||||||
|
<Text c={"green"} size={"xs"}>{freeSlots} Plätze frei
|
||||||
|
({slot.maxEntries} Plätze insgesamt)</Text>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// if there are no max entries, the slot is unlimited
|
// if there are no max entries, the slot is unlimited
|
||||||
if (slot.maxEntries === 0 || slot.maxEntries === null) {
|
if (slotIsUnlimited) {
|
||||||
return (
|
return (
|
||||||
<Group align={"center"} justify={"center"}>
|
<Group align={"center"} justify={"center"}>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
|
@ -32,13 +56,12 @@ export default function EventListSlotProgress({slot}: { slot: EventListSlotsWith
|
||||||
<ThemeIcon size={"sm"} variant={"transparent"} color={"orange"}>
|
<ThemeIcon size={"sm"} variant={"transparent"} color={"orange"}>
|
||||||
<IconX/>
|
<IconX/>
|
||||||
</ThemeIcon>
|
</ThemeIcon>
|
||||||
<Text mt={3} fz="sm" fw={700} c={"orange"}>Alle Plätze <u>{slot.entriesCount}/{slot.maxEntries}</u> belegt</Text>
|
<Text mt={3} fz="sm" fw={700} c={"orange"}>Alle
|
||||||
|
Plätze <u>{slot.entriesCount}/{slot.maxEntries}</u> belegt</Text>
|
||||||
</Group>
|
</Group>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const freeSlots = slot.maxEntries - slot.entriesCount
|
|
||||||
const occupiedSlots = slot.entriesCount
|
|
||||||
|
|
||||||
// if the slot is not full and has a max entry count
|
// if the slot is not full and has a max entry count
|
||||||
return <>
|
return <>
|
|
@ -6,8 +6,9 @@ import {showSuccessNotification} from "@/components/util.tsx";
|
||||||
import {Modal} from "@mantine/core";
|
import {Modal} from "@mantine/core";
|
||||||
import FormInput from "@/components/formUtil/FromInput";
|
import FormInput from "@/components/formUtil/FromInput";
|
||||||
import {getUserName, RenderUserName} from "@/components/users/modals/util.tsx";
|
import {getUserName, RenderUserName} from "@/components/users/modals/util.tsx";
|
||||||
|
import {getListSchemas} from "@/pages/events/util.ts";
|
||||||
|
|
||||||
export const UpdateEventListSlotEntryFormModal = ({opened, close, refetch, entry}: {
|
export const UpdateEntryFormModal = ({opened, close, refetch, entry}: {
|
||||||
opened: boolean,
|
opened: boolean,
|
||||||
close: () => void,
|
close: () => void,
|
||||||
refetch: () => void,
|
refetch: () => void,
|
||||||
|
@ -30,10 +31,7 @@ export const UpdateEventListSlotEntryFormModal = ({opened, close, refetch, entry
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const questionSchemaFields = [
|
const {questionSchema} = getListSchemas(entry)
|
||||||
...entry.entryQuestionSchema?.fields ?? [],
|
|
||||||
...entry.defaultEntryQuestionSchema?.fields ?? [],
|
|
||||||
]
|
|
||||||
|
|
||||||
return <Modal
|
return <Modal
|
||||||
size={"lg"}
|
size={"lg"}
|
||||||
|
@ -44,7 +42,7 @@ export const UpdateEventListSlotEntryFormModal = ({opened, close, refetch, entry
|
||||||
<PocketBaseErrorAlert error={mutation.error}/>
|
<PocketBaseErrorAlert error={mutation.error}/>
|
||||||
|
|
||||||
<FormInput
|
<FormInput
|
||||||
schema={{fields: questionSchemaFields}}
|
schema={questionSchema}
|
||||||
onSubmit={mutation.mutateAsync}
|
onSubmit={mutation.mutateAsync}
|
||||||
onAbort={close}
|
onAbort={close}
|
||||||
initialData={entry.entryQuestionData ?? undefined}
|
initialData={entry.entryQuestionData ?? undefined}
|
|
@ -8,8 +8,9 @@ import InnerHtml from "@/components/InnerHtml";
|
||||||
import {RenderDateRange} from "./RenderDateRange.tsx";
|
import {RenderDateRange} from "./RenderDateRange.tsx";
|
||||||
import FormInput from "@/components/formUtil/FromInput";
|
import FormInput from "@/components/formUtil/FromInput";
|
||||||
import {getUserName, RenderUserName} from "@/components/users/modals/util.tsx";
|
import {getUserName, RenderUserName} from "@/components/users/modals/util.tsx";
|
||||||
|
import {getListSchemas} from "@/pages/events/util.ts";
|
||||||
|
|
||||||
export const UpdateEventListSlotEntryStatusModal = ({opened, close, refetch, entry}: {
|
export const UpdateEntryStatusModal = ({opened, close, refetch, entry}: {
|
||||||
opened: boolean,
|
opened: boolean,
|
||||||
close: () => void,
|
close: () => void,
|
||||||
refetch: () => void,
|
refetch: () => void,
|
||||||
|
@ -18,11 +19,6 @@ export const UpdateEventListSlotEntryStatusModal = ({opened, close, refetch, ent
|
||||||
|
|
||||||
const {pb} = usePB()
|
const {pb} = usePB()
|
||||||
|
|
||||||
const statusSchemaFields = [
|
|
||||||
...entry.entryStatusSchema?.fields ?? [],
|
|
||||||
...entry.defaultEntryStatusSchema?.fields ?? [],
|
|
||||||
]
|
|
||||||
|
|
||||||
const mutation = useMutation({
|
const mutation = useMutation({
|
||||||
mutationFn: async (values: FieldEntries) => {
|
mutationFn: async (values: FieldEntries) => {
|
||||||
return await pb.collection("eventListSlotEntries").update(entry.id, {
|
return await pb.collection("eventListSlotEntries").update(entry.id, {
|
||||||
|
@ -36,6 +32,8 @@ export const UpdateEventListSlotEntryStatusModal = ({opened, close, refetch, ent
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const {statusSchema} = getListSchemas(entry)
|
||||||
|
|
||||||
return <Modal
|
return <Modal
|
||||||
size={"lg"}
|
size={"lg"}
|
||||||
opened={opened}
|
opened={opened}
|
||||||
|
@ -47,24 +45,18 @@ export const UpdateEventListSlotEntryStatusModal = ({opened, close, refetch, ent
|
||||||
|
|
||||||
<Alert title={`${entry.listName}`} color={"blue"} mb={"sm"}>
|
<Alert title={`${entry.listName}`} color={"blue"} mb={"sm"}>
|
||||||
<RenderDateRange start={new Date(entry.slotStartDate)} end={new Date(entry.slotEndDate)}/>
|
<RenderDateRange start={new Date(entry.slotStartDate)} end={new Date(entry.slotEndDate)}/>
|
||||||
</Alert>
|
|
||||||
|
|
||||||
{(entry.listDescription || entry.slotDescription) && (
|
{(entry.listDescription || entry.slotDescription) && (
|
||||||
<Alert title={"Beschreibung"} color={"blue"} mb={"sm"}>
|
<>
|
||||||
|
<br/>
|
||||||
{entry.listDescription && (
|
{entry.listDescription && <InnerHtml html={entry.listDescription}/>}
|
||||||
<InnerHtml html={entry.listDescription}/>
|
<br/>
|
||||||
|
{entry.slotDescription && <InnerHtml html={entry.slotDescription}/>}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{entry.slotDescription && (
|
|
||||||
<InnerHtml html={entry.slotDescription}/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
|
||||||
|
|
||||||
<FormInput
|
<FormInput
|
||||||
schema={{fields: statusSchemaFields}}
|
schema={statusSchema}
|
||||||
onSubmit={mutation.mutateAsync}
|
onSubmit={mutation.mutateAsync}
|
||||||
onAbort={close}
|
onAbort={close}
|
||||||
initialData={entry.entryStatusData ?? undefined}
|
initialData={entry.entryStatusData ?? undefined}
|
|
@ -9,7 +9,7 @@ import TextEditor from "@/components/input/Editor";
|
||||||
import ShowHelp from "@/components/ShowHelp.tsx";
|
import ShowHelp from "@/components/ShowHelp.tsx";
|
||||||
import {formatDuration} from "@/lib/datetime.ts";
|
import {formatDuration} from "@/lib/datetime.ts";
|
||||||
|
|
||||||
export default function UpsertEventListSlot({list, slot, onSuccess, onAbort}: {
|
export default function UpsertSlot({list, slot, onSuccess, onAbort}: {
|
||||||
list: EventListModel,
|
list: EventListModel,
|
||||||
slot?: EventListSlotModel,
|
slot?: EventListSlotModel,
|
||||||
onSuccess: () => void,
|
onSuccess: () => void,
|
|
@ -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 {IconCheckupList, IconForms, IconLink, IconList, IconUserSearch} from "@tabler/icons-react";
|
||||||
import {ReactNode} from "react";
|
import {ReactNode} from "react";
|
||||||
import {Menu} from "@mantine/core";
|
import {Menu} from "@mantine/core";
|
||||||
import EventListSearch from "./EventListSearch/index.tsx";
|
import EventListSearch from "@/pages/events/e/:eventId/EventLists/Search/index.tsx";
|
||||||
import EventListShare from "@/pages/events/e/:eventId/EventLists/EventListShare.tsx";
|
import ShareEventLists from "@/pages/events/e/:eventId/EventLists/ShareEventLists.tsx";
|
||||||
import EditEventDefaultEntryStatusSchema
|
import EditDefaultEntryStatusSchema
|
||||||
from "@/pages/events/e/:eventId/EventLists/EventListSettings/EditEventDefaultEntryStatusSchema.tsx";
|
from "@/pages/events/e/:eventId/EventLists/GeneralListSettings/EditDefaultEntryStatusSchema.tsx";
|
||||||
import EditEventDefaultEntryQuestionSchema
|
import EditDefaultEntryQuestionSchema
|
||||||
from "@/pages/events/e/:eventId/EventLists/EventListSettings/EditEventDefaultEntryQuestionSchema.tsx";
|
from "@/pages/events/e/:eventId/EventLists/GeneralListSettings/EditDefaultEntryQuestionSchema.tsx";
|
||||||
|
|
||||||
|
|
||||||
const nav = [
|
const nav = [
|
||||||
|
@ -18,17 +18,17 @@ const nav = [
|
||||||
label: "Listenaktionen",
|
label: "Listenaktionen",
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
title: "Übersicht",
|
title: "Listen und Slots",
|
||||||
icon: <IconList size={16}/>,
|
icon: <IconList size={16}/>,
|
||||||
to: "overview"
|
to: "overview"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Durchsuchen",
|
title: "Anmeldungen",
|
||||||
icon: <IconUserSearch size={16}/>,
|
icon: <IconUserSearch size={16}/>,
|
||||||
to: "search"
|
to: "search"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Teilen",
|
title: "Listen Teilen",
|
||||||
icon: <IconLink size={16}/>,
|
icon: <IconLink size={16}/>,
|
||||||
to: "share"
|
to: "share"
|
||||||
}
|
}
|
||||||
|
@ -69,7 +69,7 @@ export const EventListsMenu = ({event, target}: { event: EventModel, target: Rea
|
||||||
|
|
||||||
<Menu.Dropdown>
|
<Menu.Dropdown>
|
||||||
{
|
{
|
||||||
nav.map(({label, children,}, index, array) => <div key={index}>
|
nav.map(({label, children,}, index) => <div key={index}>
|
||||||
<Menu.Label>{label}</Menu.Label>
|
<Menu.Label>{label}</Menu.Label>
|
||||||
{children.map(({title, icon, to}) => (
|
{children.map(({title, icon, to}) => (
|
||||||
<NavLink to={`/events/e/${event.id}/lists/${to}`} key={to}>
|
<NavLink to={`/events/e/${event.id}/lists/${to}`} key={to}>
|
||||||
|
@ -80,7 +80,6 @@ export const EventListsMenu = ({event, target}: { event: EventModel, target: Rea
|
||||||
)}
|
)}
|
||||||
</NavLink>
|
</NavLink>
|
||||||
))}
|
))}
|
||||||
{index !== array.length - 1 && <Menu.Divider/>}
|
|
||||||
</div>)
|
</div>)
|
||||||
}
|
}
|
||||||
</Menu.Dropdown>
|
</Menu.Dropdown>
|
||||||
|
@ -95,10 +94,10 @@ export default function EventListsRouter({event}: { event: EventModel }) {
|
||||||
<Route index element={<Navigate to={"overview"} replace/>}/>
|
<Route index element={<Navigate to={"overview"} replace/>}/>
|
||||||
|
|
||||||
<Route path={"search"} element={<EventListSearch event={event}/>}/>
|
<Route path={"search"} element={<EventListSearch event={event}/>}/>
|
||||||
<Route path={"share"} element={<EventListShare event={event}/>}/>
|
<Route path={"share"} element={<ShareEventLists event={event}/>}/>
|
||||||
|
|
||||||
<Route path={"e/status"} element={<EditEventDefaultEntryStatusSchema event={event}/>}/>
|
<Route path={"e/status"} element={<EditDefaultEntryStatusSchema event={event}/>}/>
|
||||||
<Route path={"e/questions"} element={<EditEventDefaultEntryQuestionSchema event={event}/>}/>
|
<Route path={"e/questions"} element={<EditDefaultEntryQuestionSchema event={event}/>}/>
|
||||||
|
|
||||||
<Route path={"overview"} element={<EventListsOverview event={event}/>}/>
|
<Route path={"overview"} element={<EventListsOverview event={event}/>}/>
|
||||||
<Route path={"overview/:listId/*"} element={<EventListRouter event={event}/>}/>
|
<Route path={"overview/:listId/*"} element={<EventListRouter event={event}/>}/>
|
||||||
|
|
|
@ -14,7 +14,7 @@ import {IconAlertTriangle, IconInfoCircle} from "@tabler/icons-react";
|
||||||
* This component allows the user to edit the default questions for all eventLists of an event.
|
* This component allows the user to edit the default questions for all eventLists of an event.
|
||||||
* @param event The event to edit the default questions for.
|
* @param event The event to edit the default questions for.
|
||||||
*/
|
*/
|
||||||
export default function EditEventDefaultEntryQuestionSchema({event}: { event: EventModel }) {
|
export default function EditDefaultEntryQuestionSchema({event}: { event: EventModel }) {
|
||||||
|
|
||||||
const {pb} = usePB()
|
const {pb} = usePB()
|
||||||
|
|
|
@ -14,7 +14,7 @@ import {IconAlertTriangle} from "@tabler/icons-react";
|
||||||
* This component allows the user to edit the default entry status fields for all eventLists of an event.
|
* This component allows the user to edit the default entry status fields for all eventLists of an event.
|
||||||
* @param event The event to edit the default entry status fields for.
|
* @param event The event to edit the default entry status fields for.
|
||||||
*/
|
*/
|
||||||
export default function EditEventDefaultEntryStatusSchema({event}: { event: EventModel }) {
|
export default function EditDefaultEntryStatusSchema({event}: { event: EventModel }) {
|
||||||
|
|
||||||
const {pb} = usePB()
|
const {pb} = usePB()
|
||||||
|
|
|
@ -19,7 +19,6 @@ export default function ListSelect(props: GenericRecordSearchInputProps<EventLis
|
||||||
recordSearchMutation={
|
recordSearchMutation={
|
||||||
useMutation({
|
useMutation({
|
||||||
mutationFn: async (search: string) => {
|
mutationFn: async (search: string) => {
|
||||||
|
|
||||||
const filter: string[] = []
|
const filter: string[] = []
|
||||||
|
|
||||||
if (search) {
|
if (search) {
|
||||||
|
|
|
@ -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";
|
import {Link} from "react-router-dom";
|
||||||
|
|
||||||
|
|
||||||
export default function EventListShare({event}: { event: EventModel }) {
|
export default function ShareEventLists({event}: { event: EventModel }) {
|
||||||
|
|
||||||
const [selectedLists, setSelectedLists] = useState<EventListModel[]>([])
|
const [selectedLists, setSelectedLists] = useState<EventListModel[]>([])
|
||||||
|
|
|
@ -2,22 +2,23 @@ import {EventListSlotEntriesWithUserModel} from "@/models/EventTypes.ts";
|
||||||
import {ActionIcon, Collapse, Group, Text, ThemeIcon, Tooltip} from "@mantine/core";
|
import {ActionIcon, Collapse, Group, Text, ThemeIcon, Tooltip} from "@mantine/core";
|
||||||
import {IconChevronDown, IconChevronRight, IconConfetti, IconForms, IconList, IconTrash} from "@tabler/icons-react";
|
import {IconChevronDown, IconChevronRight, IconConfetti, IconForms, IconList, IconTrash} from "@tabler/icons-react";
|
||||||
import TextWithIcon from "@/components/layout/TextWithIcon";
|
import TextWithIcon from "@/components/layout/TextWithIcon";
|
||||||
import {
|
|
||||||
EventListSlotEntryDetails
|
|
||||||
} from "@/pages/events/e/:eventId/EventLists/:listId/EventListSlotsTable/EventListSlotEntryRow.tsx";
|
|
||||||
|
|
||||||
import {useDisclosure} from "@mantine/hooks";
|
import {useDisclosure} from "@mantine/hooks";
|
||||||
import {RenderDateRange} from "@/pages/events/e/:eventId/EventLists/components/RenderDateRange.tsx";
|
|
||||||
import classes from "./UserEntryRow.module.css"
|
import classes from "./UserEntryRow.module.css"
|
||||||
import {humanDeltaFromNow} from "@/lib/datetime.ts";
|
import {humanDeltaFromNow} from "@/lib/datetime.ts";
|
||||||
import {useMutation} from "@tanstack/react-query";
|
import {useMutation} from "@tanstack/react-query";
|
||||||
import {showSuccessNotification} from "@/components/util.tsx";
|
import {showSuccessNotification} from "@/components/util.tsx";
|
||||||
import {useConfirmModal} from "@/components/ConfirmModal.tsx";
|
import {useConfirmModal} from "@/components/ConfirmModal.tsx";
|
||||||
import {usePB} from "@/lib/pocketbase.tsx";
|
import {usePB} from "@/lib/pocketbase.tsx";
|
||||||
import {
|
|
||||||
UpdateEventListSlotEntryFormModal
|
|
||||||
} from "@/pages/events/e/:eventId/EventLists/components/UpdateEventListSlotEntryFormModal.tsx";
|
|
||||||
import {getUserName} from "@/components/users/modals/util.tsx";
|
import {getUserName} from "@/components/users/modals/util.tsx";
|
||||||
|
import {UpdateEntryFormModal} from "@/pages/events/e/:eventId/EventLists/EventListComponents/UpdateEntryFormModal.tsx";
|
||||||
|
import {RenderDateRange} from "@/pages/events/e/:eventId/EventLists/EventListComponents/RenderDateRange.tsx";
|
||||||
|
import {
|
||||||
|
EntryQuestionAndStatusData
|
||||||
|
} from "@/pages/events/e/:eventId/EventLists/EventListComponents/EntryQuestionAndStatusData.tsx";
|
||||||
|
|
||||||
|
|
||||||
export default function UserEntryRow({entry, refetch}: {
|
export default function UserEntryRow({entry, refetch}: {
|
||||||
entry: EventListSlotEntriesWithUserModel,
|
entry: EventListSlotEntriesWithUserModel,
|
||||||
|
@ -51,7 +52,7 @@ export default function UserEntryRow({entry, refetch}: {
|
||||||
|
|
||||||
<ConfirmModal/>
|
<ConfirmModal/>
|
||||||
|
|
||||||
<UpdateEventListSlotEntryFormModal
|
<UpdateEntryFormModal
|
||||||
opened={showEditFormModal}
|
opened={showEditFormModal}
|
||||||
close={showEditFormModalHandler.close}
|
close={showEditFormModalHandler.close}
|
||||||
refetch={refetch}
|
refetch={refetch}
|
||||||
|
@ -123,7 +124,7 @@ export default function UserEntryRow({entry, refetch}: {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Collapse in={expanded}>
|
<Collapse in={expanded}>
|
||||||
<EventListSlotEntryDetails entry={entry}/>
|
<EntryQuestionAndStatusData entry={entry}/>
|
||||||
</Collapse>
|
</Collapse>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
|
@ -1,7 +1,5 @@
|
||||||
import {EventListModel, EventListSlotsWithEntriesCountModel} from "@/models/EventTypes.ts";
|
import {EventListModel, EventListSlotsWithEntriesCountModel} from "@/models/EventTypes.ts";
|
||||||
import classes from "./EventListSlotView.module.css";
|
import classes from "./EventListSlotView.module.css";
|
||||||
import {RenderDateRange} from "@/pages/events/e/:eventId/EventLists/components/RenderDateRange.tsx";
|
|
||||||
import EventListSlotProgress from "@/pages/events/e/:eventId/EventLists/components/EventListSlotProgress.tsx";
|
|
||||||
import {useDisclosure} from "@mantine/hooks";
|
import {useDisclosure} from "@mantine/hooks";
|
||||||
import {Alert, Collapse, ThemeIcon, Tooltip, UnstyledButton} from "@mantine/core";
|
import {Alert, Collapse, ThemeIcon, Tooltip, UnstyledButton} from "@mantine/core";
|
||||||
import {IconChevronDown, IconChevronRight, IconInfoCircle} from "@tabler/icons-react";
|
import {IconChevronDown, IconChevronRight, IconInfoCircle} from "@tabler/icons-react";
|
||||||
|
@ -12,25 +10,22 @@ import {showSuccessNotification} from "@/components/util.tsx";
|
||||||
import InnerHtml from "@/components/InnerHtml";
|
import InnerHtml from "@/components/InnerHtml";
|
||||||
import FormInput from "@/components/formUtil/FromInput";
|
import FormInput from "@/components/formUtil/FromInput";
|
||||||
import {useNavigate} from "react-router-dom";
|
import {useNavigate} from "react-router-dom";
|
||||||
|
import {getListSchemas} from "@/pages/events/util.ts";
|
||||||
|
import {RenderDateRange} from "@/pages/events/e/:eventId/EventLists/EventListComponents/RenderDateRange.tsx";
|
||||||
|
import SlotProgress from "@/pages/events/e/:eventId/EventLists/EventListComponents/SlotProgress.tsx";
|
||||||
|
|
||||||
export default function EventListSlotView({slot, list, refetch}: {
|
export default function EventListSlotView({slot, list, refetch}: {
|
||||||
list: EventListModel,
|
list: EventListModel,
|
||||||
slot: EventListSlotsWithEntriesCountModel,
|
slot: EventListSlotsWithEntriesCountModel,
|
||||||
refetch: () => void
|
refetch: () => void
|
||||||
}) {
|
}) {
|
||||||
|
|
||||||
const [expanded, expandedHandler] = useDisclosure(false)
|
const [expanded, expandedHandler] = useDisclosure(false)
|
||||||
|
|
||||||
const slotIsFull = slot.maxEntries === 0 || slot.maxEntries === null ? false : slot.entriesCount >= slot.maxEntries
|
const slotIsFull = slot.maxEntries === 0 || slot.maxEntries === null ? false : slot.entriesCount >= slot.maxEntries
|
||||||
|
|
||||||
const slotIsInPast = new Date(slot.endDate) < new Date()
|
const slotIsInPast = new Date(slot.endDate) < new Date()
|
||||||
|
|
||||||
const schema = {
|
const {questionSchema} = getListSchemas(slot)
|
||||||
fields: [
|
|
||||||
...slot.entryQuestionSchema?.fields ?? [],
|
|
||||||
...slot.defaultEntryQuestionSchema?.fields ?? [],
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
const {pb, user} = usePB()
|
const {pb, user} = usePB()
|
||||||
|
|
||||||
|
@ -64,7 +59,7 @@ export default function EventListSlotView({slot, list, refetch}: {
|
||||||
|
|
||||||
<RenderDateRange start={new Date(slot.startDate)} end={new Date(slot.endDate)}/>
|
<RenderDateRange start={new Date(slot.startDate)} end={new Date(slot.endDate)}/>
|
||||||
|
|
||||||
<EventListSlotProgress slot={slot}/>
|
<SlotProgress slot={slot}/>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</UnstyledButton>
|
</UnstyledButton>
|
||||||
|
@ -97,7 +92,7 @@ export default function EventListSlotView({slot, list, refetch}: {
|
||||||
:
|
:
|
||||||
<FormInput
|
<FormInput
|
||||||
disabled={!user || slotIsFull}
|
disabled={!user || slotIsFull}
|
||||||
schema={schema}
|
schema={questionSchema}
|
||||||
onSubmit={createEntryMutation.mutateAsync}
|
onSubmit={createEntryMutation.mutateAsync}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,8 @@
|
||||||
import {EventModel} from "@/models/EventTypes.ts";
|
import {
|
||||||
|
EventListSlotEntriesWithUserModel,
|
||||||
|
EventListSlotsWithEntriesCountModel,
|
||||||
|
EventModel
|
||||||
|
} from "@/models/EventTypes.ts";
|
||||||
import {usePB} from "@/lib/pocketbase.tsx";
|
import {usePB} from "@/lib/pocketbase.tsx";
|
||||||
import {useSettings} from "@/lib/settings.ts";
|
import {useSettings} from "@/lib/settings.ts";
|
||||||
import {LdapGroupModel} from "@/models/AuthTypes.ts";
|
import {LdapGroupModel} from "@/models/AuthTypes.ts";
|
||||||
|
@ -21,3 +25,27 @@ export const useEventRights = (event?: EventModel) => {
|
||||||
canEditEventList: false
|
canEditEventList: false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the schemas for the entry question and status
|
||||||
|
*
|
||||||
|
* @param slot The slot to get the schemas for
|
||||||
|
*/
|
||||||
|
export const getListSchemas = (slot: EventListSlotEntriesWithUserModel | EventListSlotsWithEntriesCountModel) => {
|
||||||
|
|
||||||
|
|
||||||
|
return {
|
||||||
|
questionSchema: {
|
||||||
|
fields: [
|
||||||
|
...slot.entryQuestionSchema?.fields ?? [],
|
||||||
|
...(!slot.ignoreDefaultEntryQuestionScheme ? slot.defaultEntryQuestionSchema?.fields ?? [] : []),
|
||||||
|
]
|
||||||
|
},
|
||||||
|
statusSchema: {
|
||||||
|
fields: [
|
||||||
|
...slot.entryStatusSchema?.fields ?? [],
|
||||||
|
...(!slot.ignoreDefaultEntryStatusSchema ? slot.defaultEntryStatusSchema?.fields ?? [] : []),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,10 +1,12 @@
|
||||||
import {useShowDebug} from "@/components/ShowDebug.tsx";
|
import {useShowDebug} from "@/components/ShowDebug.tsx";
|
||||||
import {ActionIcon, Alert, Group, LoadingOverlay, Text, TextInput, Title} from "@mantine/core";
|
import {ActionIcon, Alert, Group, LoadingOverlay, Pagination, Text, TextInput, Title} from "@mantine/core";
|
||||||
import {useForm} from "@mantine/form";
|
import {useForm} from "@mantine/form";
|
||||||
import {useQuery} from "@tanstack/react-query";
|
import {useQuery} from "@tanstack/react-query";
|
||||||
import {PocketBaseErrorAlert, usePB} from "@/lib/pocketbase.tsx";
|
import {PocketBaseErrorAlert, usePB} from "@/lib/pocketbase.tsx";
|
||||||
import {CodeHighlight} from "@mantine/code-highlight";
|
import {CodeHighlight} from "@mantine/code-highlight";
|
||||||
import {IconRefresh} from "@tabler/icons-react";
|
import {IconRefresh} from "@tabler/icons-react";
|
||||||
|
import {RecordListOptions} from "pocketbase";
|
||||||
|
import {useState} from "react";
|
||||||
|
|
||||||
export default function DebugPage() {
|
export default function DebugPage() {
|
||||||
|
|
||||||
|
@ -13,6 +15,8 @@ export default function DebugPage() {
|
||||||
|
|
||||||
const {pb} = usePB()
|
const {pb} = usePB()
|
||||||
|
|
||||||
|
const [page, setPage] = useState(1)
|
||||||
|
|
||||||
const formValues = useForm({
|
const formValues = useForm({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
collectionName: "",
|
collectionName: "",
|
||||||
|
@ -25,11 +29,16 @@ export default function DebugPage() {
|
||||||
const debugQuery = useQuery({
|
const debugQuery = useQuery({
|
||||||
queryKey: ["debug", formValues.values.collectionName, formValues.values.filter, formValues.values.sort, formValues.values.expand],
|
queryKey: ["debug", formValues.values.collectionName, formValues.values.filter, formValues.values.sort, formValues.values.expand],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
return await pb.collection(formValues.values.collectionName).getList(1, 10, {
|
|
||||||
filter: formValues.values.filter,
|
const options: RecordListOptions = {}
|
||||||
sort: formValues.values.sort,
|
|
||||||
expand: formValues.values.expand
|
if (formValues.values.filter) options["filter"] = formValues.values.filter
|
||||||
})
|
|
||||||
|
if (formValues.values.sort) options["sort"] = formValues.values.sort
|
||||||
|
|
||||||
|
if (formValues.values.expand) options["expand"] = formValues.values.expand
|
||||||
|
|
||||||
|
return await pb.collection(formValues.values.collectionName).getList(page, 10, options)
|
||||||
},
|
},
|
||||||
enabled: formValues.values.collectionName !== ""
|
enabled: formValues.values.collectionName !== ""
|
||||||
})
|
})
|
||||||
|
@ -79,9 +88,7 @@ export default function DebugPage() {
|
||||||
{debugQuery.data.totalItems} Ergebniss(e)
|
{debugQuery.data.totalItems} Ergebniss(e)
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Text c={"dimmed"}>
|
<Pagination size={"xs"} value={page} onChange={setPage} total={debugQuery.data.totalPages ?? 1}/>
|
||||||
{debugQuery.data.page}/{debugQuery.data.totalPages} Seiten
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
onClick={() => debugQuery.refetch()}
|
onClick={() => debugQuery.refetch()}
|
||||||
|
@ -96,6 +103,7 @@ export default function DebugPage() {
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<CodeHighlight code={JSON.stringify(debugQuery.data.items, null, 2)} language="json"/>
|
<CodeHighlight code={JSON.stringify(debugQuery.data.items, null, 2)} language="json"/>
|
||||||
|
|
||||||
</div>}
|
</div>}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabler-icon {
|
.tabler-icon {
|
||||||
stroke-width: 1.5;
|
stroke-width: 1.2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scrollable {
|
.scrollable {
|
||||||
|
@ -145,6 +145,14 @@ a:hover, a:active, a:visited {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.wrapWords {
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-wrap: break-word; /* Ensures text wraps within the cell */
|
||||||
|
overflow-wrap: break-word; /* Ensures text wraps within the cell */
|
||||||
|
hyphens: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.monospace {
|
.monospace {
|
||||||
font-family: var(--mantine-font-family-monospace);
|
font-family: var(--mantine-font-family-monospace);
|
||||||
}
|
}
|
85
yarn.lock
85
yarn.lock
|
@ -461,68 +461,68 @@
|
||||||
"@jridgewell/resolve-uri" "^3.1.0"
|
"@jridgewell/resolve-uri" "^3.1.0"
|
||||||
"@jridgewell/sourcemap-codec" "^1.4.14"
|
"@jridgewell/sourcemap-codec" "^1.4.14"
|
||||||
|
|
||||||
"@mantine/code-highlight@^7.8.0":
|
"@mantine/code-highlight@^7.10.0":
|
||||||
version "7.8.0"
|
version "7.10.0"
|
||||||
resolved "https://registry.yarnpkg.com/@mantine/code-highlight/-/code-highlight-7.8.0.tgz#de91ef30224ae95d40c4a1b3ecf68f8c89772d50"
|
resolved "https://registry.yarnpkg.com/@mantine/code-highlight/-/code-highlight-7.10.0.tgz#189c0c109168a1b2aeab0069a6376b6538e827db"
|
||||||
integrity sha512-AeuOG5TuKPOv2ifHrvwlOfNCaFpDdSwXCKq336oFBUm3Jq3B3pK+pVn3ZnX+6ccMQHwsHf/Lyv2muWHG54IEDA==
|
integrity sha512-7dep/kbqdDihCz3RYM5/q28I8M6aLgcZl0iQQN04zP12ZDgqWJ5NiyiNUodZCF4H34X/7XtQdz+cxVesJffXqw==
|
||||||
dependencies:
|
dependencies:
|
||||||
clsx "2.1.0"
|
clsx "^2.1.1"
|
||||||
highlight.js "^11.9.0"
|
highlight.js "^11.9.0"
|
||||||
|
|
||||||
"@mantine/core@^7.8.0":
|
"@mantine/core@^7.10.0":
|
||||||
version "7.8.0"
|
version "7.10.0"
|
||||||
resolved "https://registry.yarnpkg.com/@mantine/core/-/core-7.8.0.tgz#b4bbd82ea2f1a25f5fb3d11ae5583cf80ecd8383"
|
resolved "https://registry.yarnpkg.com/@mantine/core/-/core-7.10.0.tgz#bfaafc92cf2346e5a6cbb49289f577ce3f7c05f7"
|
||||||
integrity sha512-19RKuNdJ/s8pZjy2w2rvTsl4ybi/XM6vf+Kc0WY7kpLFCvdG+/UxNi1MuJF8t2Zs0QSFeb/H5yZQNe0XPbegHw==
|
integrity sha512-hNqhdn/+4x8+FDWzR5fu1eMgnG1Mw4fZHw4WjIYjKrSv0NeKHY263RiesZz8RwcUQ8r7LlD95/2tUOMnKVTV5Q==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@floating-ui/react" "^0.26.9"
|
"@floating-ui/react" "^0.26.9"
|
||||||
clsx "2.1.0"
|
clsx "^2.1.1"
|
||||||
react-number-format "^5.3.1"
|
react-number-format "^5.3.1"
|
||||||
react-remove-scroll "^2.5.7"
|
react-remove-scroll "^2.5.7"
|
||||||
react-textarea-autosize "8.5.3"
|
react-textarea-autosize "8.5.3"
|
||||||
type-fest "^4.12.0"
|
type-fest "^4.12.0"
|
||||||
|
|
||||||
"@mantine/dates@^7.8.0":
|
"@mantine/dates@^7.10.0":
|
||||||
version "7.8.0"
|
version "7.10.0"
|
||||||
resolved "https://registry.yarnpkg.com/@mantine/dates/-/dates-7.8.0.tgz#a8785030000487158e1bd23655ea26245bbf299a"
|
resolved "https://registry.yarnpkg.com/@mantine/dates/-/dates-7.10.0.tgz#0c2a02883d5fb4a36b40a578b26ef5a697c333e5"
|
||||||
integrity sha512-9jjiYMwP3jQOpOLKkjhp9uf2BGhtEbOnOzyAlpLOS0CJJlYtB0tO6dJ3JaogrOZ/Yfee7ZUBgouCG5EkR4/qtQ==
|
integrity sha512-LBBh1U/RzxFQKGA6sSYxbCwYEMoM5lNIhwofY6g8zOTAZuRQqo5FIWItmB9I9ltT+M2o75SADeP6ZBLi4ec8ZA==
|
||||||
dependencies:
|
dependencies:
|
||||||
clsx "2.1.0"
|
clsx "^2.1.1"
|
||||||
|
|
||||||
"@mantine/form@^7.8.0":
|
"@mantine/form@^7.10.0":
|
||||||
version "7.8.0"
|
version "7.10.0"
|
||||||
resolved "https://registry.yarnpkg.com/@mantine/form/-/form-7.8.0.tgz#3f16b2e0124c65286892ed50181d192ae03d988b"
|
resolved "https://registry.yarnpkg.com/@mantine/form/-/form-7.10.0.tgz#3e8e3fb836948becb13b89412c74016b50bac3d3"
|
||||||
integrity sha512-Qn3/69zGt/p3wyMwGz2V0+FbmvqC2/PvXaeyO0a4CnwhROeE7ObyCKXDcBmgapOSBHr/7wFvMeTDMaTMfe3DXw==
|
integrity sha512-ChAtqdQCAZrnH6iiCivumyMuMsev+tFWIgsCCgAmbP2sOyMtjbNtypKrcwBwI/PzAH9N4jSJlsmJsnRdXNeEkQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
fast-deep-equal "^3.1.3"
|
fast-deep-equal "^3.1.3"
|
||||||
klona "^2.0.6"
|
klona "^2.0.6"
|
||||||
|
|
||||||
"@mantine/hooks@^7.8.0":
|
"@mantine/hooks@^7.10.0":
|
||||||
version "7.8.0"
|
version "7.10.0"
|
||||||
resolved "https://registry.yarnpkg.com/@mantine/hooks/-/hooks-7.8.0.tgz#fc32e07746689459c4b049dc581d1dbda5545686"
|
resolved "https://registry.yarnpkg.com/@mantine/hooks/-/hooks-7.10.0.tgz#10a259e204a8af29df6aeeb24090c1e2c6debca0"
|
||||||
integrity sha512-+70fkgjhVJeJ+nJqnburIM3UAsfvxat1Low9HMPobLbv64FIdB4Nzu5ct3qojNQ58r5sK01tg5UoFIJYslaVrg==
|
integrity sha512-fnalwYS2WQEFS4wmhmAetDZ/VdJPLNeUXPX9t+S21o3p/dRTX1xhU2mS7yWaQUKM0hPD1TcujqXGlP2M2g/A9A==
|
||||||
|
|
||||||
"@mantine/modals@^7.9.0":
|
"@mantine/modals@^7.10.0":
|
||||||
version "7.9.0"
|
version "7.10.0"
|
||||||
resolved "https://registry.yarnpkg.com/@mantine/modals/-/modals-7.9.0.tgz#aedfb29de1630fc0498aebf5d3c17acfae14321b"
|
resolved "https://registry.yarnpkg.com/@mantine/modals/-/modals-7.10.0.tgz#c08789491bfbfb1d432818e0fc4b2eac71fd480e"
|
||||||
integrity sha512-shPoUmuAj6587DbGjc3AWpGRnxDvHftw7Ps7Xqcoy7ScxQ8+HpynBk9j7ojyXMolovK4hThW4oZdysPfpjQyaA==
|
integrity sha512-UVtmRpTBWDqcJjdv97IUYLduYcZBrqteyDwnspHT453iFZlvCglHUXYR+LvN5ExE+kxUe2IUXL/pEaIRTjwtKQ==
|
||||||
|
|
||||||
"@mantine/notifications@^7.8.1":
|
"@mantine/notifications@^7.10.0":
|
||||||
version "7.8.1"
|
version "7.10.0"
|
||||||
resolved "https://registry.yarnpkg.com/@mantine/notifications/-/notifications-7.8.1.tgz#ce2a9e97753a4f18a5e7b1822c3be42bda43fd85"
|
resolved "https://registry.yarnpkg.com/@mantine/notifications/-/notifications-7.10.0.tgz#aa638b8bb6c3d6bfb34d518a49ef8a8b6ab499e4"
|
||||||
integrity sha512-HpjZs45EmBYYmJj72+hbwLVxbO7nbZrkpjcrYc40/VIbcDjjJBXDZrAX4VLu7ZIjkbeoxxPgxp245XheC68ikA==
|
integrity sha512-3a0mmM9Kr3nPP+8VHsIuly507nda6ciu2aB/xSxb7gFIKHw3GqSu77pxXa+5l4Y6AQKKvP9360K4KjH6+rOBWw==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@mantine/store" "7.8.1"
|
"@mantine/store" "7.10.0"
|
||||||
react-transition-group "4.4.5"
|
react-transition-group "4.4.5"
|
||||||
|
|
||||||
"@mantine/store@7.8.1":
|
"@mantine/store@7.10.0":
|
||||||
version "7.8.1"
|
version "7.10.0"
|
||||||
resolved "https://registry.yarnpkg.com/@mantine/store/-/store-7.8.1.tgz#75edadd8b42491466f681f1d23de528826b7cdff"
|
resolved "https://registry.yarnpkg.com/@mantine/store/-/store-7.10.0.tgz#68368c6ca5b75cfb331220e06a3235be753df055"
|
||||||
integrity sha512-lX1l8zPd7LazenTKqmI/E17BssRLpf/c8aKYiwV1ZgNONWc+XlWr+6MW3GLKB5uM/DS1Zg0bAYAaRUtKGP1MIQ==
|
integrity sha512-B6AyUX0cA97/hI9v0att7eJJnQTcUG7zBlTdWhOsptBV5UoDNrzdv3DDWIFxrA8h+nhNKGBh6Dif5HWh1+QLeA==
|
||||||
|
|
||||||
"@mantine/tiptap@^7.8.0":
|
"@mantine/tiptap@^7.10.0":
|
||||||
version "7.8.0"
|
version "7.10.0"
|
||||||
resolved "https://registry.yarnpkg.com/@mantine/tiptap/-/tiptap-7.8.0.tgz#390eb44659bf01d898335f6063c3d6231bb8e756"
|
resolved "https://registry.yarnpkg.com/@mantine/tiptap/-/tiptap-7.10.0.tgz#77926f0a2d81c05e3f4084e65786778d5227fa56"
|
||||||
integrity sha512-0O766+DZ8/L2VX//7GKUCfOiMyC7lJXfLmMNqQdth2JtlGoDzWDAF7hhQos0hBor+rT7ci3gPLVMv39l2U0CQw==
|
integrity sha512-C8wURpoh1dduWPgGgyknVc+E9/gDZVOMIyPxZXNx/r74/OoaE8tu8tgF/T21t8DtCQ4jter0PEGZFDB9hIXuag==
|
||||||
|
|
||||||
"@nodelib/fs.scandir@2.1.5":
|
"@nodelib/fs.scandir@2.1.5":
|
||||||
version "2.1.5"
|
version "2.1.5"
|
||||||
|
@ -1396,11 +1396,6 @@ character-reference-invalid@^2.0.0:
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
fsevents "~2.3.2"
|
fsevents "~2.3.2"
|
||||||
|
|
||||||
clsx@2.1.0:
|
|
||||||
version "2.1.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.0.tgz#e851283bcb5c80ee7608db18487433f7b23f77cb"
|
|
||||||
integrity sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==
|
|
||||||
|
|
||||||
clsx@^1.2.1:
|
clsx@^1.2.1:
|
||||||
version "1.2.1"
|
version "1.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12"
|
resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12"
|
||||||
|
|
Loading…
Reference in New Issue