Compare commits

..

12 Commits

Author SHA1 Message Date
fdd093b8aa feat(ui): Verbesserung von CreateFilterModal mit Validierung und dynamischer Filtererstellung
- Validierung für `name` und `type` vor der Filtererstellung hinzugefügt
- Verbinden der Felder `label`, `name` und `type` mit dem Formularstatus
- Hardcodierte Werte im Aufruf `createFiltersAsync` durch Benutzereingaben ersetzt
- Einführung der Funktion `tryCreateFilter` für eine bessere Struktur
- Falsche Verwendung von `Name` und `Label` behoben
2025-07-09 10:38:13 +02:00
4056719b50 refactor(create-filter-modal): add useStates to name and label text-fields 2025-07-09 01:29:31 +02:00
dbea5cbeec refactor(create-filter-model): add combobox for types and text fields for label and name 2025-07-09 01:27:35 +02:00
ae5c9908bb feat(filter-service): add create method.
- create FilterCreateDto
2025-07-09 00:17:27 +02:00
89adc16a0e feat(create-filter-model): Erstellen ohne Komponente.
- Use-States hinzufügen
2025-07-08 23:44:01 +02:00
f340130f89 refactor(doc-search-view): Gitter zu Filtern hinzufügen 2025-07-07 13:51:02 +02:00
593f4deb3e refactor(doc-search-view): Hinzufügen eines Fehlerprotokolls unter der Standard-Fallanweisung. 2025-07-07 13:25:17 +02:00
042a1a76c3 feat(date-filter): Hinzufügen von Zeit- und DateTime-Filtern 2025-07-07 13:24:10 +02:00
cf67c387c6 refactor(Typ): Hinzufügen der Typen ‚TIME‘ und ‚DATETIME‘.
- Hinzufügen von Mock-Daten für neue Typen
2025-07-07 11:17:51 +02:00
b8e2100331 unnötige useState und useEffect entfernen 2025-07-07 11:07:51 +02:00
66ab925b5d feat(date-filter): Hinzufügen, um DATE-Datentyp zu behandeln 2025-07-07 11:01:38 +02:00
29e033b8de feat(TextFilter): Hinzufügen zur Behandlung von VARCHAR 2025-07-07 10:35:07 +02:00
10 changed files with 343 additions and 53 deletions

View File

@ -17,6 +17,7 @@
"@mui/icons-material": "^7.2.0",
"@mui/lab": "^7.0.0-beta.10",
"@mui/material": "^7.0.1",
"@mui/x-date-pickers": "^8.7.0",
"apexcharts": "^4.5.0",
"dayjs": "^1.11.13",
"es-toolkit": "^1.34.1",
@ -1438,6 +1439,94 @@
}
}
},
"node_modules/@mui/x-date-pickers": {
"version": "8.7.0",
"resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-8.7.0.tgz",
"integrity": "sha512-7fCRhhoE/2s7wsJWLoY2IoHlN5ZA+ev7ZzhIjLPAOzMXwIflzCgljq6iG/iXpATugsmlxWHhO/7wdDSD6zUNOw==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.27.6",
"@mui/utils": "^7.1.1",
"@mui/x-internals": "8.7.0",
"@types/react-transition-group": "^4.4.12",
"clsx": "^2.1.1",
"prop-types": "^15.8.1",
"react-transition-group": "^4.4.5"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@emotion/react": "^11.9.0",
"@emotion/styled": "^11.8.1",
"@mui/material": "^5.15.14 || ^6.0.0 || ^7.0.0",
"@mui/system": "^5.15.14 || ^6.0.0 || ^7.0.0",
"date-fns": "^2.25.0 || ^3.2.0 || ^4.0.0",
"date-fns-jalali": "^2.13.0-0 || ^3.2.0-0 || ^4.0.0-0",
"dayjs": "^1.10.7",
"luxon": "^3.0.2",
"moment": "^2.29.4",
"moment-hijri": "^2.1.2 || ^3.0.0",
"moment-jalaali": "^0.7.4 || ^0.8.0 || ^0.9.0 || ^0.10.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/react": {
"optional": true
},
"@emotion/styled": {
"optional": true
},
"date-fns": {
"optional": true
},
"date-fns-jalali": {
"optional": true
},
"dayjs": {
"optional": true
},
"luxon": {
"optional": true
},
"moment": {
"optional": true
},
"moment-hijri": {
"optional": true
},
"moment-jalaali": {
"optional": true
}
}
},
"node_modules/@mui/x-internals": {
"version": "8.7.0",
"resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-8.7.0.tgz",
"integrity": "sha512-1aduds7L2i6t0HIFNlqG4UB07SVEg+wcnJ9GGu8B/X8EVwO72Rt+rc8ZlqK10ooscq1AlTwi2dd0q+hz+aWk+Q==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.27.6",
"@mui/utils": "^7.1.1",
"reselect": "^5.1.1"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@mui/system": "^5.15.14 || ^6.0.0 || ^7.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/@napi-rs/wasm-runtime": {
"version": "0.2.8",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.8.tgz",
@ -5743,6 +5832,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/reselect": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
"integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==",
"license": "MIT"
},
"node_modules/resolve": {
"version": "1.22.10",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",

View File

@ -35,6 +35,7 @@
"@mui/icons-material": "^7.2.0",
"@mui/lab": "^7.0.0-beta.10",
"@mui/material": "^7.0.1",
"@mui/x-date-pickers": "^8.7.0",
"apexcharts": "^4.5.0",
"dayjs": "^1.11.13",
"es-toolkit": "^1.34.1",

View File

@ -235,5 +235,7 @@ export const _filters: Filter[] = [
{ id: 7, label: 'Höchstbetrag', name: 'maxAmount', type: 'DECIMAL' },
{ id: 8, label: 'Steuer inbegriffen?', name: 'taxIncluded', type: 'BOOLEAN' },
{ id: 9, label: 'Währung', name: 'currency', type: 'VARCHAR' },
{ id: 10, label: 'Erstellungsdatum', name: 'createdAt', type: 'DATE' }
{ id: 10, label: 'Erstellungsdatum', name: 'createdAt', type: 'DATE' },
{ id: 11, label: 'Lieferzeit', name: 'deliveryTime', type: 'TIME' },
{ id: 12, label: 'Letzte Aktualisierung', name: 'lastUpdated', type: 'DATETIME' }
];

View File

@ -1,14 +1,36 @@
import { _filters } from 'src/_mock/_data';
export type Type = 'BOOLEAN' | 'DATE' | 'VARCHAR' | 'INTEGER' | 'DECIMAL';
export type Type = 'BOOLEAN' | 'DATE' | 'TIME' | 'DATETIME' | 'VARCHAR' | 'INTEGER' | 'DECIMAL';
export type Filter = {
id: number;
label?: string;
export const filterTypes: Type[] = [
'BOOLEAN',
'DATE',
'TIME',
'DATETIME',
'VARCHAR',
'INTEGER',
'DECIMAL'
];
export type FilterCreateDto = {
label?: string | undefined;
name: string;
type: Type;
};
export type Filter = FilterCreateDto & {
id: number;
};
export function getFiltersAsync(): Promise<Filter[]> {
return Promise.resolve(_filters);
}
export function createFiltersAsync(filter: FilterCreateDto): Promise<Filter> {
const newFilter: Filter = {
...filter,
id: _filters.length + 1
};
_filters.push(newFilter);
return Promise.resolve(newFilter);
}

View File

@ -0,0 +1,55 @@
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
import { TimePicker } from '@mui/x-date-pickers/TimePicker';
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
import { DateTimePicker } from '@mui/x-date-pickers/DateTimePicker';
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
// ----------------------------------------------------------------------
type DateFilterProps = {
label: string;
}
export function DateFilter({ label }: DateFilterProps) {
return (
<LocalizationProvider dateAdapter={AdapterDayjs}>
<DatePicker
slotProps={{
openPickerIcon: (ownerState: any) => ({
color: ownerState.isPickerOpen ? 'secondary' : 'primary',
}),
}}
label={label}
/>
</LocalizationProvider>
);
}
export function TimeFilter({ label }: DateFilterProps) {
return (
<LocalizationProvider dateAdapter={AdapterDayjs}>
<TimePicker
slotProps={{
openPickerIcon: (ownerState: any) => ({
color: ownerState.isPickerOpen ? 'secondary' : 'primary',
}),
}}
label={label}
/>
</LocalizationProvider>
);
}
export function DateTimeFilter({ label }: DateFilterProps) {
return (
<LocalizationProvider dateAdapter={AdapterDayjs}>
<DateTimePicker
slotProps={{
openPickerIcon: (ownerState: any) => ({
color: ownerState.isPickerOpen ? 'secondary' : 'primary',
}),
}}
label={label}
/>
</LocalizationProvider>
);
}

View File

@ -19,17 +19,9 @@ export function IntFilter({ label }: BoolFilterProps) {
}
};
return (
<TextField label={label} value={val} onChange={onInputChange} />
);
return <TextField label={label} value={val} onChange={onInputChange} variant="filled" />;
}
export function DecimalFilter({ label }: BoolFilterProps) {
return (
<TextField
type="number"
label={label}
variant="standard"
/>
);
return <TextField type="number" label={label} variant="filled" />;
}

View File

@ -0,0 +1,10 @@
import TextField from '@mui/material/TextField';
// ----------------------------------------------------------------------
type TextFilterProps = {
label: string;
}
export function TextFilter({ label }: TextFilterProps) {
return <TextField label={label} variant="filled" />;
}

View File

@ -0,0 +1,76 @@
import { useState } from 'react';
import Box from '@mui/material/Box';
import Modal from '@mui/material/Modal';
import Button from '@mui/material/Button';
import TextField from '@mui/material/TextField';
import Autocomplete from '@mui/material/Autocomplete';
import { Iconify } from 'src/components/iconify/iconify';
import { createFiltersAsync, FilterCreateDto, filterTypes, Type } from '../../../api/filter-service';
const style = {
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: 400,
bgcolor: 'background.paper',
boxShadow: 24,
p: 4,
};
type ModalProps = {
open: boolean;
handleClose: () => void;
}
export default function CreateFilterModal({ open, handleClose }: ModalProps) {
const [name, setName] = useState<string | undefined>(undefined);
const [label, setLabel] = useState<string | undefined>(undefined);
const [selectedType, setSelectedType] = useState<Type | undefined>(undefined);
async function tryCreateFilter(): Promise<any> {
if (!name) {
alert('No name.');
}
else if (!selectedType) {
alert('No type.');
}
else {
await createFiltersAsync({ name: name, type: selectedType, label: label }).then(() => handleClose());
}
}
return (
<div>
<Modal
open={open}
onClose={handleClose}
aria-labelledby="modal-modal-title"
aria-describedby="modal-modal-description"
>
<Box sx={style}>
<TextField label="Label" variant="filled" value={name} onChange={e => setName(e.target.value)} />
<TextField label="Name" variant="filled" value={label} onChange={e => setLabel(e.target.value)} />
<Autocomplete
disablePortal
options={filterTypes}
value={selectedType}
onChange={(event, newValue) => setSelectedType(newValue ?? undefined)}
renderInput={params => <TextField {...params} label="Type" variant="filled" />}
/>
<Button
variant="contained"
color="inherit"
startIcon={<Iconify icon="mingcute:add-line" />}
onClick={() => tryCreateFilter()}
>
Add filter
</Button>
</Box>
</Modal>
</div>
);
}

View File

@ -1,10 +1,8 @@
import React from 'react';
import { useState, useCallback, useEffect } from 'react';
import Box from '@mui/material/Box';
import Grid from '@mui/material/Grid';
import Button from '@mui/material/Button';
import TextField from '@mui/material/TextField';
import Typography from '@mui/material/Typography';
import Pagination from '@mui/material/Pagination';
@ -15,7 +13,10 @@ import { Iconify } from 'src/components/iconify';
import { DocItem } from '../doc-item';
import { BoolFilter } from '../bool-filter';
import { TextFilter } from '../text-filter';
import CreateFilterModal from './create-filter-modal';
import { DecimalFilter, IntFilter } from '../num-filter';
import { DateFilter, DateTimeFilter, TimeFilter } from '../date-filter';
import type { IDocItem } from '../doc-item';
// ----------------------------------------------------------------------
@ -29,15 +30,6 @@ export function DocSearchView({ posts }: Props) {
const [filters, setFilters] = useState<Filter[]>([])
const [disabledStates, setDisabledStates] = useState<Record<number, boolean>>({});
const setDisabledState = useCallback((index: number, state: boolean) => {
setDisabledStates(prev => ({
...prev,
[index]: state,
}));
}, []);
const handleSort = useCallback((newSort: string) => {
setSortBy(newSort);
}, []);
@ -45,16 +37,11 @@ export function DocSearchView({ posts }: Props) {
useEffect(() => {
getFiltersAsync().then((res) => {
setFilters(res);
const newDisabledStates = res.reduce<Record<number, boolean>>((acc, filter, index) => {
if (filter.type === 'BOOLEAN') {
acc[index] = true;
}
return acc;
}, {});
setDisabledStates(newDisabledStates);
});
}, []);
const [openModal, setOpenModal] = useState(false);
//#region example components
// <Box
// sx={{
@ -94,13 +81,17 @@ export function DocSearchView({ posts }: Props) {
variant="contained"
color="inherit"
startIcon={<Iconify icon="mingcute:add-line" />}
onClick={() => setOpenModal(true)}
>
New filter
</Button>
</Box>
<>
<CreateFilterModal open={openModal} handleClose={() => setOpenModal(false)} />
<Grid container spacing={3}>
{filters.map((filter, index) => {
let filterComp;
switch (filter.type) {
case 'BOOLEAN':
@ -113,26 +104,45 @@ export function DocSearchView({ posts }: Props) {
filterComp = <DecimalFilter label={filter.label ?? filter.name} />
break;
case 'VARCHAR':
case 'DATE':
default:
filterComp = <TextField id={`filter-${filter.id.toString()}`} label={filter.label ?? filter.type} variant="filled" />
filterComp = <TextFilter label={filter.label ?? filter.name} />
break;
case 'DATE':
filterComp = <DateFilter label={filter.label ?? filter.name} />
break;
case 'TIME':
filterComp = <TimeFilter label={filter.label ?? filter.name} />
break;
case 'DATETIME':
filterComp = <DateTimeFilter label={filter.label ?? filter.name} />
break;
default:
console.error(`Unknown filter type: ${filter.type}`);
}
return (
<Box
sx={{
mb: 5,
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
<Grid
key={filter.id}
size={{
xs: 12,
sm: 6,
md: 3,
}}
>
{filterComp}
</Box>
)
<Box
sx={{
mb: 5,
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
}}
>
{filterComp}
</Box>
</Grid>
);
}
)}
</>
</Grid>
<Grid container spacing={3}>
{posts.map((post, index) => {

View File

@ -127,7 +127,7 @@
resolved "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz"
integrity sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==
"@emotion/react@^11.0.0-rc.0", "@emotion/react@^11.14.0", "@emotion/react@^11.4.1", "@emotion/react@^11.5.0":
"@emotion/react@^11.0.0-rc.0", "@emotion/react@^11.14.0", "@emotion/react@^11.4.1", "@emotion/react@^11.5.0", "@emotion/react@^11.9.0":
version "11.14.0"
resolved "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz"
integrity sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==
@ -157,7 +157,7 @@
resolved "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz"
integrity sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==
"@emotion/styled@^11.14.0", "@emotion/styled@^11.3.0":
"@emotion/styled@^11.14.0", "@emotion/styled@^11.3.0", "@emotion/styled@^11.8.1":
version "11.14.0"
resolved "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.0.tgz"
integrity sha512-XxfOnXFffatap2IyCeJyNov3kiDQWoR08gPUQxvbL7fxKryGBKUZUkG6Hz48DZwVrJSVh9sJboyV1Ds4OW6SgA==
@ -373,7 +373,7 @@
clsx "^2.1.1"
prop-types "^15.8.1"
"@mui/material@^7.0.1", "@mui/material@^7.2.0":
"@mui/material@^5.15.14 || ^6.0.0 || ^7.0.0", "@mui/material@^7.0.1", "@mui/material@^7.2.0":
version "7.2.0"
resolved "https://registry.npmjs.org/@mui/material/-/material-7.2.0.tgz"
integrity sha512-NTuyFNen5Z2QY+I242MDZzXnFIVIR6ERxo7vntFi9K1wCgSwvIl0HcAO2OOydKqqKApE6omRiYhpny1ZhGuH7Q==
@ -412,7 +412,7 @@
csstype "^3.1.3"
prop-types "^15.8.1"
"@mui/system@^7.0.1", "@mui/system@^7.2.0":
"@mui/system@^5.15.14 || ^6.0.0 || ^7.0.0", "@mui/system@^7.0.1", "@mui/system@^7.2.0":
version "7.2.0"
resolved "https://registry.npmjs.org/@mui/system/-/system-7.2.0.tgz"
integrity sha512-PG7cm/WluU6RAs+gNND2R9vDwNh+ERWxPkqTaiXQJGIFAyJ+VxhyKfzpdZNk0z0XdmBxxi9KhFOpgxjehf/O0A==
@ -433,7 +433,7 @@
dependencies:
"@babel/runtime" "^7.27.6"
"@mui/utils@^7.0.1", "@mui/utils@^7.2.0":
"@mui/utils@^7.0.1", "@mui/utils@^7.1.1", "@mui/utils@^7.2.0":
version "7.2.0"
resolved "https://registry.npmjs.org/@mui/utils/-/utils-7.2.0.tgz"
integrity sha512-O0i1GQL6MDzhKdy9iAu5Yr0Sz1wZjROH1o3aoztuivdCXqEeQYnEjTDiRLGuFxI9zrUbTHBwobMyQH5sNtyacw==
@ -445,6 +445,28 @@
prop-types "^15.8.1"
react-is "^19.1.0"
"@mui/x-date-pickers@^8.7.0":
version "8.7.0"
resolved "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-8.7.0.tgz"
integrity sha512-7fCRhhoE/2s7wsJWLoY2IoHlN5ZA+ev7ZzhIjLPAOzMXwIflzCgljq6iG/iXpATugsmlxWHhO/7wdDSD6zUNOw==
dependencies:
"@babel/runtime" "^7.27.6"
"@mui/utils" "^7.1.1"
"@mui/x-internals" "8.7.0"
"@types/react-transition-group" "^4.4.12"
clsx "^2.1.1"
prop-types "^15.8.1"
react-transition-group "^4.4.5"
"@mui/x-internals@8.7.0":
version "8.7.0"
resolved "https://registry.npmjs.org/@mui/x-internals/-/x-internals-8.7.0.tgz"
integrity sha512-1aduds7L2i6t0HIFNlqG4UB07SVEg+wcnJ9GGu8B/X8EVwO72Rt+rc8ZlqK10ooscq1AlTwi2dd0q+hz+aWk+Q==
dependencies:
"@babel/runtime" "^7.27.6"
"@mui/utils" "^7.1.1"
reselect "^5.1.1"
"@nodelib/fs.scandir@2.1.5":
version "2.1.5"
resolved "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz"
@ -1012,7 +1034,7 @@ data-view-byte-offset@^1.0.1:
es-errors "^1.3.0"
is-data-view "^1.0.1"
dayjs@^1.11.13:
dayjs@^1.10.7, dayjs@^1.11.13:
version "1.11.13"
resolved "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz"
integrity sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==
@ -2381,6 +2403,11 @@ regexp.prototype.flags@^1.5.3:
gopd "^1.2.0"
set-function-name "^2.0.2"
reselect@^5.1.1:
version "5.1.1"
resolved "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz"
integrity sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==
resolve-from@^4.0.0:
version "4.0.0"
resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz"