@salesforce/webapp-template-app-react-sample-b2e-experimental 1.73.0 → 1.74.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/CHANGELOG.md +16 -0
- package/dist/force-app/main/default/objects/Maintenance_Request__c/Maintenance_Request__c.object-meta.xml +11 -1
- package/dist/force-app/main/default/objects/Maintenance_Worker__c/Maintenance_Worker__c.object-meta.xml +6 -1
- package/dist/force-app/main/default/objects/Property__c/Property__c.object-meta.xml +6 -1
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/package.json +7 -5
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/api/maintenanceWorkers.ts +60 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/ApplicationsTable.tsx +59 -62
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/FiltersFromApi.tsx +200 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/ListPageFilters.tsx +97 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/MaintenanceTable.tsx +2 -1
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/ObjectSelect.tsx +39 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/VerticalNav.tsx +6 -4
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/dashboard/GlobalSearchBar.tsx +125 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/feedback/FilterErrorAlert.tsx +15 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/feedback/PageErrorState.tsx +19 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/feedback/PageLoadingState.tsx +18 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/filters/FilterFieldRange.tsx +40 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/filters/FilterFieldSelect.tsx +190 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/filters/FilterFieldText.tsx +32 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/filters/ListPageFilterRow.tsx +100 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/layout/PageContainer.tsx +9 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/layout/PageHeader.tsx +21 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/list/ListPageWithFilters.tsx +70 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/constants.ts +39 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/api/index.ts +19 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/api/objectDetailService.ts +125 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/api/objectInfoGraphQLService.ts +194 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/api/objectInfoService.ts +199 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/api/recordListGraphQLService.ts +364 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/DetailFields.tsx +55 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/DetailForm.tsx +146 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/DetailHeader.tsx +34 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/DetailLayoutSections.tsx +80 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/Section.tsx +108 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/SectionRow.tsx +20 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/UiApiDetailForm.tsx +140 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/formatted/FieldValueDisplay.tsx +73 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/formatted/FormattedAddress.tsx +29 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/formatted/FormattedEmail.tsx +17 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/formatted/FormattedPhone.tsx +24 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/formatted/FormattedText.tsx +11 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/formatted/FormattedUrl.tsx +29 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/formatted/index.ts +6 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/filters/FilterField.tsx +54 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/filters/FilterInput.tsx +55 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/filters/FilterSelect.tsx +72 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/filters/FiltersPanel.tsx +380 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/forms/filters-form.tsx +114 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/forms/submit-button.tsx +47 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/search/GlobalSearchInput.tsx +114 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/search/ResultCardFields.tsx +71 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/search/SearchHeader.tsx +31 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/search/SearchPagination.tsx +144 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/search/SearchResultCard.tsx +136 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/search/SearchResultsPanel.tsx +197 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/shared/LoadingFallback.tsx +61 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/filters/FilterInput.tsx +55 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/filters/FilterSelect.tsx +72 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/hooks/form.tsx +209 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/hooks/index.ts +22 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/hooks/useObjectInfoBatch.ts +65 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/hooks/useObjectSearchData.ts +395 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/hooks/useRecordDetailLayout.ts +156 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/hooks/useRecordListGraphQL.ts +135 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/pages/DetailPage.tsx +109 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/pages/GlobalSearch.tsx +229 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/types/filters/filters.ts +121 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/types/filters/picklist.ts +32 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/types/index.ts +5 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/types/objectInfo/objectInfo.ts +166 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/types/recordDetail/recordDetail.ts +61 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/types/search/searchResults.ts +229 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/apiUtils.ts +125 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/cacheUtils.ts +76 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/debounce.ts +89 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/fieldUtils.ts +354 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/fieldValueExtractor.ts +67 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/filterUtils.ts +32 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/formDataTransformUtils.ts +260 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/formUtils.ts +142 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/graphQLNodeFieldUtils.ts +186 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/graphQLObjectInfoAdapter.ts +319 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/graphQLRecordAdapter.ts +90 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/index.ts +59 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/layoutTransformUtils.ts +236 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/linkUtils.ts +14 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/paginationUtils.ts +49 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/recordUtils.ts +159 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/sanitizationUtils.ts +49 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/hooks/useAccumulatedListPages.ts +29 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/hooks/useListPage.ts +167 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/index.ts +8 -4
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/applicationAdapter.ts +33 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/applicationColumns.ts +28 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/constants.ts +24 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/fieldMappers.ts +71 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/filterUtils.ts +165 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/globalSearchConstants.ts +40 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/listFilters.ts +152 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/listPageConfig.ts +65 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/maintenanceAdapter.ts +110 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/maintenanceColumns.ts +24 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/maintenanceWorkerAdapter.ts +29 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/maintenanceWorkerColumns.ts +25 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/objectApiNames.ts +13 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/propertyAdapter.ts +68 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/propertyColumns.ts +17 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/routeConfig.ts +35 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/types.ts +10 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/pages/Applications.tsx +47 -62
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/pages/Home.tsx +130 -98
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/pages/Maintenance.tsx +74 -91
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/pages/MaintenanceWorkers.tsx +138 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/pages/Properties.tsx +166 -85
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/routes.tsx +41 -2
- package/dist/package.json +1 -1
- package/package.json +5 -1
package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/pages/Maintenance.tsx
CHANGED
|
@@ -1,46 +1,30 @@
|
|
|
1
1
|
import { useEffect, useState } from "react";
|
|
2
2
|
import { ChevronDown } from "lucide-react";
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
3
|
+
import { useListPage } from "../hooks/useListPage.js";
|
|
4
|
+
import { maintenanceRequestsListConfig } from "../lib/listPageConfig.js";
|
|
5
|
+
import { ListPageWithFilters } from "../components/list/ListPageWithFilters.js";
|
|
6
|
+
import { PageHeader } from "../components/layout/PageHeader.js";
|
|
5
7
|
import { UserAvatar } from "../components/UserAvatar.js";
|
|
6
8
|
import { StatusBadge } from "../components/StatusBadge.js";
|
|
7
9
|
import { MaintenanceDetailsModal } from "../components/MaintenanceDetailsModal.js";
|
|
10
|
+
import { updateMaintenanceStatus } from "../api/maintenance.js";
|
|
11
|
+
import type { MaintenanceRequest } from "../lib/types.js";
|
|
8
12
|
|
|
9
13
|
export default function Maintenance() {
|
|
10
|
-
const
|
|
11
|
-
const [loading, setLoading] = useState(true);
|
|
14
|
+
const list = useListPage(maintenanceRequestsListConfig);
|
|
12
15
|
const [selectedRequest, setSelectedRequest] = useState<MaintenanceRequest | null>(null);
|
|
13
16
|
const [notification, setNotification] = useState<{
|
|
14
17
|
message: string;
|
|
15
18
|
type: "success" | "error";
|
|
16
19
|
} | null>(null);
|
|
17
20
|
|
|
18
|
-
useEffect(() => {
|
|
19
|
-
loadMaintenanceRequests();
|
|
20
|
-
}, []);
|
|
21
|
-
|
|
22
|
-
// Clear notification after 5 seconds
|
|
23
21
|
useEffect(() => {
|
|
24
22
|
if (notification) {
|
|
25
|
-
const timer = setTimeout(() =>
|
|
26
|
-
setNotification(null);
|
|
27
|
-
}, 5000);
|
|
23
|
+
const timer = setTimeout(() => setNotification(null), 5000);
|
|
28
24
|
return () => clearTimeout(timer);
|
|
29
25
|
}
|
|
30
26
|
}, [notification]);
|
|
31
27
|
|
|
32
|
-
const loadMaintenanceRequests = async () => {
|
|
33
|
-
try {
|
|
34
|
-
setLoading(true);
|
|
35
|
-
const data = await getAllMaintenanceRequests();
|
|
36
|
-
setRequests(data);
|
|
37
|
-
} catch (error) {
|
|
38
|
-
console.error("Error loading maintenance requests:", error);
|
|
39
|
-
} finally {
|
|
40
|
-
setLoading(false);
|
|
41
|
-
}
|
|
42
|
-
};
|
|
43
|
-
|
|
44
28
|
const handleRowClick = (request: MaintenanceRequest) => {
|
|
45
29
|
setSelectedRequest(request);
|
|
46
30
|
};
|
|
@@ -52,66 +36,63 @@ export default function Maintenance() {
|
|
|
52
36
|
const handleSaveStatus = async (requestId: string, status: string) => {
|
|
53
37
|
try {
|
|
54
38
|
const success = await updateMaintenanceStatus(requestId, status);
|
|
55
|
-
|
|
56
39
|
if (success) {
|
|
57
|
-
// Update the local state
|
|
58
|
-
setRequests((prev) => prev.map((req) => (req.id === requestId ? { ...req, status } : req)));
|
|
59
|
-
|
|
60
|
-
// Update selected request if it's the one being edited
|
|
61
40
|
if (selectedRequest?.id === requestId) {
|
|
62
41
|
setSelectedRequest({ ...selectedRequest, status });
|
|
63
42
|
}
|
|
64
|
-
|
|
65
|
-
|
|
43
|
+
setNotification({
|
|
44
|
+
message: "Maintenance request status updated successfully!",
|
|
45
|
+
type: "success",
|
|
46
|
+
});
|
|
66
47
|
} else {
|
|
67
|
-
|
|
48
|
+
setNotification({ message: "Failed to update maintenance request status", type: "error" });
|
|
68
49
|
}
|
|
69
50
|
} catch (error) {
|
|
70
51
|
console.error("Error updating maintenance request:", error);
|
|
71
|
-
|
|
52
|
+
setNotification({ message: "An error occurred while updating the status", type: "error" });
|
|
72
53
|
}
|
|
73
54
|
};
|
|
74
55
|
|
|
75
|
-
const showNotification = (message: string, type: "success" | "error") => {
|
|
76
|
-
setNotification({ message, type });
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
if (loading) {
|
|
80
|
-
return (
|
|
81
|
-
<div className="flex items-center justify-center h-screen bg-gray-50">
|
|
82
|
-
<div className="text-lg text-gray-600">Loading maintenance requests...</div>
|
|
83
|
-
</div>
|
|
84
|
-
);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
56
|
return (
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
99
|
-
>
|
|
100
|
-
<div className="flex items-center gap-3">
|
|
101
|
-
<span className="text-lg">{notification.type === "success" ? "✓" : "✕"}</span>
|
|
102
|
-
<p className="font-medium">{notification.message}</p>
|
|
103
|
-
</div>
|
|
57
|
+
<>
|
|
58
|
+
{notification && (
|
|
59
|
+
<div className="fixed top-4 right-4 z-50 animate-slide-in">
|
|
60
|
+
<div
|
|
61
|
+
className={`px-6 py-4 rounded-lg shadow-lg ${
|
|
62
|
+
notification.type === "success" ? "bg-green-500 text-white" : "bg-red-500 text-white"
|
|
63
|
+
}`}
|
|
64
|
+
>
|
|
65
|
+
<div className="flex items-center gap-3">
|
|
66
|
+
<span className="text-lg">{notification.type === "success" ? "✓" : "✕"}</span>
|
|
67
|
+
<p className="font-medium">{notification.message}</p>
|
|
104
68
|
</div>
|
|
105
69
|
</div>
|
|
106
|
-
)}
|
|
107
|
-
|
|
108
|
-
{/* Header */}
|
|
109
|
-
<div className="mb-6">
|
|
110
|
-
<h1 className="text-2xl font-bold text-gray-900">Maintenance</h1>
|
|
111
|
-
<p className="text-gray-600 mt-1">Track and manage maintenance requests</p>
|
|
112
70
|
</div>
|
|
113
|
-
|
|
114
|
-
|
|
71
|
+
)}
|
|
72
|
+
|
|
73
|
+
<PageHeader
|
|
74
|
+
title="Maintenance Requests"
|
|
75
|
+
description="Track and manage maintenance requests"
|
|
76
|
+
/>
|
|
77
|
+
|
|
78
|
+
<ListPageWithFilters
|
|
79
|
+
filterProps={{
|
|
80
|
+
filters: list.filters,
|
|
81
|
+
picklistValues: list.picklistValues,
|
|
82
|
+
formValues: list.formValues,
|
|
83
|
+
onFormValueChange: list.onFormValueChange,
|
|
84
|
+
onApply: list.onApplyFilters,
|
|
85
|
+
onReset: list.onResetFilters,
|
|
86
|
+
ariaLabel: maintenanceRequestsListConfig.filtersAriaLabel,
|
|
87
|
+
}}
|
|
88
|
+
filterError={list.filterError}
|
|
89
|
+
loading={list.loading}
|
|
90
|
+
error={list.error}
|
|
91
|
+
loadingMessage={maintenanceRequestsListConfig.loadingMessage}
|
|
92
|
+
isEmpty={list.items.length === 0}
|
|
93
|
+
searchPlaceholder="Search by description, tenant, property, status..."
|
|
94
|
+
searchAriaLabel="Search maintenance requests"
|
|
95
|
+
>
|
|
115
96
|
<div className="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
|
|
116
97
|
<div className="grid grid-cols-12 gap-4 px-6 py-4 bg-gray-50 border-b border-gray-200">
|
|
117
98
|
<div className="col-span-6 flex items-center gap-2">
|
|
@@ -131,21 +112,17 @@ export default function Maintenance() {
|
|
|
131
112
|
</span>
|
|
132
113
|
</div>
|
|
133
114
|
</div>
|
|
134
|
-
|
|
135
|
-
{/* Table Rows */}
|
|
136
115
|
<div className="divide-y divide-gray-200">
|
|
137
|
-
{
|
|
116
|
+
{list.items.length === 0 ? (
|
|
138
117
|
<div className="text-center py-12 text-gray-500">No maintenance requests found</div>
|
|
139
118
|
) : (
|
|
140
|
-
|
|
119
|
+
list.items.map((request) => (
|
|
141
120
|
<div
|
|
142
121
|
key={request.id}
|
|
143
122
|
onClick={() => handleRowClick(request)}
|
|
144
123
|
className="grid grid-cols-12 gap-4 px-6 py-5 hover:bg-gray-50 transition-colors cursor-pointer"
|
|
145
124
|
>
|
|
146
|
-
{/* Maintenance Task */}
|
|
147
125
|
<div className="col-span-6 flex items-center gap-4">
|
|
148
|
-
{/* Task Image */}
|
|
149
126
|
<div className="w-16 h-16 rounded-lg bg-gray-200 flex-shrink-0 overflow-hidden">
|
|
150
127
|
{request.imageUrl ? (
|
|
151
128
|
<img
|
|
@@ -159,8 +136,6 @@ export default function Maintenance() {
|
|
|
159
136
|
</div>
|
|
160
137
|
)}
|
|
161
138
|
</div>
|
|
162
|
-
|
|
163
|
-
{/* Task Details */}
|
|
164
139
|
<div className="flex-1 min-w-0">
|
|
165
140
|
<h3 className="font-semibold text-gray-900 truncate mb-1">
|
|
166
141
|
{request.description}
|
|
@@ -168,8 +143,6 @@ export default function Maintenance() {
|
|
|
168
143
|
<p className="text-sm text-gray-500">By Tenant</p>
|
|
169
144
|
</div>
|
|
170
145
|
</div>
|
|
171
|
-
|
|
172
|
-
{/* Tenant Unit */}
|
|
173
146
|
<div className="col-span-4 flex items-center">
|
|
174
147
|
<div className="flex items-center gap-3">
|
|
175
148
|
<UserAvatar name={request.tenantName || "Unknown"} size="md" />
|
|
@@ -183,8 +156,6 @@ export default function Maintenance() {
|
|
|
183
156
|
</div>
|
|
184
157
|
</div>
|
|
185
158
|
</div>
|
|
186
|
-
|
|
187
|
-
{/* Status */}
|
|
188
159
|
<div className="col-span-2 flex items-center">
|
|
189
160
|
<StatusBadge status={request.status} />
|
|
190
161
|
</div>
|
|
@@ -194,16 +165,28 @@ export default function Maintenance() {
|
|
|
194
165
|
</div>
|
|
195
166
|
</div>
|
|
196
167
|
|
|
197
|
-
{
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
168
|
+
{list.canLoadMore && (
|
|
169
|
+
<div className="flex justify-center mt-6">
|
|
170
|
+
<button
|
|
171
|
+
type="button"
|
|
172
|
+
onClick={list.onLoadMore}
|
|
173
|
+
disabled={list.loadMoreLoading}
|
|
174
|
+
className="px-6 py-2 bg-purple-700 hover:bg-purple-800 text-white rounded-lg text-sm font-medium disabled:opacity-50"
|
|
175
|
+
>
|
|
176
|
+
{list.loadMoreLoading ? "Loading..." : "Load More"}
|
|
177
|
+
</button>
|
|
178
|
+
</div>
|
|
205
179
|
)}
|
|
206
|
-
</
|
|
207
|
-
|
|
180
|
+
</ListPageWithFilters>
|
|
181
|
+
|
|
182
|
+
{selectedRequest && (
|
|
183
|
+
<MaintenanceDetailsModal
|
|
184
|
+
request={selectedRequest}
|
|
185
|
+
isOpen={!!selectedRequest}
|
|
186
|
+
onClose={handleCloseModal}
|
|
187
|
+
onSave={handleSaveStatus}
|
|
188
|
+
/>
|
|
189
|
+
)}
|
|
190
|
+
</>
|
|
208
191
|
);
|
|
209
192
|
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { useListPage } from "../hooks/useListPage.js";
|
|
3
|
+
import { maintenanceWorkersListConfig } from "../lib/listPageConfig.js";
|
|
4
|
+
import { ListPageWithFilters } from "../components/list/ListPageWithFilters.js";
|
|
5
|
+
import { PageHeader } from "../components/layout/PageHeader.js";
|
|
6
|
+
import type { MaintenanceWorker } from "../lib/types.js";
|
|
7
|
+
|
|
8
|
+
export default function MaintenanceWorkers() {
|
|
9
|
+
const list = useListPage(maintenanceWorkersListConfig);
|
|
10
|
+
const [selectedWorker, setSelectedWorker] = useState<MaintenanceWorker | null>(null);
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<>
|
|
14
|
+
<PageHeader title="Maintenance Workers" description="View and filter maintenance workers" />
|
|
15
|
+
|
|
16
|
+
<ListPageWithFilters
|
|
17
|
+
filterProps={{
|
|
18
|
+
filters: list.filters,
|
|
19
|
+
picklistValues: list.picklistValues,
|
|
20
|
+
formValues: list.formValues,
|
|
21
|
+
onFormValueChange: list.onFormValueChange,
|
|
22
|
+
onApply: list.onApplyFilters,
|
|
23
|
+
onReset: list.onResetFilters,
|
|
24
|
+
ariaLabel: maintenanceWorkersListConfig.filtersAriaLabel,
|
|
25
|
+
}}
|
|
26
|
+
filterError={list.filterError}
|
|
27
|
+
loading={list.loading}
|
|
28
|
+
error={list.error}
|
|
29
|
+
loadingMessage={maintenanceWorkersListConfig.loadingMessage}
|
|
30
|
+
isEmpty={list.items.length === 0}
|
|
31
|
+
searchPlaceholder="Search by name, organization, status..."
|
|
32
|
+
searchAriaLabel="Search workers"
|
|
33
|
+
>
|
|
34
|
+
<div className="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
|
|
35
|
+
<div className="grid grid-cols-12 gap-4 px-6 py-4 bg-gray-50 border-b border-gray-200">
|
|
36
|
+
<div className="col-span-5">
|
|
37
|
+
<span className="text-sm font-semibold text-purple-700 uppercase tracking-wide">
|
|
38
|
+
Name
|
|
39
|
+
</span>
|
|
40
|
+
</div>
|
|
41
|
+
<div className="col-span-4">
|
|
42
|
+
<span className="text-sm font-semibold text-purple-700 uppercase tracking-wide">
|
|
43
|
+
Organization
|
|
44
|
+
</span>
|
|
45
|
+
</div>
|
|
46
|
+
<div className="col-span-2">
|
|
47
|
+
<span className="text-sm font-semibold text-purple-700 uppercase tracking-wide">
|
|
48
|
+
Active Requests
|
|
49
|
+
</span>
|
|
50
|
+
</div>
|
|
51
|
+
<div className="col-span-1">
|
|
52
|
+
<span className="text-sm font-semibold text-purple-700 uppercase tracking-wide">
|
|
53
|
+
Status
|
|
54
|
+
</span>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
<div className="divide-y divide-gray-200">
|
|
58
|
+
{list.items.length === 0 ? (
|
|
59
|
+
<div className="text-center py-12 text-gray-500">No maintenance workers found</div>
|
|
60
|
+
) : (
|
|
61
|
+
list.items.map((worker) => (
|
|
62
|
+
<div
|
|
63
|
+
key={worker.id}
|
|
64
|
+
onClick={() => setSelectedWorker(worker)}
|
|
65
|
+
className="grid grid-cols-12 gap-4 px-6 py-4 hover:bg-gray-50 transition-colors cursor-pointer"
|
|
66
|
+
>
|
|
67
|
+
<div className="col-span-5 font-medium text-gray-900">{worker.name}</div>
|
|
68
|
+
<div className="col-span-4 text-gray-600">{worker.organization ?? "—"}</div>
|
|
69
|
+
<div className="col-span-2 text-gray-600">
|
|
70
|
+
{worker.activeRequestsCount ?? "—"}
|
|
71
|
+
</div>
|
|
72
|
+
<div className="col-span-1 text-gray-600">{worker.status ?? "—"}</div>
|
|
73
|
+
</div>
|
|
74
|
+
))
|
|
75
|
+
)}
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
{list.canLoadMore && (
|
|
79
|
+
<div className="flex justify-center mt-6">
|
|
80
|
+
<button
|
|
81
|
+
type="button"
|
|
82
|
+
onClick={list.onLoadMore}
|
|
83
|
+
disabled={list.loadMoreLoading}
|
|
84
|
+
className="px-6 py-2 bg-purple-700 hover:bg-purple-800 text-white rounded-lg text-sm font-medium disabled:opacity-50"
|
|
85
|
+
>
|
|
86
|
+
{list.loadMoreLoading ? "Loading..." : "Load More"}
|
|
87
|
+
</button>
|
|
88
|
+
</div>
|
|
89
|
+
)}
|
|
90
|
+
</ListPageWithFilters>
|
|
91
|
+
{selectedWorker && (
|
|
92
|
+
<div
|
|
93
|
+
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50"
|
|
94
|
+
role="dialog"
|
|
95
|
+
aria-modal="true"
|
|
96
|
+
aria-labelledby="worker-dialog-title"
|
|
97
|
+
onClick={() => setSelectedWorker(null)}
|
|
98
|
+
>
|
|
99
|
+
<div
|
|
100
|
+
className="bg-white rounded-lg shadow-xl max-w-md w-full mx-4 p-6"
|
|
101
|
+
onClick={(e) => e.stopPropagation()}
|
|
102
|
+
>
|
|
103
|
+
<h2 id="worker-dialog-title" className="text-lg font-semibold text-gray-900 mb-4">
|
|
104
|
+
Worker Details
|
|
105
|
+
</h2>
|
|
106
|
+
<dl className="space-y-2 text-sm">
|
|
107
|
+
<div>
|
|
108
|
+
<dt className="text-gray-500">Name</dt>
|
|
109
|
+
<dd className="font-medium text-gray-900">{selectedWorker.name}</dd>
|
|
110
|
+
</div>
|
|
111
|
+
<div>
|
|
112
|
+
<dt className="text-gray-500">Organization</dt>
|
|
113
|
+
<dd className="text-gray-900">{selectedWorker.organization ?? "—"}</dd>
|
|
114
|
+
</div>
|
|
115
|
+
<div>
|
|
116
|
+
<dt className="text-gray-500">Phone</dt>
|
|
117
|
+
<dd className="text-gray-900">{selectedWorker.phone ?? "—"}</dd>
|
|
118
|
+
</div>
|
|
119
|
+
<div>
|
|
120
|
+
<dt className="text-gray-500">Status</dt>
|
|
121
|
+
<dd className="text-gray-900">{selectedWorker.status ?? "—"}</dd>
|
|
122
|
+
</div>
|
|
123
|
+
</dl>
|
|
124
|
+
<div className="mt-6 flex justify-end">
|
|
125
|
+
<button
|
|
126
|
+
type="button"
|
|
127
|
+
onClick={() => setSelectedWorker(null)}
|
|
128
|
+
className="px-4 py-2 text-sm font-medium text-purple-700 hover:bg-purple-50 rounded-md"
|
|
129
|
+
>
|
|
130
|
+
Close
|
|
131
|
+
</button>
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
)}
|
|
136
|
+
</>
|
|
137
|
+
);
|
|
138
|
+
}
|
package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/pages/Properties.tsx
CHANGED
|
@@ -1,114 +1,195 @@
|
|
|
1
|
-
import { useEffect, useState } from "react";
|
|
2
|
-
import {
|
|
1
|
+
import { useEffect, useState, useRef, useMemo, useCallback } from "react";
|
|
2
|
+
import { useSearchParams } from "react-router";
|
|
3
|
+
import {
|
|
4
|
+
useObjectListMetadata,
|
|
5
|
+
useRecordListGraphQL,
|
|
6
|
+
type FilterCriteria,
|
|
7
|
+
} from "@salesforce/webapp-template-feature-react-global-search-experimental";
|
|
3
8
|
import type { Property } from "../lib/types.js";
|
|
4
9
|
import { PropertyCard } from "../components/PropertyCard.js";
|
|
5
10
|
import { PropertyDetailsModal } from "../components/PropertyDetailsModal.js";
|
|
6
|
-
import {
|
|
11
|
+
import { PageContainer } from "../components/layout/PageContainer.js";
|
|
12
|
+
import { PageHeader } from "../components/layout/PageHeader.js";
|
|
13
|
+
import { PageLoadingState } from "../components/feedback/PageLoadingState.js";
|
|
14
|
+
import { PageErrorState } from "../components/feedback/PageErrorState.js";
|
|
15
|
+
import { FilterErrorAlert } from "../components/feedback/FilterErrorAlert.js";
|
|
16
|
+
import { ListPageFilterRow } from "../components/filters/ListPageFilterRow.js";
|
|
17
|
+
import { GLOBAL_SEARCH_OBJECT_API_NAME } from "../lib/globalSearchConstants.js";
|
|
18
|
+
import { nodeToProperty } from "../lib/propertyAdapter.js";
|
|
19
|
+
import { getPropertyListColumns } from "../lib/propertyColumns.js";
|
|
20
|
+
import {
|
|
21
|
+
buildFilterCriteriaFromFormValues,
|
|
22
|
+
getDefaultFilterFormValues,
|
|
23
|
+
getApplicableFilters,
|
|
24
|
+
} from "../lib/filterUtils.js";
|
|
25
|
+
import { PAGE_SIZE_LIST, PROPERTY_FILTER_EXCLUDED_FIELD_PATHS } from "../lib/constants.js";
|
|
26
|
+
import { useAccumulatedListPages } from "../hooks/useAccumulatedListPages.js";
|
|
27
|
+
|
|
28
|
+
const PROPERTIES_DEFAULT_SORT = "CreatedDate DESC";
|
|
29
|
+
|
|
30
|
+
const mapNodeToProperty = (node: unknown) =>
|
|
31
|
+
nodeToProperty(node as Record<string, unknown> | undefined);
|
|
7
32
|
|
|
8
33
|
export default function Properties() {
|
|
9
|
-
const [
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
const [
|
|
13
|
-
const [
|
|
34
|
+
const [searchParams] = useSearchParams();
|
|
35
|
+
const searchQuery = searchParams.get("q") ?? "";
|
|
36
|
+
|
|
37
|
+
const [afterCursor, setAfterCursor] = useState<string | null>(null);
|
|
38
|
+
const [appliedFilters, setAppliedFilters] = useState<FilterCriteria[]>([]);
|
|
39
|
+
const [filterFormValues, setFilterFormValues] = useState<Record<string, string>>({});
|
|
40
|
+
const [filterError, setFilterError] = useState<string | null>(null);
|
|
14
41
|
const [selectedProperty, setSelectedProperty] = useState<Property | null>(null);
|
|
42
|
+
const hasInitializedFiltersRef = useRef(false);
|
|
43
|
+
|
|
44
|
+
const listMeta = useObjectListMetadata(GLOBAL_SEARCH_OBJECT_API_NAME);
|
|
45
|
+
const columns = useMemo(() => getPropertyListColumns(listMeta.columns), [listMeta.columns]);
|
|
46
|
+
const filters = useMemo(
|
|
47
|
+
() => getApplicableFilters(listMeta.filters ?? [], PROPERTY_FILTER_EXCLUDED_FIELD_PATHS),
|
|
48
|
+
[listMeta.filters],
|
|
49
|
+
);
|
|
50
|
+
const picklistValues = listMeta.picklistValues ?? {};
|
|
51
|
+
|
|
52
|
+
const {
|
|
53
|
+
edges,
|
|
54
|
+
pageInfo,
|
|
55
|
+
loading: resultsLoading,
|
|
56
|
+
error: resultsError,
|
|
57
|
+
} = useRecordListGraphQL({
|
|
58
|
+
objectApiName: GLOBAL_SEARCH_OBJECT_API_NAME,
|
|
59
|
+
columns,
|
|
60
|
+
columnsLoading: listMeta.loading,
|
|
61
|
+
columnsError: listMeta.error,
|
|
62
|
+
first: PAGE_SIZE_LIST,
|
|
63
|
+
after: afterCursor,
|
|
64
|
+
searchQuery: searchQuery.trim() || undefined,
|
|
65
|
+
sortBy: PROPERTIES_DEFAULT_SORT,
|
|
66
|
+
filters: appliedFilters,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const [accumulated, setAccumulated] = useAccumulatedListPages(
|
|
70
|
+
edges,
|
|
71
|
+
resultsLoading,
|
|
72
|
+
afterCursor,
|
|
73
|
+
mapNodeToProperty,
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
if (filters.length === 0) return;
|
|
78
|
+
if (!hasInitializedFiltersRef.current) {
|
|
79
|
+
hasInitializedFiltersRef.current = true;
|
|
80
|
+
setFilterFormValues(getDefaultFilterFormValues(filters));
|
|
81
|
+
}
|
|
82
|
+
}, [filters]);
|
|
15
83
|
|
|
16
84
|
useEffect(() => {
|
|
17
|
-
|
|
85
|
+
setAfterCursor(null);
|
|
86
|
+
setAccumulated([]);
|
|
87
|
+
}, [searchQuery, appliedFilters, setAccumulated]);
|
|
88
|
+
|
|
89
|
+
const hasNextPage = Boolean(pageInfo?.hasNextPage);
|
|
90
|
+
const endCursor = pageInfo?.endCursor ?? null;
|
|
91
|
+
|
|
92
|
+
const handleLoadMore = useCallback(() => {
|
|
93
|
+
if (endCursor && !searchQuery.trim()) setAfterCursor(endCursor);
|
|
94
|
+
}, [endCursor, searchQuery]);
|
|
95
|
+
|
|
96
|
+
const handlePropertyClick = useCallback((property: Property) => {
|
|
97
|
+
setSelectedProperty(property);
|
|
18
98
|
}, []);
|
|
19
99
|
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
setLoading(false);
|
|
31
|
-
}
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
const loadMoreProperties = async () => {
|
|
35
|
-
if (!endCursor) return;
|
|
36
|
-
|
|
37
|
-
try {
|
|
38
|
-
setLoadingMore(true);
|
|
39
|
-
const result = await getProperties(12, endCursor);
|
|
40
|
-
setProperties((prev) => [...prev, ...result.properties]);
|
|
41
|
-
setHasNextPage(result.pageInfo.hasNextPage);
|
|
42
|
-
setEndCursor(result.pageInfo.endCursor!);
|
|
43
|
-
} catch (error) {
|
|
44
|
-
console.error("Error loading more properties:", error);
|
|
45
|
-
} finally {
|
|
46
|
-
setLoadingMore(false);
|
|
100
|
+
const handleApplyFilters = useCallback(() => {
|
|
101
|
+
setFilterError(null);
|
|
102
|
+
const result = buildFilterCriteriaFromFormValues(
|
|
103
|
+
GLOBAL_SEARCH_OBJECT_API_NAME,
|
|
104
|
+
filters,
|
|
105
|
+
filterFormValues,
|
|
106
|
+
);
|
|
107
|
+
if (result.rangeError) {
|
|
108
|
+
setFilterError(result.rangeError);
|
|
109
|
+
return;
|
|
47
110
|
}
|
|
48
|
-
|
|
111
|
+
setAppliedFilters(result.criteria);
|
|
112
|
+
setAfterCursor(null);
|
|
113
|
+
}, [filters, filterFormValues]);
|
|
49
114
|
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
115
|
+
const handleResetFilters = useCallback(() => {
|
|
116
|
+
setFilterFormValues(getDefaultFilterFormValues(filters));
|
|
117
|
+
setAppliedFilters([]);
|
|
118
|
+
setAfterCursor(null);
|
|
119
|
+
setFilterError(null);
|
|
120
|
+
}, [filters]);
|
|
53
121
|
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
122
|
+
const handleFilterFormValueChange = useCallback((key: string, value: string) => {
|
|
123
|
+
setFilterFormValues((prev) => ({ ...prev, [key]: value }));
|
|
124
|
+
setFilterError(null);
|
|
125
|
+
}, []);
|
|
57
126
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
127
|
+
const loading = listMeta.loading || resultsLoading;
|
|
128
|
+
const error = listMeta.error ?? resultsError;
|
|
129
|
+
|
|
130
|
+
if (error) {
|
|
131
|
+
return <PageErrorState message={error} />;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (loading && accumulated.length === 0) {
|
|
135
|
+
return <PageLoadingState message="Loading properties..." />;
|
|
64
136
|
}
|
|
65
137
|
|
|
66
138
|
return (
|
|
67
|
-
|
|
68
|
-
<
|
|
69
|
-
|
|
70
|
-
<div className="
|
|
71
|
-
<
|
|
72
|
-
|
|
73
|
-
|
|
139
|
+
<>
|
|
140
|
+
<PageHeader title="Properties" description="Browse and manage available properties" />
|
|
141
|
+
<PageContainer>
|
|
142
|
+
<div className="max-w-7xl mx-auto space-y-6">
|
|
143
|
+
<ListPageFilterRow
|
|
144
|
+
filters={filters}
|
|
145
|
+
picklistValues={picklistValues}
|
|
146
|
+
formValues={filterFormValues}
|
|
147
|
+
onFormValueChange={handleFilterFormValueChange}
|
|
148
|
+
onApply={handleApplyFilters}
|
|
149
|
+
onReset={handleResetFilters}
|
|
150
|
+
ariaLabel="Properties filters"
|
|
151
|
+
/>
|
|
152
|
+
{filterError && <FilterErrorAlert message={filterError} />}
|
|
74
153
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
<p className="text-gray-500 text-lg">No properties found</p>
|
|
79
|
-
</div>
|
|
80
|
-
) : (
|
|
81
|
-
<>
|
|
82
|
-
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
83
|
-
{properties.map((property) => (
|
|
84
|
-
<PropertyCard key={property.id} property={property} onClick={handlePropertyClick} />
|
|
85
|
-
))}
|
|
154
|
+
{accumulated.length === 0 ? (
|
|
155
|
+
<div className="text-center py-12">
|
|
156
|
+
<p className="text-gray-500 text-lg">No properties found</p>
|
|
86
157
|
</div>
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
</Button>
|
|
158
|
+
) : (
|
|
159
|
+
<>
|
|
160
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
161
|
+
{accumulated.map((property) => (
|
|
162
|
+
<PropertyCard
|
|
163
|
+
key={property.id}
|
|
164
|
+
property={property}
|
|
165
|
+
onClick={handlePropertyClick}
|
|
166
|
+
/>
|
|
167
|
+
))}
|
|
98
168
|
</div>
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
169
|
+
{hasNextPage && !searchQuery.trim() && (
|
|
170
|
+
<div className="flex justify-center mt-6">
|
|
171
|
+
<button
|
|
172
|
+
type="button"
|
|
173
|
+
onClick={handleLoadMore}
|
|
174
|
+
disabled={resultsLoading}
|
|
175
|
+
className="px-6 py-2 bg-purple-700 hover:bg-purple-800 text-white rounded-lg text-sm font-medium disabled:opacity-50"
|
|
176
|
+
>
|
|
177
|
+
{resultsLoading ? "Loading..." : "Load More"}
|
|
178
|
+
</button>
|
|
179
|
+
</div>
|
|
180
|
+
)}
|
|
181
|
+
</>
|
|
182
|
+
)}
|
|
183
|
+
</div>
|
|
102
184
|
|
|
103
|
-
{/* Property Details Modal */}
|
|
104
185
|
{selectedProperty && (
|
|
105
186
|
<PropertyDetailsModal
|
|
106
187
|
property={selectedProperty}
|
|
107
188
|
isOpen={!!selectedProperty}
|
|
108
|
-
onClose={
|
|
189
|
+
onClose={() => setSelectedProperty(null)}
|
|
109
190
|
/>
|
|
110
191
|
)}
|
|
111
|
-
</
|
|
112
|
-
|
|
192
|
+
</PageContainer>
|
|
193
|
+
</>
|
|
113
194
|
);
|
|
114
195
|
}
|