@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.
Files changed (81) hide show
  1. package/dist/CHANGELOG.md +9 -0
  2. package/dist/README.md +77 -27
  3. package/dist/force-app/main/default/applications/PropertyManagement.app-meta.xml +33 -0
  4. package/dist/force-app/main/default/data/Application__c.json +10 -10
  5. package/dist/force-app/main/default/data/data-plan.json +0 -6
  6. package/dist/force-app/main/default/objects/Application__c/fields/User__c.field-meta.xml +2 -2
  7. package/dist/force-app/main/default/objects/Property__c/Property__c.object-meta.xml +1 -1
  8. package/dist/force-app/main/default/permissionsets/Property_Management_Access.permissionset-meta.xml +76 -0
  9. package/dist/force-app/main/default/permissionsets/Property_Rental_Guest_User_Access.permissionset-meta.xml +217 -0
  10. package/dist/force-app/main/default/permissionsets/Tenant_Maintenance_Access.permissionset-meta.xml +1 -1
  11. package/dist/force-app/main/default/profiles/Property Rental Prospect Profile.profile-meta.xml +295 -0
  12. package/dist/force-app/main/default/roles/Admin.role-meta.xml +9 -0
  13. package/dist/force-app/main/default/sharingrules/Property__c.sharingRules-meta.xml +17 -0
  14. package/dist/force-app/main/default/tabs/Agent__c.tab-meta.xml +5 -0
  15. package/dist/force-app/main/default/tabs/Application__c.tab-meta.xml +5 -0
  16. package/dist/force-app/main/default/tabs/KPI_Snapshot__c.tab-meta.xml +5 -0
  17. package/dist/force-app/main/default/tabs/Lease__c.tab-meta.xml +5 -0
  18. package/dist/force-app/main/default/tabs/Maintenance_Request__c.tab-meta.xml +5 -0
  19. package/dist/force-app/main/default/tabs/Maintenance_Worker__c.tab-meta.xml +5 -0
  20. package/dist/force-app/main/default/tabs/Notification__c.tab-meta.xml +5 -0
  21. package/dist/force-app/main/default/tabs/Payment__c.tab-meta.xml +5 -0
  22. package/dist/force-app/main/default/tabs/Property_Cost__c.tab-meta.xml +5 -0
  23. package/dist/force-app/main/default/tabs/Property_Feature__c.tab-meta.xml +5 -0
  24. package/dist/force-app/main/default/tabs/Property_Image__c.tab-meta.xml +5 -0
  25. package/dist/force-app/main/default/tabs/Property_Listing__c.tab-meta.xml +5 -0
  26. package/dist/force-app/main/default/tabs/Property_Management_Company__c.tab-meta.xml +5 -0
  27. package/dist/force-app/main/default/tabs/Property_Owner__c.tab-meta.xml +5 -0
  28. package/dist/force-app/main/default/tabs/Property_Sale__c.tab-meta.xml +5 -0
  29. package/dist/force-app/main/default/tabs/Property__c.tab-meta.xml +5 -0
  30. package/dist/force-app/main/default/tabs/Tenant__c.tab-meta.xml +5 -0
  31. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/api/applications/applicationApi.ts +5 -6
  32. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/api/graphql-operations-types.ts +14751 -2937
  33. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/api/maintenanceRequests/maintenanceRequestApi.ts +4 -0
  34. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/api/query/tenantProperties.graphql +24 -0
  35. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/api/tenants/tenantApi.ts +23 -0
  36. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/components/alerts/status-alert.tsx +11 -8
  37. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/components/ui/input.tsx +1 -1
  38. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/context/TenantAccessContext.tsx +24 -12
  39. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/authentication/api/userProfileApi.ts +2 -1
  40. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/authentication/authenticationConfig.ts +9 -9
  41. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/authentication/context/AuthContext.tsx +21 -4
  42. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/authentication/forms/auth-form.tsx +15 -1
  43. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/authentication/hooks/form.tsx +1 -1
  44. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/authentication/layouts/privateRouteLayout.tsx +2 -11
  45. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/authentication/pages/ChangePassword.tsx +20 -4
  46. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/authentication/pages/ForgotPassword.tsx +19 -4
  47. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/authentication/pages/Login.tsx +19 -4
  48. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/authentication/pages/Profile.tsx +80 -43
  49. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/authentication/pages/Register.tsx +15 -4
  50. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/authentication/pages/ResetPassword.tsx +19 -4
  51. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/authentication/utils/helpers.ts +15 -52
  52. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/object-search/__examples__/pages/AccountObjectDetailPage.tsx +2 -4
  53. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/object-search/__examples__/pages/AccountSearch.tsx +7 -15
  54. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/object-search/components/filters/NumericRangeFilter.tsx +9 -5
  55. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/object-search/components/filters/SearchFilter.tsx +5 -3
  56. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/object-search/components/filters/TextFilter.tsx +5 -3
  57. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/object-search/hooks/useAsyncData.ts +11 -4
  58. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/hooks/useAsyncData.ts +67 -0
  59. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/hooks/useGeocode.ts +5 -9
  60. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/hooks/useMaintenanceRequests.ts +3 -11
  61. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/hooks/usePropertyDetail.ts +6 -17
  62. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/hooks/usePropertyMapMarkers.ts +43 -34
  63. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/hooks/useWeather.ts +2 -3
  64. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/pages/Application.tsx +45 -44
  65. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/pages/Contact.tsx +5 -9
  66. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/pages/Home.tsx +2 -3
  67. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/pages/Maintenance.tsx +43 -15
  68. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/pages/PropertySearch.tsx +21 -19
  69. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/routes.tsx +19 -25
  70. package/dist/package-lock.json +2 -2
  71. package/dist/package.json +1 -1
  72. package/dist/scripts/org-setup.config.json +18 -3
  73. package/dist/scripts/org-setup.mjs +528 -44
  74. package/package.json +1 -1
  75. package/dist/force-app/main/default/data/Contact.json +0 -44
  76. package/dist/force-app/main/default/scripts/org-setup.config.json +0 -9
  77. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/api/query/tenantAccess.graphql +0 -13
  78. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/api/tenantApi.ts +0 -12
  79. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/authentication/layout/card-skeleton.tsx +0 -38
  80. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/authentication/layouts/authenticationRouteLayout.tsx +0 -21
  81. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/object-search/hooks/useCachedAsyncData.ts +0 -188
@@ -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 } = useCachedAsyncData(fetchMaintenanceNodes, [generation], {
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
- }, [generation]);
32
+ }, []);
41
33
 
42
34
  return { requests: data ?? [], loading, error, refetch };
43
35
  }
@@ -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 } = useCachedAsyncData(
44
- () => {
45
- if (!trimmedId) return Promise.resolve(null);
46
- return fetchDetail(trimmedId);
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
- }, [cacheKey]);
44
+ }, []);
56
45
 
57
46
  return {
58
47
  property: data ?? null,
@@ -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
- const resultsRef = useRef(results);
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
- const uniqIds = idsKey === "" ? [] : idsKey.split(",");
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
- setMarkers(spreadDuplicateMarkers(directMarkers));
134
- setLoading(false);
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 { useCachedAsyncData } from "@/features/object-search/hooks/useCachedAsyncData";
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 = useCachedAsyncData(
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 { fetchUserContact } from "../features/authentication/api/userProfileApi";
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
- } = useCachedAsyncData(
64
- () => {
65
- if (!propertyId?.trim()) return Promise.resolve(null);
66
- return fetchPropertyDetailById(propertyId);
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 [submittedId, setSubmittedId] = useState<string | null>(null);
73
+ const [submittedName, setSubmittedName] = useState<string | null>(null);
89
74
 
90
- const handleSubmit = async (e: SubmitEvent<HTMLFormElement>) => {
91
- e.preventDefault();
92
- setSubmitError(null);
93
- setSubmitting(true);
94
- try {
95
- const id = await createApplicationRecord({
96
- Property__c: propertyId || null,
97
- Status__c: "Submitted",
98
- User__c: contactId || user?.id || "",
99
- Start_Date__c: moveInDate.trim() || null,
100
- Employment__c: employment.trim() || null,
101
- References__c: references.trim() || null,
102
- });
103
- setSubmittedId(id.id);
104
- } catch (err) {
105
- setSubmitError(err instanceof Error ? err.message : "Failed to submit application.");
106
- } finally {
107
- setSubmitting(false);
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 (submittedId) {
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 has been saved. Reference: {submittedId}
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 { useCachedAsyncData } from "@/features/object-search/hooks/useCachedAsyncData";
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 } = useCachedAsyncData(
171
- () => {
172
- if (!isAuthenticated || !user?.id) return Promise.resolve(null);
173
- return fetchUserProfile<UserInfo>(user.id);
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 { useCachedAsyncData } from "@/features/object-search/hooks/useCachedAsyncData";
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 } = useCachedAsyncData<PropertySearchResult>(
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
- } = useCachedAsyncData(
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, searchKey],
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>
@@ -18,7 +18,7 @@ import {
18
18
  useObjectSearchParams,
19
19
  type PaginationConfig,
20
20
  } from "@/features/object-search/hooks/useObjectSearchParams";
21
- import { useCachedAsyncData } from "@/features/object-search/hooks/useCachedAsyncData";
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
- } = useCachedAsyncData<PropertySearchResult>(
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
- useEffect(() => {
206
+ const [prevCommittedSearchValue, setPrevCommittedSearchValue] = useState(committedSearchValue);
207
+ if (prevCommittedSearchValue !== committedSearchValue) {
208
+ setPrevCommittedSearchValue(committedSearchValue);
215
209
  setSearchQuery(committedSearchValue);
216
- }, [committedSearchValue]);
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
- useEffect(() => {
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
- }, [committedPriceFilter?.min, committedPriceFilter?.max]);
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
- useEffect(() => {
230
+ const [prevCommittedBedrooms, setPrevCommittedBedrooms] = useState(committedBedrooms);
231
+ if (prevCommittedBedrooms !== committedBedrooms) {
232
+ setPrevCommittedBedrooms(committedBedrooms);
233
233
  setStagedBedrooms(committedBedrooms);
234
- }, [committedBedrooms]);
234
+ }
235
235
 
236
236
  const committedSortBy = sortStateToSortBy(sort.current);
237
237
  const [stagedSortBy, setStagedSortBy] = useState<SortBy>(committedSortBy ?? "price_asc");
238
- useEffect(() => {
238
+ const [prevCommittedSortBy, setPrevCommittedSortBy] = useState(committedSortBy);
239
+ if (prevCommittedSortBy !== committedSortBy) {
240
+ setPrevCommittedSortBy(committedSortBy);
239
241
  setStagedSortBy(committedSortBy ?? "price_asc");
240
- }, [committedSortBy]);
242
+ }
241
243
 
242
244
  // Set default sort on mount if none set
243
245
  useEffect(() => {