import React from 'react'; import {AppShell, Box, Button, Flex, Input, Popover, Switch,} from '@mantine/core'; import {useTranslation} from "react-i18next"; import {AgGridReact, CustomCellRendererProps} from "ag-grid-react"; import { ClientSideRowModelModule, CsvExportModule, LocaleModule, ModuleRegistry, RowSelectionModule, TextFilterModule, themeBalham } from 'ag-grid-community'; import '../../App.css' import {AG_GRID_LOCALE_HK} from '@ag-grid-community/locale' import {useThrottledState} from '@mantine/hooks'; import {IconArrowMerge, IconEye, IconFilter, IconNews, IconSettings, IconTool} from '@tabler/icons-react' import {FilterPanelProps, ListingFilterForm} from "./ListingFilterForm.tsx"; import {abbreviateNumber, deduplicateConsecutive} from "../../utils.ts"; // import {PropertyMap} from "../../components/Map/PropertyMap.tsx"; import {FormattedNumberAnchor} from "../../components/FormattedNumberAnchor.tsx"; import { CheckboxEditorModule, ContextMenuModule, GridApi, IRichCellEditorParams, IRowNode, ListOption, RichSelectModule, SelectEditorModule, SetFilterModule, SideBarModule, TextEditorModule } from "ag-grid-enterprise"; // import DraggableDialog from "../../components/DraggableDialog.tsx"; import {ListingQuickInfo} from "./ListingQuickInfo.tsx"; import {CommonAction} from "./CommonAction.tsx"; import {TabbedAside} from "../../components/TabbedAside.tsx"; import {useQuery} from "@tanstack/react-query"; import {AuthContext} from "../../components/AuthContext/AuthContext.tsx"; import {useNavigate} from "react-router"; import dayjs from "dayjs"; import {Listing, PropertyDetail} from "../../shared/types.ts"; import {AppContext} from "../../components/AppContext/AppContext.tsx"; import {useWorkspace} from "../../shared/workspace.ts"; ModuleRegistry.registerModules([ ClientSideRowModelModule, TextFilterModule, LocaleModule, RowSelectionModule, CsvExportModule, SetFilterModule, ContextMenuModule, TextEditorModule, CheckboxEditorModule, SelectEditorModule, SideBarModule, RichSelectModule, ]) export function ListingPage() { const {t} = useTranslation(); const [mapOpened, setMapOpened] = React.useState(false); const [historyOpened, setHistoryOpened] = React.useState(false); const [listingId] = React.useState(); const [page, setPage] = React.useState('filter'); const auth = React.useContext(AuthContext); const context = React.useContext(AppContext); const {currentWorkspace, setWorkspace, selectedPropertyId} = useWorkspace(context.workspaceId); const [propertyGridApi, setPropertyGridApi] = React.useState>(); const navigate = useNavigate(); const [filter, setFilter] = useThrottledState>({}, 200); const {data: listingData} = useQuery({ queryKey: ['building', 'listing', filter], queryFn: async function() { const resp = await auth.fetch('fetchListingByBuilding', (import.meta.env.VITE_API || '') + `/search/listing`, {method: 'POST', body: JSON.stringify({limit: 2000, ...filter})} ) if (!resp || resp?.status !== 200) { return []; } const listingList: unknown = await resp?.json(); if (Array.isArray(listingList)) { return listingList as PropertyDetail[] } return [] }, placeholderData: (prev) => prev, initialData: [], }) const {data: listingHistoryData} = useQuery({ queryKey: ['listing', listingId, 'history'], queryFn: async function() { const resp = await auth.fetch('fetchListingHistoryById', (import.meta.env.VITE_API || '') + `/listing/${listingId}/history`, {method: 'GET'} ) if (!resp || resp?.status !== 200) { return []; } const listingList: unknown = await resp?.json(); if (Array.isArray(listingList)) { return listingList as Listing[] } return [] }, placeholderData: (prev) => prev, initialData: [], enabled: !!listingId, }) React.useEffect(() => { if (!propertyGridApi) { return } const nodes: IRowNode[] = [] selectedPropertyId.forEach(i => { const node = propertyGridApi.getRowNode(i) if (node) { nodes.push(node) } }) propertyGridApi.setNodesSelected({nodes: nodes, newValue: true}) }, [propertyGridApi]) // const {data: comment} = useQuery({ // queryKey: ['comment', selectedProperty?.propertyId], // queryFn: async function () { // const searchTerm: CommentSearchTerms = {propertyId: selectedProperty?.propertyId, limit: 30}; // const resp = await auth.fetch('comment', `${import.meta.env.VITE_API || ''}/search/comment`, { // method: 'POST', // body: JSON.stringify(searchTerm), // }) // return (await resp?.json() ?? []) as Comment[]; // }, // enabled: !!(selectedProperty?.propertyId), // initialData: [], // }) 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)}/> alert('TODO')}/> row.data.id + row.data.propertyId} localeText={AG_GRID_LOCALE_HK} // editType = 'fullRow' sideBar={true} onGridReady={ev => setPropertyGridApi(ev.api)} columnDefs={[{ field: 'district', headerName: t('district'), cellRenderer: (props: CustomCellRendererProps) => t('districts.' + props.value), initialWidth: 80, }, { field: 'buildingName', headerName: t('buildingName'), initialWidth: 80, }, { 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', headerName: t('listingAreaSqft'), cellRenderer: (props: CustomCellRendererProps) => deduplicateConsecutive(props.value as number[] ?? []).join(' - '), editable: true, cellEditor: 'agNumberCellEditor', initialWidth: 80 }, { field: 'rent', headerName: t('rent'), cellRenderer: (props: CustomCellRendererProps) => abbreviateNumber(val, 2, { prefix: '$', invalidValue: '-' })}/>, editable: true, cellEditor: 'agNumberCellEditor', initialWidth: 60 }, { field: 'rentRange', headerName: t('listingRentRange'), cellRenderer: (props: CustomCellRendererProps) => abbreviateNumber(val, 2, { prefix: '$', invalidValue: '-' })}/>, editable: true, cellEditor: 'agNumberCellEditor', initialWidth: 80 }, { field: 'rentPerSqft', headerName: t('rentPerSqft'), cellRenderer: (props: CustomCellRendererProps) => abbreviateNumber(val, 2, { prefix: '$', invalidValue: '-' })}/>, cellEditor: 'agNumberCellEditor', initialWidth: 60 }, { field: 'price', headerName: t('price'), cellRenderer: (props: CustomCellRendererProps) => abbreviateNumber(val, 2, { prefix: '$', invalidValue: '-' })}/>, cellEditor: 'agNumberCellEditor', initialWidth: 60 }, { field: 'priceRange', headerName: t('listingPriceRange'), cellRenderer: (props: CustomCellRendererProps) => abbreviateNumber(val, 2, { prefix: '$', invalidValue: '-' })}/>, editable: true, cellEditor: 'agNumberCellEditor', initialWidth: 80 }, { field: 'pricePerSqft', headerName: t('pricePerSqft'), cellRenderer: (props: CustomCellRendererProps) => abbreviateNumber(val, 2, { prefix: '$', invalidValue: '-' })}/>, cellEditor: 'agNumberCellEditor', 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'), type: 'number', 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', }}) } } ]} rowSelection={{mode: 'multiRow'}} onSelectionChanged={({api}) => { setWorkspace({...currentWorkspace, selectedProperty: api.getSelectedRows()}) }} rowData={[ ...currentWorkspace.selectedProperty, ...listingData .filter(i => !selectedPropertyId.includes(i.id + i.propertyId)) // .filter(i => i.listingPropertyId.length === 1) // ...listingData.filter(i => i.listingPropertyId.length > 1) ]} /> ) => dayjs(props.value).format('YYYY-MM-DD HH:mm:ss')}, {field: 'price', headerName: t('price'), width: 80, cellRenderer: (props: CustomCellRendererProps) => t('intlNumber', {val: props.value})}, {field: 'rent', headerName: t('rent'), width: 80, cellRenderer: (props: CustomCellRendererProps) => t('intlNumber', {val: props.value})}, {field: 'updatedBy', headerName: t('updatedBy'), flex: 1} ]} rowData={listingHistoryData.map(i => ({...i, updatedAt: dayjs(i.updatedAt).toDate()}))} /> }, {value: 'quick-info', label: }, {value: 'action', label: }, ]} currentTab={page} setCurrentTab={(value) => setPage(value)} > {page === 'filter' ? setFilter(values)} /> : null} {page === 'quick-info' ? : null} {page === 'action' ? : null} ); }