mirror of
https://github.com/orangecoding/fredy.git
synced 2026-06-16 12:31:07 +00:00
Feature/spec filter (#276)
* feat(): create map component, add area filtering to the job config * feat(): filter listings by area filter * chore(): cleanup * feat(): solve feedback * feat(): solve most providers * feat(): solve maybe other providers * feat(): add specFilter config, also add rooms to listing * feat(): change tests * feat(): fix kleinanzeigen parser * feat(): add spec filter switch for listing overviiews * feat(): add rooms and size to the overview and detail of a listing * feat(): rem label * feat(): add types, update providers, they now return specs as numbers * feat(): add jsonconfig to enable type checks * feat: add type for prividerConfig, add fieldNames per provider * feat: fix tests, provider, add formatListing * chore: remov duplicates * feat(): fix tests * feat: fix immoscout * chore: geojson typing * feat: solve requested changes
This commit is contained in:
@@ -25,6 +25,7 @@ import {
|
||||
Empty,
|
||||
Radio,
|
||||
RadioGroup,
|
||||
Space,
|
||||
} from '@douyinfe/semi-ui-19';
|
||||
import {
|
||||
IconBriefcase,
|
||||
@@ -293,12 +294,14 @@ const ListingsGrid = () => {
|
||||
>
|
||||
{item.address || 'No address provided'}
|
||||
</Text>
|
||||
<Text type="tertiary" size="small" icon={<IconClock />}>
|
||||
{timeService.format(item.created_at, false)}
|
||||
</Text>
|
||||
<Text type="tertiary" size="small" icon={<IconBriefcase />}>
|
||||
{item.provider.charAt(0).toUpperCase() + item.provider.slice(1)}
|
||||
</Text>
|
||||
<Space spacing={12} wrap>
|
||||
<Text type="tertiary" size="small" icon={<IconBriefcase />}>
|
||||
{item.provider.charAt(0).toUpperCase() + item.provider.slice(1)}
|
||||
</Text>
|
||||
<Text type="tertiary" size="small" icon={<IconClock />}>
|
||||
{timeService.format(item.created_at, false)}
|
||||
</Text>
|
||||
</Space>
|
||||
{item.distance_to_destination ? (
|
||||
<Text type="tertiary" size="small" icon={<IconActivity />}>
|
||||
{item.distance_to_destination} m to chosen address
|
||||
|
||||
@@ -69,6 +69,7 @@
|
||||
}
|
||||
|
||||
&--inactive {
|
||||
|
||||
.listingsGrid__imageContainer,
|
||||
.listingsGrid__content {
|
||||
opacity: 0.6;
|
||||
@@ -169,4 +170,16 @@
|
||||
background: var(--semi-color-primary-hover);
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure icons and text are vertically aligned
|
||||
.semi-typography {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
.semi-typography-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 1px; // Minor nudge if needed, but flex should handle most
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,9 +24,15 @@ import {
|
||||
IconPlayCircle,
|
||||
IconPlusCircle,
|
||||
IconUser,
|
||||
IconClear,
|
||||
IconFilter,
|
||||
} from '@douyinfe/semi-icons';
|
||||
|
||||
const SPEC_FILTERS = [
|
||||
{ key: 'maxPrice', translation: 'Max Price' },
|
||||
{ key: 'minSize', translation: 'Min Size (m²)' },
|
||||
{ key: 'minRooms', translation: 'Min Rooms' },
|
||||
];
|
||||
|
||||
export default function JobMutator() {
|
||||
const jobs = useSelector((state) => state.jobsData.jobs);
|
||||
const shareableUserList = useSelector((state) => state.jobsData.shareableUserList);
|
||||
@@ -46,6 +52,7 @@ export default function JobMutator() {
|
||||
const defaultEnabled = sourceJob?.enabled ?? true;
|
||||
const defaultShareWithUsers = sourceJob?.shared_with_user ?? [];
|
||||
const defaultSpatialFilter = sourceJob?.spatialFilter || null;
|
||||
const defaultSpecFilter = sourceJob?.specFilter || null;
|
||||
|
||||
const [providerToEdit, setProviderToEdit] = useState(null);
|
||||
const [providerCreationVisible, setProviderCreationVisibility] = useState(false);
|
||||
@@ -58,6 +65,7 @@ export default function JobMutator() {
|
||||
const [shareWithUsers, setShareWithUsers] = useState(defaultShareWithUsers);
|
||||
const [enabled, setEnabled] = useState(defaultEnabled);
|
||||
const [spatialFilter, setSpatialFilter] = useState(defaultSpatialFilter);
|
||||
const [specFilter, setSpecFilter] = useState(defaultSpecFilter);
|
||||
const navigate = useNavigate();
|
||||
const actions = useActions();
|
||||
|
||||
@@ -66,6 +74,12 @@ export default function JobMutator() {
|
||||
setSpatialFilter(data);
|
||||
}, []);
|
||||
|
||||
const handleSpecFilterChange = (key, value) => {
|
||||
if (!SPEC_FILTERS.map(({ key }) => key).includes(key)) return;
|
||||
|
||||
setSpecFilter({ ...specFilter, [key]: value ? parseFloat(value) : null });
|
||||
};
|
||||
|
||||
const isSavingEnabled = () => {
|
||||
return Boolean(notificationAdapterData.length && providerData.length && name);
|
||||
};
|
||||
@@ -85,6 +99,7 @@ export default function JobMutator() {
|
||||
name,
|
||||
blacklist,
|
||||
spatialFilter,
|
||||
specFilter,
|
||||
enabled,
|
||||
jobId: jobToBeEdit?.id || null,
|
||||
});
|
||||
@@ -204,7 +219,7 @@ export default function JobMutator() {
|
||||
</SegmentPart>
|
||||
<Divider margin="1rem" />
|
||||
<SegmentPart
|
||||
Icon={IconClear}
|
||||
Icon={IconFilter}
|
||||
name="Blacklist"
|
||||
helpText="If a listing contains one of these words, it will be filtered out. Type in a word, then hit enter."
|
||||
>
|
||||
@@ -216,6 +231,27 @@ export default function JobMutator() {
|
||||
</SegmentPart>
|
||||
<Divider margin="1rem" />
|
||||
<SegmentPart
|
||||
Icon={IconFilter}
|
||||
name="Criteria Filter"
|
||||
helpText="Filter listings by specific criteria. Only numbers are allowed. You can leave fields empty if you don't want to filter by them."
|
||||
>
|
||||
<div className="jobMutation__specFilter">
|
||||
{SPEC_FILTERS.map((filter) => (
|
||||
<div key={filter.key} className="jobMutation__specFilterItem">
|
||||
<div className="jobMutation__specFilterLabel">{filter.translation}</div>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="Add a number"
|
||||
value={specFilter?.[filter.key]}
|
||||
onChange={(value) => handleSpecFilterChange(filter.key, value)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</SegmentPart>
|
||||
<Divider margin="1rem" />
|
||||
<SegmentPart
|
||||
Icon={IconFilter}
|
||||
name="Area Filter"
|
||||
helpText="Define multiple geographic areas on the map to filter listings. Start drawing by clicking on the square symbol in the top left corner of the map. Click on the map to add points of the polygon. Select the first point to close the polygon. After that, click on a free area of the map to apply this polygon (the color will change from yellow to blue). To delete a polygon, select it first and then click on the trash symbol."
|
||||
>
|
||||
|
||||
@@ -3,6 +3,24 @@
|
||||
float: right;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
&__specFilter {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
&__specFilterItem {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
flex: 1;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
&__specFilterLabel {
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.semi-select-option-list-wrapper {
|
||||
|
||||
@@ -31,7 +31,8 @@ import {
|
||||
IconLink,
|
||||
IconStar,
|
||||
IconStarStroked,
|
||||
IconRealSize,
|
||||
IconExpand,
|
||||
IconGridView,
|
||||
} from '@douyinfe/semi-icons';
|
||||
import maplibregl from 'maplibre-gl';
|
||||
import 'maplibre-gl/dist/maplibre-gl.css';
|
||||
@@ -259,6 +260,17 @@ export default function ListingDetail() {
|
||||
if (!listing) return null;
|
||||
|
||||
const data = [
|
||||
{ key: 'Price', value: `${listing.price} €`, Icon: <IconCart /> },
|
||||
{
|
||||
key: 'Size',
|
||||
value: listing.size ? `${listing.size} m²` : 'N/A',
|
||||
Icon: <IconExpand />,
|
||||
},
|
||||
{
|
||||
key: 'Rooms',
|
||||
value: listing.rooms ? `${listing.rooms} Rooms` : 'N/A',
|
||||
Icon: <IconGridView />,
|
||||
},
|
||||
{
|
||||
key: 'Job',
|
||||
value: listing.job_name,
|
||||
@@ -269,12 +281,6 @@ export default function ListingDetail() {
|
||||
value: listing.provider.charAt(0).toUpperCase() + listing.provider.slice(1),
|
||||
Icon: <IconBriefcase />,
|
||||
},
|
||||
{ key: 'Price', value: `${listing.price} €`, Icon: <IconCart /> },
|
||||
{
|
||||
key: 'Size',
|
||||
value: listing.size ? `${listing.size} m²` : 'N/A',
|
||||
Icon: <IconRealSize />,
|
||||
},
|
||||
{
|
||||
key: 'Added',
|
||||
value: timeService.format(listing.created_at),
|
||||
|
||||
Reference in New Issue
Block a user