@salesforce/ui-bundle-template-app-react-sample-b2x 3.0.0 → 3.1.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 +9 -0
- package/dist/README.md +77 -27
- package/dist/force-app/main/default/applications/PropertyManagement.app-meta.xml +33 -0
- package/dist/force-app/main/default/data/Application__c.json +10 -10
- package/dist/force-app/main/default/data/data-plan.json +0 -6
- package/dist/force-app/main/default/objects/Application__c/fields/User__c.field-meta.xml +2 -2
- package/dist/force-app/main/default/objects/Property__c/Property__c.object-meta.xml +1 -1
- package/dist/force-app/main/default/permissionsets/Property_Management_Access.permissionset-meta.xml +76 -0
- package/dist/force-app/main/default/permissionsets/Property_Rental_Guest_User_Access.permissionset-meta.xml +217 -0
- package/dist/force-app/main/default/permissionsets/Tenant_Maintenance_Access.permissionset-meta.xml +1 -1
- package/dist/force-app/main/default/profiles/Property Rental Prospect Profile.profile-meta.xml +295 -0
- package/dist/force-app/main/default/roles/Admin.role-meta.xml +9 -0
- package/dist/force-app/main/default/sharingrules/Property__c.sharingRules-meta.xml +17 -0
- package/dist/force-app/main/default/tabs/Agent__c.tab-meta.xml +5 -0
- package/dist/force-app/main/default/tabs/Application__c.tab-meta.xml +5 -0
- package/dist/force-app/main/default/tabs/KPI_Snapshot__c.tab-meta.xml +5 -0
- package/dist/force-app/main/default/tabs/Lease__c.tab-meta.xml +5 -0
- package/dist/force-app/main/default/tabs/Maintenance_Request__c.tab-meta.xml +5 -0
- package/dist/force-app/main/default/tabs/Maintenance_Worker__c.tab-meta.xml +5 -0
- package/dist/force-app/main/default/tabs/Notification__c.tab-meta.xml +5 -0
- package/dist/force-app/main/default/tabs/Payment__c.tab-meta.xml +5 -0
- package/dist/force-app/main/default/tabs/Property_Cost__c.tab-meta.xml +5 -0
- package/dist/force-app/main/default/tabs/Property_Feature__c.tab-meta.xml +5 -0
- package/dist/force-app/main/default/tabs/Property_Image__c.tab-meta.xml +5 -0
- package/dist/force-app/main/default/tabs/Property_Listing__c.tab-meta.xml +5 -0
- package/dist/force-app/main/default/tabs/Property_Management_Company__c.tab-meta.xml +5 -0
- package/dist/force-app/main/default/tabs/Property_Owner__c.tab-meta.xml +5 -0
- package/dist/force-app/main/default/tabs/Property_Sale__c.tab-meta.xml +5 -0
- package/dist/force-app/main/default/tabs/Property__c.tab-meta.xml +5 -0
- package/dist/force-app/main/default/tabs/Tenant__c.tab-meta.xml +5 -0
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/api/applications/applicationApi.ts +5 -6
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/api/graphql-operations-types.ts +14751 -2937
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/api/maintenanceRequests/maintenanceRequestApi.ts +4 -0
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/api/query/tenantProperties.graphql +24 -0
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/api/tenants/tenantApi.ts +23 -0
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/components/alerts/status-alert.tsx +11 -8
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/components/ui/input.tsx +1 -1
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/context/TenantAccessContext.tsx +24 -12
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/authentication/api/userProfileApi.ts +2 -1
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/authentication/authenticationConfig.ts +9 -9
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/authentication/context/AuthContext.tsx +21 -4
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/authentication/forms/auth-form.tsx +15 -1
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/authentication/hooks/form.tsx +1 -1
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/authentication/layouts/privateRouteLayout.tsx +2 -11
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/authentication/pages/ChangePassword.tsx +20 -4
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/authentication/pages/ForgotPassword.tsx +19 -4
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/authentication/pages/Login.tsx +19 -4
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/authentication/pages/Profile.tsx +80 -43
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/authentication/pages/Register.tsx +15 -4
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/authentication/pages/ResetPassword.tsx +19 -4
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/authentication/utils/helpers.ts +15 -52
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/object-search/__examples__/pages/AccountObjectDetailPage.tsx +2 -4
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/object-search/__examples__/pages/AccountSearch.tsx +7 -15
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/object-search/components/filters/NumericRangeFilter.tsx +9 -5
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/object-search/components/filters/SearchFilter.tsx +5 -3
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/object-search/components/filters/TextFilter.tsx +5 -3
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/object-search/hooks/useAsyncData.ts +11 -4
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/hooks/useAsyncData.ts +67 -0
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/hooks/useGeocode.ts +5 -9
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/hooks/useMaintenanceRequests.ts +3 -11
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/hooks/usePropertyDetail.ts +6 -17
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/hooks/usePropertyMapMarkers.ts +43 -34
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/hooks/useWeather.ts +2 -3
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/pages/Application.tsx +45 -44
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/pages/Contact.tsx +5 -9
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/pages/Home.tsx +2 -3
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/pages/Maintenance.tsx +43 -15
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/pages/PropertySearch.tsx +21 -19
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/routes.tsx +19 -25
- package/dist/package-lock.json +2 -2
- package/dist/package.json +1 -1
- package/dist/scripts/org-setup.config.json +18 -3
- package/dist/scripts/org-setup.mjs +528 -44
- package/package.json +1 -1
- package/dist/force-app/main/default/data/Contact.json +0 -44
- package/dist/force-app/main/default/scripts/org-setup.config.json +0 -9
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/api/query/tenantAccess.graphql +0 -13
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/api/tenantApi.ts +0 -12
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/authentication/layout/card-skeleton.tsx +0 -38
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/authentication/layouts/authenticationRouteLayout.tsx +0 -21
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/object-search/hooks/useCachedAsyncData.ts +0 -188
package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/hooks/useMaintenanceRequests.ts
CHANGED
|
@@ -7,12 +7,7 @@ import {
|
|
|
7
7
|
type MaintenanceRequestNode,
|
|
8
8
|
} from "@/api/maintenanceRequests/maintenanceRequestApi";
|
|
9
9
|
import { ResultOrder } from "@/api/graphql-operations-types";
|
|
10
|
-
import {
|
|
11
|
-
useCachedAsyncData,
|
|
12
|
-
clearCacheEntry,
|
|
13
|
-
} from "@/features/object-search/hooks/useCachedAsyncData";
|
|
14
|
-
|
|
15
|
-
const CACHE_KEY = "maintenance-requests";
|
|
10
|
+
import { useAsyncData } from "@/hooks/useAsyncData";
|
|
16
11
|
|
|
17
12
|
async function fetchMaintenanceNodes(): Promise<MaintenanceRequestNode[]> {
|
|
18
13
|
const result = await searchMaintenanceRequests({
|
|
@@ -30,14 +25,11 @@ export function useMaintenanceRequests(): {
|
|
|
30
25
|
} {
|
|
31
26
|
const [generation, setGeneration] = useState(0);
|
|
32
27
|
|
|
33
|
-
const { data, loading, error } =
|
|
34
|
-
key: `${CACHE_KEY}:${generation}`,
|
|
35
|
-
});
|
|
28
|
+
const { data, loading, error } = useAsyncData(fetchMaintenanceNodes, [generation]);
|
|
36
29
|
|
|
37
30
|
const refetch = useCallback(() => {
|
|
38
|
-
clearCacheEntry(`${CACHE_KEY}:${generation}`);
|
|
39
31
|
setGeneration((g) => g + 1);
|
|
40
|
-
}, [
|
|
32
|
+
}, []);
|
|
41
33
|
|
|
42
34
|
return { requests: data ?? [], loading, error, refetch };
|
|
43
35
|
}
|
package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/hooks/usePropertyDetail.ts
CHANGED
|
@@ -8,10 +8,7 @@ import {
|
|
|
8
8
|
fetchListingById,
|
|
9
9
|
type PropertyDetailNode,
|
|
10
10
|
} from "@/api/properties/propertyDetailGraphQL";
|
|
11
|
-
import {
|
|
12
|
-
useCachedAsyncData,
|
|
13
|
-
clearCacheEntry,
|
|
14
|
-
} from "@/features/object-search/hooks/useCachedAsyncData";
|
|
11
|
+
import { useAsyncData } from "@/hooks/useAsyncData";
|
|
15
12
|
|
|
16
13
|
export interface PropertyDetailState {
|
|
17
14
|
property: PropertyDetailNode | null;
|
|
@@ -19,8 +16,6 @@ export interface PropertyDetailState {
|
|
|
19
16
|
error: string | null;
|
|
20
17
|
}
|
|
21
18
|
|
|
22
|
-
const CACHE_KEY_PREFIX = "property-detail";
|
|
23
|
-
|
|
24
19
|
async function fetchDetail(id: string): Promise<PropertyDetailNode | null> {
|
|
25
20
|
// First try directly as a Property__c ID (common path).
|
|
26
21
|
const detail = await fetchPropertyDetailById(id);
|
|
@@ -38,21 +33,15 @@ export function usePropertyDetail(
|
|
|
38
33
|
): PropertyDetailState & { refetch: () => void } {
|
|
39
34
|
const [generation, setGeneration] = useState(0);
|
|
40
35
|
const trimmedId = id?.trim() ?? "";
|
|
41
|
-
const cacheKey = `${CACHE_KEY_PREFIX}:${trimmedId}:${generation}`;
|
|
42
36
|
|
|
43
|
-
const { data, loading, error } =
|
|
44
|
-
()
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
},
|
|
48
|
-
[trimmedId, generation],
|
|
49
|
-
{ key: cacheKey },
|
|
50
|
-
);
|
|
37
|
+
const { data, loading, error } = useAsyncData(() => {
|
|
38
|
+
if (!trimmedId) return Promise.resolve(null);
|
|
39
|
+
return fetchDetail(trimmedId);
|
|
40
|
+
}, [trimmedId, generation]);
|
|
51
41
|
|
|
52
42
|
const refetch = useCallback(() => {
|
|
53
|
-
clearCacheEntry(cacheKey);
|
|
54
43
|
setGeneration((g) => g + 1);
|
|
55
|
-
}, [
|
|
44
|
+
}, []);
|
|
56
45
|
|
|
57
46
|
return {
|
|
58
47
|
property: data ?? null,
|
package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/hooks/usePropertyMapMarkers.ts
CHANGED
|
@@ -82,47 +82,53 @@ export function usePropertyMapMarkers(results: PropertySearchNode[]): {
|
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
84
|
const idsKey = [...new Set(propertyIds)].join(",");
|
|
85
|
+
const uniqIds = idsKey === "" ? [] : idsKey.split(",");
|
|
85
86
|
|
|
86
|
-
|
|
87
|
+
// Compute direct markers synchronously (nodes that already have coordinates)
|
|
88
|
+
const directMarkers: MapMarker[] = [];
|
|
89
|
+
const missingIds: string[] = [];
|
|
90
|
+
for (const node of results) {
|
|
91
|
+
if (!uniqIds.includes(node.Id)) continue;
|
|
92
|
+
const coords = getCoordinatesFromNode(node);
|
|
93
|
+
if (coords) {
|
|
94
|
+
directMarkers.push({
|
|
95
|
+
lat: coords.lat,
|
|
96
|
+
lng: coords.lng,
|
|
97
|
+
label: propertyIdToLabel.get(node.Id) ?? "Property",
|
|
98
|
+
propertyId: node.Id,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
for (const id of uniqIds) {
|
|
103
|
+
const hasDirect = directMarkers.some((m) => m.propertyId === id);
|
|
104
|
+
if (!hasDirect) missingIds.push(id);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Handle cases that don't need async work during render
|
|
108
|
+
const [prevIdsKey, setPrevIdsKey] = useState(idsKey);
|
|
109
|
+
if (prevIdsKey !== idsKey) {
|
|
110
|
+
setPrevIdsKey(idsKey);
|
|
111
|
+
if (uniqIds.length === 0) {
|
|
112
|
+
if (markers.length > 0) setMarkers([]);
|
|
113
|
+
if (loading) setLoading(false);
|
|
114
|
+
} else if (missingIds.length === 0) {
|
|
115
|
+
setMarkers(spreadDuplicateMarkers(directMarkers));
|
|
116
|
+
if (loading) setLoading(false);
|
|
117
|
+
} else {
|
|
118
|
+
if (!loading) setLoading(true);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Stable reference to labels for use in the async effect
|
|
87
123
|
const labelMapRef = useRef(propertyIdToLabel);
|
|
88
124
|
useEffect(() => {
|
|
89
|
-
resultsRef.current = results;
|
|
90
125
|
labelMapRef.current = propertyIdToLabel;
|
|
91
126
|
});
|
|
92
127
|
|
|
93
128
|
useEffect(() => {
|
|
94
|
-
|
|
95
|
-
if (uniqIds.length === 0) {
|
|
96
|
-
setMarkers([]);
|
|
97
|
-
setLoading(false);
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
129
|
+
if (uniqIds.length === 0 || missingIds.length === 0) return;
|
|
100
130
|
let cancelled = false;
|
|
101
|
-
setLoading(true);
|
|
102
131
|
const currentLabels = labelMapRef.current;
|
|
103
|
-
const directMarkers: MapMarker[] = [];
|
|
104
|
-
const missingIds: string[] = [];
|
|
105
|
-
for (const node of results) {
|
|
106
|
-
if (!uniqIds.includes(node.Id)) continue;
|
|
107
|
-
const coords = getCoordinatesFromNode(node);
|
|
108
|
-
if (coords) {
|
|
109
|
-
directMarkers.push({
|
|
110
|
-
lat: coords.lat,
|
|
111
|
-
lng: coords.lng,
|
|
112
|
-
label: propertyIdToLabel.get(node.Id) ?? "Property",
|
|
113
|
-
propertyId: node.Id,
|
|
114
|
-
});
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
for (const id of uniqIds) {
|
|
118
|
-
const hasDirect = directMarkers.some((m) => m.propertyId === id);
|
|
119
|
-
if (!hasDirect) missingIds.push(id);
|
|
120
|
-
}
|
|
121
|
-
if (missingIds.length === 0) {
|
|
122
|
-
setMarkers(spreadDuplicateMarkers(directMarkers));
|
|
123
|
-
setLoading(false);
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
126
132
|
fetchPropertyAddresses(missingIds)
|
|
127
133
|
.then((idToAddress) => {
|
|
128
134
|
if (cancelled) return;
|
|
@@ -130,8 +136,10 @@ export function usePropertyMapMarkers(results: PropertySearchNode[]): {
|
|
|
130
136
|
([, addr]) => addr != null && addr.trim() !== "",
|
|
131
137
|
);
|
|
132
138
|
if (toGeocode.length === 0) {
|
|
133
|
-
|
|
134
|
-
|
|
139
|
+
if (!cancelled) {
|
|
140
|
+
setMarkers(spreadDuplicateMarkers(directMarkers));
|
|
141
|
+
setLoading(false);
|
|
142
|
+
}
|
|
135
143
|
return;
|
|
136
144
|
}
|
|
137
145
|
Promise.all(
|
|
@@ -175,6 +183,7 @@ export function usePropertyMapMarkers(results: PropertySearchNode[]): {
|
|
|
175
183
|
return () => {
|
|
176
184
|
cancelled = true;
|
|
177
185
|
};
|
|
186
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps --- only re-run when IDs change
|
|
178
187
|
}, [idsKey]);
|
|
179
188
|
|
|
180
189
|
return { markers, loading };
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Detects user location via Geolocation API; falls back to San Francisco.
|
|
4
4
|
*/
|
|
5
5
|
import { useState, useEffect } from "react";
|
|
6
|
-
import {
|
|
6
|
+
import { useAsyncData } from "@/hooks/useAsyncData";
|
|
7
7
|
|
|
8
8
|
const FALLBACK = {
|
|
9
9
|
LAT: 37.7749,
|
|
@@ -297,10 +297,9 @@ export function useWeather(lat?: number | null, lng?: number | null) {
|
|
|
297
297
|
const longitude = lng ?? geo.longitude;
|
|
298
298
|
const canFetch = lat != null || geo.resolved;
|
|
299
299
|
|
|
300
|
-
const cached =
|
|
300
|
+
const cached = useAsyncData(
|
|
301
301
|
() => fetchWeather(latitude, longitude),
|
|
302
302
|
[latitude, longitude, canFetch],
|
|
303
|
-
{ key: `weather:${latitude},${longitude}:${canFetch}`, ttl: 300_000 },
|
|
304
303
|
);
|
|
305
304
|
|
|
306
305
|
if (!canFetch) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useSearchParams, Link } from "react-router";
|
|
2
|
-
import { useState, type ChangeEvent, type SubmitEvent } from "react";
|
|
2
|
+
import { useCallback, useState, type ChangeEvent, type SubmitEvent } from "react";
|
|
3
3
|
import { Button } from "../components/ui/button";
|
|
4
4
|
import { Input } from "../components/ui/input";
|
|
5
5
|
import { Label } from "../components/ui/label";
|
|
@@ -9,8 +9,7 @@ import { SkeletonField } from "@/components/SkeletonPrimitives";
|
|
|
9
9
|
import { fetchPropertyDetailById } from "@/api/properties/propertyDetailGraphQL";
|
|
10
10
|
import { createApplicationRecord } from "@/api/applications/applicationApi";
|
|
11
11
|
import { useAuth } from "@/features/authentication/context/AuthContext";
|
|
12
|
-
import {
|
|
13
|
-
import { useCachedAsyncData } from "@/features/object-search/hooks/useCachedAsyncData";
|
|
12
|
+
import { useAsyncData } from "@/hooks/useAsyncData";
|
|
14
13
|
|
|
15
14
|
function ApplicationSkeleton() {
|
|
16
15
|
return (
|
|
@@ -46,28 +45,14 @@ export default function Application() {
|
|
|
46
45
|
const [searchParams] = useSearchParams();
|
|
47
46
|
const propertyId = searchParams.get("propertyId") ?? "";
|
|
48
47
|
|
|
49
|
-
const { data: contactData } = useCachedAsyncData(
|
|
50
|
-
() => {
|
|
51
|
-
if (!user?.id) return Promise.resolve(null);
|
|
52
|
-
return fetchUserContact<{ ContactId?: string }>(user.id);
|
|
53
|
-
},
|
|
54
|
-
[user?.id],
|
|
55
|
-
{ key: `contact:${user?.id ?? ""}`, ttl: 300_000 },
|
|
56
|
-
);
|
|
57
|
-
const contactId = contactData?.ContactId ?? null;
|
|
58
|
-
|
|
59
48
|
const {
|
|
60
49
|
data: property,
|
|
61
50
|
loading,
|
|
62
51
|
error: loadError,
|
|
63
|
-
} =
|
|
64
|
-
()
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
},
|
|
68
|
-
[propertyId],
|
|
69
|
-
{ key: `app-property:${propertyId}` },
|
|
70
|
-
);
|
|
52
|
+
} = useAsyncData(() => {
|
|
53
|
+
if (!propertyId?.trim()) return Promise.resolve(null);
|
|
54
|
+
return fetchPropertyDetailById(propertyId);
|
|
55
|
+
}, [propertyId]);
|
|
71
56
|
|
|
72
57
|
const listing = property?.Property_Listings__r?.edges?.[0]?.node ?? null;
|
|
73
58
|
const images = (property?.Property_Images__r?.edges ?? []).flatMap((e) =>
|
|
@@ -85,40 +70,56 @@ export default function Application() {
|
|
|
85
70
|
|
|
86
71
|
const [submitting, setSubmitting] = useState(false);
|
|
87
72
|
const [submitError, setSubmitError] = useState<string | null>(null);
|
|
88
|
-
const [
|
|
73
|
+
const [submittedName, setSubmittedName] = useState<string | null>(null);
|
|
89
74
|
|
|
90
|
-
const handleSubmit =
|
|
91
|
-
e
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
75
|
+
const handleSubmit = useCallback(
|
|
76
|
+
async (e: SubmitEvent<HTMLFormElement>) => {
|
|
77
|
+
e.preventDefault();
|
|
78
|
+
setSubmitError(null);
|
|
79
|
+
setSubmitting(true);
|
|
80
|
+
try {
|
|
81
|
+
const result = await createApplicationRecord({
|
|
82
|
+
Property__c: propertyId || null,
|
|
83
|
+
Status__c: "Submitted",
|
|
84
|
+
User__c: user?.id || "",
|
|
85
|
+
Start_Date__c: moveInDate.trim() || null,
|
|
86
|
+
Employment__c: employment.trim() || null,
|
|
87
|
+
References__c: references.trim() || null,
|
|
88
|
+
});
|
|
89
|
+
setSubmittedName(result.name ?? result.id);
|
|
90
|
+
} catch (err) {
|
|
91
|
+
setSubmitError(err instanceof Error ? err.message : "Failed to submit application.");
|
|
92
|
+
} finally {
|
|
93
|
+
setSubmitting(false);
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
[propertyId, moveInDate, employment, references, user],
|
|
97
|
+
);
|
|
110
98
|
|
|
111
99
|
if (loading) {
|
|
112
100
|
return <ApplicationSkeleton />;
|
|
113
101
|
}
|
|
114
102
|
|
|
115
|
-
if (
|
|
103
|
+
if (submittedName) {
|
|
116
104
|
return (
|
|
117
105
|
<div className="mx-auto max-w-[900px]">
|
|
118
106
|
<Card className="mb-6 rounded-2xl border border-border p-6 shadow-sm">
|
|
119
107
|
<h2 className="mb-2 text-2xl font-semibold text-foreground">Application submitted</h2>
|
|
120
108
|
<p className="text-sm text-muted-foreground">
|
|
121
|
-
Your application
|
|
109
|
+
Your application
|
|
110
|
+
{propertyName ? (
|
|
111
|
+
<>
|
|
112
|
+
{" "}
|
|
113
|
+
for <span className="font-medium text-foreground">{propertyName}</span>
|
|
114
|
+
</>
|
|
115
|
+
) : null}{" "}
|
|
116
|
+
has been submitted.
|
|
117
|
+
{submittedName && (
|
|
118
|
+
<>
|
|
119
|
+
{" "}
|
|
120
|
+
Reference: <span className="font-medium text-foreground">{submittedName}</span>
|
|
121
|
+
</>
|
|
122
|
+
)}
|
|
122
123
|
</p>
|
|
123
124
|
<div className="mt-4 flex gap-2 items-center">
|
|
124
125
|
<Link to="/properties" className="text-sm text-primary no-underline hover:underline">
|
|
@@ -9,7 +9,7 @@ import { Loader2 } from "lucide-react";
|
|
|
9
9
|
import { createContactUsLead } from "@/api/leads/leadApi";
|
|
10
10
|
import { useAuth } from "@/features/authentication/context/AuthContext";
|
|
11
11
|
import { fetchUserProfile } from "@/features/authentication/api/userProfileApi";
|
|
12
|
-
import {
|
|
12
|
+
import { useAsyncData } from "@/hooks/useAsyncData";
|
|
13
13
|
import type { UserInfo } from "@/api/leads/leadApi";
|
|
14
14
|
|
|
15
15
|
function SuccessCard() {
|
|
@@ -167,14 +167,10 @@ export default function Contact() {
|
|
|
167
167
|
const [submitError, setSubmitError] = useState<string | null>(null);
|
|
168
168
|
const [submitted, setSubmitted] = useState(false);
|
|
169
169
|
|
|
170
|
-
const { data: userInfo } =
|
|
171
|
-
()
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
},
|
|
175
|
-
[isAuthenticated, user?.id],
|
|
176
|
-
{ key: `contact-user-profile:${user?.id ?? ""}`, ttl: 300_000 },
|
|
177
|
-
);
|
|
170
|
+
const { data: userInfo } = useAsyncData(() => {
|
|
171
|
+
if (!isAuthenticated || !user?.id) return Promise.resolve(null);
|
|
172
|
+
return fetchUserProfile<UserInfo>(user.id);
|
|
173
|
+
}, [isAuthenticated, user?.id]);
|
|
178
174
|
|
|
179
175
|
const handleSubmit = useCallback(
|
|
180
176
|
async (e: SubmitEvent<HTMLFormElement>) => {
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
extractAmenities,
|
|
14
14
|
extractAddress,
|
|
15
15
|
} from "@/api/properties/propertyNodeUtils";
|
|
16
|
-
import {
|
|
16
|
+
import { useAsyncData } from "@/hooks/useAsyncData";
|
|
17
17
|
import PropertyListingCard, {
|
|
18
18
|
PropertyListingCardSkeleton,
|
|
19
19
|
} from "@/components/properties/PropertyListingCard";
|
|
@@ -102,10 +102,9 @@ export default function Home() {
|
|
|
102
102
|
text: string;
|
|
103
103
|
} | null>(null);
|
|
104
104
|
|
|
105
|
-
const { data: searchResult, loading: featuredLoading } =
|
|
105
|
+
const { data: searchResult, loading: featuredLoading } = useAsyncData<PropertySearchResult>(
|
|
106
106
|
() => searchProperties({ first: FEATURED_PAGE_SIZE }),
|
|
107
107
|
[],
|
|
108
|
-
{ key: "featuredProperties" },
|
|
109
108
|
);
|
|
110
109
|
|
|
111
110
|
const featuredResults = useMemo(
|
|
@@ -13,14 +13,12 @@ import {
|
|
|
13
13
|
type MaintenanceRequestNode,
|
|
14
14
|
} from "@/api/maintenanceRequests/maintenanceRequestApi";
|
|
15
15
|
import { useAuth } from "@/features/authentication/context/AuthContext";
|
|
16
|
+
import { useTenantAccess } from "@/context/TenantAccessContext";
|
|
16
17
|
import {
|
|
17
18
|
useObjectSearchParams,
|
|
18
19
|
type PaginationConfig,
|
|
19
20
|
} from "@/features/object-search/hooks/useObjectSearchParams";
|
|
20
|
-
import {
|
|
21
|
-
useCachedAsyncData,
|
|
22
|
-
clearCacheEntry,
|
|
23
|
-
} from "@/features/object-search/hooks/useCachedAsyncData";
|
|
21
|
+
import { useAsyncData } from "@/hooks/useAsyncData";
|
|
24
22
|
import PaginationControls from "@/features/object-search/components/PaginationControls";
|
|
25
23
|
import type {
|
|
26
24
|
Maintenance_Request__C_Filter,
|
|
@@ -90,6 +88,7 @@ function MaintenanceSkeleton() {
|
|
|
90
88
|
|
|
91
89
|
export default function Maintenance() {
|
|
92
90
|
const { loading: authLoading } = useAuth();
|
|
91
|
+
const { tenantProperties } = useTenantAccess();
|
|
93
92
|
|
|
94
93
|
const { query, pagination } = useObjectSearchParams<
|
|
95
94
|
Maintenance_Request__C_Filter,
|
|
@@ -98,18 +97,11 @@ export default function Maintenance() {
|
|
|
98
97
|
|
|
99
98
|
const [refreshCounter, setRefreshCounter] = useState(0);
|
|
100
99
|
|
|
101
|
-
const searchKey = `maintenance-requests:${JSON.stringify({
|
|
102
|
-
orderBy: query.orderBy,
|
|
103
|
-
first: pagination.pageSize,
|
|
104
|
-
after: pagination.afterCursor,
|
|
105
|
-
refresh: refreshCounter,
|
|
106
|
-
})}`;
|
|
107
|
-
|
|
108
100
|
const {
|
|
109
101
|
data: searchResult,
|
|
110
102
|
loading,
|
|
111
103
|
error,
|
|
112
|
-
} =
|
|
104
|
+
} = useAsyncData(
|
|
113
105
|
() =>
|
|
114
106
|
searchMaintenanceRequests({
|
|
115
107
|
where: query.where,
|
|
@@ -118,7 +110,6 @@ export default function Maintenance() {
|
|
|
118
110
|
after: pagination.afterCursor,
|
|
119
111
|
}),
|
|
120
112
|
[query.where, query.orderBy, pagination.pageSize, pagination.afterCursor, refreshCounter],
|
|
121
|
-
{ key: searchKey },
|
|
122
113
|
);
|
|
123
114
|
|
|
124
115
|
const requests = useMemo(
|
|
@@ -138,6 +129,7 @@ export default function Maintenance() {
|
|
|
138
129
|
const [description, setDescription] = useState("");
|
|
139
130
|
const [type, setType] = useState<string>("");
|
|
140
131
|
const [priority, setPriority] = useState<string>("Standard");
|
|
132
|
+
const [selectedTenantIdx, setSelectedTenantIdx] = useState(0);
|
|
141
133
|
const [dateRequested, setDateRequested] = useState(() => {
|
|
142
134
|
const d = new Date();
|
|
143
135
|
return d.toISOString().slice(0, 10);
|
|
@@ -146,6 +138,8 @@ export default function Maintenance() {
|
|
|
146
138
|
const [submitError, setSubmitError] = useState<string | null>(null);
|
|
147
139
|
const [submitSuccess, setSubmitSuccess] = useState(false);
|
|
148
140
|
|
|
141
|
+
const selectedTenant = tenantProperties[selectedTenantIdx] ?? tenantProperties[0];
|
|
142
|
+
|
|
149
143
|
const handleSubmit = useCallback(
|
|
150
144
|
async (e: React.FormEvent) => {
|
|
151
145
|
e.preventDefault();
|
|
@@ -155,6 +149,10 @@ export default function Maintenance() {
|
|
|
155
149
|
setSubmitError("Title or description is required");
|
|
156
150
|
return;
|
|
157
151
|
}
|
|
152
|
+
if (!selectedTenant) {
|
|
153
|
+
setSubmitError("No property associated with your tenant record");
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
158
156
|
setSubmitting(true);
|
|
159
157
|
setSubmitError(null);
|
|
160
158
|
setSubmitSuccess(false);
|
|
@@ -165,6 +163,8 @@ export default function Maintenance() {
|
|
|
165
163
|
Priority__c: priority,
|
|
166
164
|
Status__c: "New",
|
|
167
165
|
Scheduled__c: dateRequested ? new Date(dateRequested).toISOString() : undefined,
|
|
166
|
+
Property__c: selectedTenant.Property__c.value,
|
|
167
|
+
User__c: selectedTenant.Id,
|
|
168
168
|
});
|
|
169
169
|
setSubmitSuccess(true);
|
|
170
170
|
setTitle("");
|
|
@@ -172,7 +172,6 @@ export default function Maintenance() {
|
|
|
172
172
|
setType("");
|
|
173
173
|
setPriority("Standard");
|
|
174
174
|
setDateRequested(new Date().toISOString().slice(0, 10));
|
|
175
|
-
clearCacheEntry(searchKey);
|
|
176
175
|
setRefreshCounter((c) => c + 1);
|
|
177
176
|
} catch (err) {
|
|
178
177
|
setSubmitError(err instanceof Error ? err.message : "Failed to submit request");
|
|
@@ -180,7 +179,7 @@ export default function Maintenance() {
|
|
|
180
179
|
setSubmitting(false);
|
|
181
180
|
}
|
|
182
181
|
},
|
|
183
|
-
[title, description, type, priority, dateRequested,
|
|
182
|
+
[title, description, type, priority, dateRequested, selectedTenant],
|
|
184
183
|
);
|
|
185
184
|
|
|
186
185
|
if (authLoading) return <MaintenanceSkeleton />;
|
|
@@ -193,6 +192,35 @@ export default function Maintenance() {
|
|
|
193
192
|
</CardHeader>
|
|
194
193
|
<CardContent className="space-y-4">
|
|
195
194
|
<form onSubmit={handleSubmit} className="space-y-4">
|
|
195
|
+
{tenantProperties.length > 1 && (
|
|
196
|
+
<div className="space-y-2">
|
|
197
|
+
<Label htmlFor="maintenance-property">Property *</Label>
|
|
198
|
+
<select
|
|
199
|
+
id="maintenance-property"
|
|
200
|
+
className="flex h-9 w-full rounded-xl border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-[color,box-shadow] duration-200 outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:border-primary"
|
|
201
|
+
aria-label="Property"
|
|
202
|
+
value={selectedTenantIdx}
|
|
203
|
+
onChange={(e: ChangeEvent<HTMLSelectElement>) =>
|
|
204
|
+
setSelectedTenantIdx(Number(e.target.value))
|
|
205
|
+
}
|
|
206
|
+
>
|
|
207
|
+
{tenantProperties.map((tp, idx) => (
|
|
208
|
+
<option key={tp.Property__c.value} value={idx}>
|
|
209
|
+
{tp.Property__r?.Address__c?.value ?? tp.Property__r?.Name?.value}
|
|
210
|
+
</option>
|
|
211
|
+
))}
|
|
212
|
+
</select>
|
|
213
|
+
</div>
|
|
214
|
+
)}
|
|
215
|
+
{tenantProperties.length === 1 && selectedTenant && (
|
|
216
|
+
<div className="space-y-2">
|
|
217
|
+
<Label>Property</Label>
|
|
218
|
+
<p className="text-sm text-muted-foreground">
|
|
219
|
+
{selectedTenant.Property__r?.Address__c?.value ??
|
|
220
|
+
selectedTenant.Property__r?.Name?.value}
|
|
221
|
+
</p>
|
|
222
|
+
</div>
|
|
223
|
+
)}
|
|
196
224
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
|
197
225
|
<div className="space-y-2">
|
|
198
226
|
<Label htmlFor="maintenance-title">Title *</Label>
|
package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/pages/PropertySearch.tsx
CHANGED
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
useObjectSearchParams,
|
|
19
19
|
type PaginationConfig,
|
|
20
20
|
} from "@/features/object-search/hooks/useObjectSearchParams";
|
|
21
|
-
import {
|
|
21
|
+
import { useAsyncData } from "@/hooks/useAsyncData";
|
|
22
22
|
import type { FilterFieldConfig } from "@/features/object-search/utils/filterUtils";
|
|
23
23
|
import type {
|
|
24
24
|
SortFieldConfig,
|
|
@@ -107,18 +107,11 @@ export default function PropertySearch() {
|
|
|
107
107
|
>(FILTER_CONFIGS, SORT_CONFIGS, PAGINATION_CONFIG);
|
|
108
108
|
|
|
109
109
|
/* ── Data fetching ── */
|
|
110
|
-
const searchKey = `rentalProperties:${JSON.stringify({
|
|
111
|
-
where: query.where,
|
|
112
|
-
orderBy: query.orderBy,
|
|
113
|
-
first: pagination.pageSize,
|
|
114
|
-
after: pagination.afterCursor,
|
|
115
|
-
})}`;
|
|
116
|
-
|
|
117
110
|
const {
|
|
118
111
|
data: searchResult,
|
|
119
112
|
loading: resultsLoading,
|
|
120
113
|
error: resultsError,
|
|
121
|
-
} =
|
|
114
|
+
} = useAsyncData<PropertySearchResult>(
|
|
122
115
|
() =>
|
|
123
116
|
searchProperties({
|
|
124
117
|
where: query.where,
|
|
@@ -127,7 +120,6 @@ export default function PropertySearch() {
|
|
|
127
120
|
after: pagination.afterCursor,
|
|
128
121
|
}),
|
|
129
122
|
[query.where, query.orderBy, pagination.pageSize, pagination.afterCursor],
|
|
130
|
-
{ key: searchKey },
|
|
131
123
|
);
|
|
132
124
|
|
|
133
125
|
/* ── Derive results + maps ── */
|
|
@@ -208,20 +200,26 @@ export default function PropertySearch() {
|
|
|
208
200
|
}
|
|
209
201
|
}, SEARCH_FILTER_DEBOUNCE_MS);
|
|
210
202
|
return () => clearTimeout(t);
|
|
211
|
-
}, [searchQuery]);
|
|
203
|
+
}, [searchQuery, filters]);
|
|
212
204
|
|
|
213
205
|
// Sync local searchQuery when the committed value changes externally (e.g. URL nav)
|
|
214
|
-
|
|
206
|
+
const [prevCommittedSearchValue, setPrevCommittedSearchValue] = useState(committedSearchValue);
|
|
207
|
+
if (prevCommittedSearchValue !== committedSearchValue) {
|
|
208
|
+
setPrevCommittedSearchValue(committedSearchValue);
|
|
215
209
|
setSearchQuery(committedSearchValue);
|
|
216
|
-
}
|
|
210
|
+
}
|
|
217
211
|
|
|
218
212
|
const committedPriceFilter = filters.active.find((f) => f.field === "Monthly_Rent__c");
|
|
219
213
|
const [stagedPriceMin, setStagedPriceMin] = useState(committedPriceFilter?.min ?? "");
|
|
220
214
|
const [stagedPriceMax, setStagedPriceMax] = useState(committedPriceFilter?.max ?? "");
|
|
221
|
-
|
|
215
|
+
const [prevPriceMin, setPrevPriceMin] = useState(committedPriceFilter?.min);
|
|
216
|
+
const [prevPriceMax, setPrevPriceMax] = useState(committedPriceFilter?.max);
|
|
217
|
+
if (prevPriceMin !== committedPriceFilter?.min || prevPriceMax !== committedPriceFilter?.max) {
|
|
218
|
+
setPrevPriceMin(committedPriceFilter?.min);
|
|
219
|
+
setPrevPriceMax(committedPriceFilter?.max);
|
|
222
220
|
setStagedPriceMin(committedPriceFilter?.min ?? "");
|
|
223
221
|
setStagedPriceMax(committedPriceFilter?.max ?? "");
|
|
224
|
-
}
|
|
222
|
+
}
|
|
225
223
|
|
|
226
224
|
const committedBedroomFilter = filters.active.find((f) => f.field === "Bedrooms__c");
|
|
227
225
|
const committedBedrooms = rangeToBedroomBucket(
|
|
@@ -229,15 +227,19 @@ export default function PropertySearch() {
|
|
|
229
227
|
committedBedroomFilter?.max,
|
|
230
228
|
);
|
|
231
229
|
const [stagedBedrooms, setStagedBedrooms] = useState<BedroomFilter>(committedBedrooms);
|
|
232
|
-
|
|
230
|
+
const [prevCommittedBedrooms, setPrevCommittedBedrooms] = useState(committedBedrooms);
|
|
231
|
+
if (prevCommittedBedrooms !== committedBedrooms) {
|
|
232
|
+
setPrevCommittedBedrooms(committedBedrooms);
|
|
233
233
|
setStagedBedrooms(committedBedrooms);
|
|
234
|
-
}
|
|
234
|
+
}
|
|
235
235
|
|
|
236
236
|
const committedSortBy = sortStateToSortBy(sort.current);
|
|
237
237
|
const [stagedSortBy, setStagedSortBy] = useState<SortBy>(committedSortBy ?? "price_asc");
|
|
238
|
-
|
|
238
|
+
const [prevCommittedSortBy, setPrevCommittedSortBy] = useState(committedSortBy);
|
|
239
|
+
if (prevCommittedSortBy !== committedSortBy) {
|
|
240
|
+
setPrevCommittedSortBy(committedSortBy);
|
|
239
241
|
setStagedSortBy(committedSortBy ?? "price_asc");
|
|
240
|
-
}
|
|
242
|
+
}
|
|
241
243
|
|
|
242
244
|
// Set default sort on mount if none set
|
|
243
245
|
useEffect(() => {
|