import React from 'react'; import {AppShell, Badge, Box, Button, Flex, Popover, Stack, Switch,} from '@mantine/core'; import {useTranslation} from "react-i18next"; import {AgGridReact, CustomCellRendererProps} from "ag-grid-react"; import { themeBalham } from 'ag-grid-community'; import '../../App.css' import {AG_GRID_LOCALE_HK} from '@ag-grid-community/locale' import {IconArrowMerge, IconEdit, IconTool} from '@tabler/icons-react' import {ListingFilterForm, FilterPanelFormValues} from "./ListingFilterForm.tsx"; import {FormattedNumberAnchor} from "../../components/FormattedNumberAnchor.tsx"; import { GridApi, IRichCellEditorParams, ListOption, } from "ag-grid-enterprise"; import {ListingQuickInfo} from "./ListingQuickInfo.tsx"; import {CommonAction} from "./CommonAction.tsx"; import {TabbedAside} from "../../components/TabbedAside.tsx"; import {AppContext} from "../../components/AppContext/AppContext.tsx"; import {useNavigate} from "react-router"; import {GetPropertyDetail} from "../../shared/types.ts"; import {abbreviateNumber, deduplicateConsecutive, getPropertyDetailId} from "../../shared/utils.ts"; import {useWorkspace} from "../../shared/workspace.tsx"; import {FilterPillBar} from "./FilterPillBar.tsx"; import {AiSearchBarProvider, AiSearchInput, AiSuggestionsPanel} from "./AiSearchBar.tsx"; export function ListingPage() { const {t} = useTranslation(); const [mapOpened, setMapOpened] = React.useState(false); const [historyOpened, setHistoryOpened] = React.useState(false); const [page, setPage] = React.useState('filter'); const [focusField, setFocusField] = React.useState(null); const aiInputRef = React.useRef(null); const {setAsideVisible} = React.useContext(AppContext); const { selectedPropertyDetailId, rowData, setPropertyDetailSelection, setFocusedPropertyDetail, setPropertyDetailFilter, propertyDetailFilter, } = useWorkspace(); const [propertyGridApi, setPropertyGridApi] = React.useState>(); const navigate = useNavigate(); // Sync workspace selection state back into AG Grid node selection React.useEffect(() => { if (!propertyGridApi) { return } const selectedRows = propertyGridApi.getSelectedRows().map(i => getPropertyDetailId(i)) const shouldUnselect = selectedRows .filter(i => !selectedPropertyDetailId.includes(i)) .map(i => propertyGridApi.getRowNode(i)) .filter(i => !!i) const shouldSelect = selectedPropertyDetailId .filter(i => !selectedRows.includes(i)) .map(i => propertyGridApi.getRowNode(i)) .filter(i => !!i) propertyGridApi.setNodesSelected({nodes: shouldUnselect, newValue: false}) propertyGridApi.setNodesSelected({nodes: shouldSelect, newValue: true}) }, [propertyGridApi, selectedPropertyDetailId, rowData]) return ( <> {/*{mapOpened ?*/} {/* setMapOpened(false)}*/} {/* default={{x: 80, y: 150, width: 300, height: 200}}>*/} {/* 0 ? names : debouncedRowData.map(i => i.name)}*/} {/* idAttr='name'*/} {/* popup={
}*/} {/* />*/} {/* : null*/} {/*}*/} {/*{historyOpened ?*/} {/* setHistoryOpened(false)}*/} {/* default={{x: 80, y: 150, width: 300, height: 200}}>*/} {/* ({...i, updatedAt: dayjs(i.updatedAt).toDate()}))}*/} {/* />*/} {/* : null*/} {/*}*/} setMapOpened(!mapOpened)}/> setHistoryOpened(!historyOpened)}/> { setAsideVisible(true); setPage('filter'); setFocusField(field); }} /> {Object.entries(propertyDetailFilter ?? {}).filter(([key, value]) => key !== 'limit' && value !== null).length > 0 ? setPropertyDetailFilter({})} > {t('clearAllFilter')} : null } getPropertyDetailId(row.data)} localeText={AG_GRID_LOCALE_HK} postSortRows={({nodes, api}) => { // Move rows with null/undefined values to the bottom (nulls last) // regardless of sort direction const sortedCols = (api.getColumnState() ?? []) .filter(col => col.sort != null) .sort((a, b) => (a.sortIndex ?? 0) - (b.sortIndex ?? 0)) .map(col => col.colId); if (sortedCols.length === 0) return; let nullStart = nodes.length; let i = 0; while (i < nullStart) { const data = nodes[i].data; const isNullRow = data == null || sortedCols.every( colId => data[colId as keyof GetPropertyDetail] == null ); if (isNullRow) { nodes.splice(nullStart - 1, 0, nodes.splice(i, 1)[0]); nullStart--; } else { i++; } } }} onRowClicked={({data}) => { setFocusedPropertyDetail(data ? getPropertyDetailId(data) : null) }} onGridReady={ev => setPropertyGridApi(ev.api)} rowSelection={{mode: 'multiRow'}} onSelectionChanged={({api}) => { setPropertyDetailSelection(api.getSelectedRows()) }} onRowSelected={({data}) => { setFocusedPropertyDetail(data ? getPropertyDetailId(data) : null) }} selectionColumnDef={{ // Explicitly pin the checkbox column to the left pinned: 'left', lockPosition: 'left', width: 35 }} rowData={rowData} columnDefs={[{ field: 'district', headerName: t('district'), cellRenderer: (props: CustomCellRendererProps) => t('districts.' + props.value), initialWidth: 80, }, { field: 'buildingName', headerName: t('buildingName'), initialWidth: 80, onCellClicked: (event) => { setPropertyDetailFilter({...propertyDetailFilter, buildingName: event.data?.buildingName}) }, }, { field: 'floor', headerName: t('floor'), initialWidth: 50, }, { field: 'unit', headerName: t('unit'), initialWidth: 50, }, { field: 'status', headerName: t('status'), initialWidth: 80, cellRenderer: (props: CustomCellRendererProps) => t('propertyStatus.' + props.value), }, { field: 'areaSqft', headerName: t('areaSqft'), initialWidth: 70 }, { field: 'areaSqftRange', initialHide: true, headerName: t('listingAreaSqft'), cellRenderer: (props: CustomCellRendererProps) => deduplicateConsecutive(props.value as number[] ?? []).join(' - '), editable: true, initialWidth: 80 }, { field: 'rent', headerName: t('rent'), cellRenderer: (props: CustomCellRendererProps) => abbreviateNumber(val, 2, { prefix: '$', invalidValue: '-' })}/>, editable: true, initialWidth: 60 }, { field: 'rentRange', initialHide: true, headerName: t('listingRentRange'), cellRenderer: (props: CustomCellRendererProps) => abbreviateNumber(val, 2, { prefix: '$', invalidValue: '-' })}/>, editable: true, initialWidth: 80 }, { field: 'rentPerSqft', headerName: t('rentPerSqft'), cellRenderer: (props: CustomCellRendererProps) => abbreviateNumber(val, 2, { prefix: '@', invalidValue: '-' })}/>, initialWidth: 60 }, { field: 'price', headerName: t('price'), cellRenderer: (props: CustomCellRendererProps) => abbreviateNumber(val, 2, { prefix: '$', invalidValue: '-' })}/>, initialWidth: 60 }, { field: 'priceRange', initialHide: true, headerName: t('listingPriceRange'), cellRenderer: (props: CustomCellRendererProps) => abbreviateNumber(val, 2, { prefix: '$', invalidValue: '-' })}/>, initialWidth: 80 }, { field: 'pricePerSqft', headerName: t('pricePerSqft'), cellRenderer: (props: CustomCellRendererProps) => abbreviateNumber(val, 2, { prefix: '@', invalidValue: '-' })}/>, initialWidth: 60 }, { field: 'buildingUsage', headerName: t('buildingUsage'), cellRenderer: (props: CustomCellRendererProps) => props.value ? t('buildingUsages.' + props.value) : '-', initialWidth: 80 }, { field: 'propertyUsage', headerName: t('propertyUsage'), cellRenderer: (props: CustomCellRendererProps) => props.value ? t('propertyUsages.' + props.value) : '-', initialWidth: 80 }, { field: 'scene', headerName: t('scene'), cellRenderer: (props: CustomCellRendererProps) => props.value ? t('scenes.' + props.value) : '-', initialWidth: 80, editable: true, cellEditor: 'agRichSelectCellEditor', cellEditorParams: { values: (Object.entries(t('scenes', {returnObjects: true})) ?? []).map(([key, value]: [string, string]): ListOption => ({text: value, value: key})), formatValue: (i: ListOption) => i?.text, parseValue: (i: ListOption) => i?.value, } as IRichCellEditorParams, }, { field: 'interior', headerName: t('interior'), cellRenderer: (props: CustomCellRendererProps) => props.value ? t('interiors.' + props.value) : '-', initialWidth: 80 }, { field: 'isToiletInternal', cellDataType: 'boolean', headerName: t('isToiletInternal'), initialWidth: 50, editable: true, cellEditor: 'agCheckboxCellEditor', }, { field: 'isKeyAvailable', cellDataType: 'boolean', headerName: t('isKeyAvailable'), initialWidth: 60, editable: true, cellEditor: 'agCheckboxCellEditor', }, { field: 'clearHeightMeter', headerName: t('clearHeightMeter'), initialWidth: 60, cellRenderer: (props: CustomCellRendererProps) => typeof props.value === 'number' && Number.isFinite(props.value) ? `${props.value}m` : '', }, { field: 'airConditioning', headerName: t('airConditioning'), initialWidth: 80, editable: true, cellEditor: 'agRichSelectCellEditor', cellRenderer: (props: CustomCellRendererProps) => props.value ? t('airConditionings.' + props.value) : '-', cellEditorParams: { values: (Object.entries(t('airConditionings', {returnObjects: true})) ?? []).map(([key, value]: [string, string]): ListOption => ({text: value, value: key})), formatValue: (i: ListOption) => i?.text, parseValue: (i: ListOption) => i?.value, } as IRichCellEditorParams, }, { field: 'electricalCapacityAmp', headerName: t('electricalCapacityAmp'), initialWidth: 70, cellRenderer: (props: CustomCellRendererProps) => typeof props.value === 'number' && Number.isFinite(props.value) ? `${props.value}A` : '', editable: true, cellEditor: 'agNumberCellEditor', }, { field: 'floorLoadCapacityKpa', headerName: t('floorLoadCapacityKpa'), initialWidth: 70, cellRenderer: (props: CustomCellRendererProps) => typeof props.value === 'number' && Number.isFinite(props.value) ? `${props.value}kPa` : '', editable: true, cellEditor: 'agNumberCellEditor', }, { field: 'entranceWidthMeter', headerName: t('entranceWidthMeter'), initialWidth: 70, cellRenderer: (props: CustomCellRendererProps) => typeof props.value === 'number' && Number.isFinite(props.value) ? `${props.value}m` : '', editable: true, cellEditor: 'agNumberCellEditor', }, { field: 'hasLoft', headerName: t('hasLoft'), initialWidth: 60, editable: true, cellEditor: 'agCheckboxCellEditor', }, { field: 'listingPropertyId', width: 25, headerName: '', pinned: 'right', cellRenderer: (value: CustomCellRendererProps) => ( {(value.data?.listingPropertyId.length ?? 0) > 1 ? : null} ) }, { field: 'id', headerName: '', pinned: 'right', width: 30, sortable: false, cellRenderer: () => ( ), onCellClicked: (event) => { void navigate('/building', {state: { buildingId: event.data?.buildingId, propertyId: event.data?.propertyId, listingId: event.data?.id, formMode: 'patch', tab: event.data?.availableAt ? 'listing' : 'property', }}) } } ]} /> setPage(value)} > {page === 'filter' ? setFocusField(null)} /> : null} {page === 'quick-info' ? : null} {page === 'action' ? : null} ); }