@salesforce/webapp-template-app-react-sample-b2x-experimental 1.116.8 → 1.116.9
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 +8 -0
- package/dist/force-app/main/default/webapplications/propertyrentalapp/package.json +3 -3
- package/dist/force-app/main/default/webapplications/propertyrentalapp/src/api/graphql-operations-types.ts +24594 -7234
- package/dist/force-app/main/default/webapplications/propertyrentalapp/src/api/maintenanceRequests/maintenanceRequestApi.ts +21 -157
- package/dist/force-app/main/default/webapplications/propertyrentalapp/src/api/maintenanceRequests/query/maintenanceRequests.graphql +60 -0
- package/dist/force-app/main/default/webapplications/propertyrentalapp/src/api/properties/propertyDetailGraphQL.ts +45 -444
- package/dist/force-app/main/default/webapplications/propertyrentalapp/src/api/properties/propertyNodeUtils.ts +29 -0
- package/dist/force-app/main/default/webapplications/propertyrentalapp/src/api/properties/propertySearchService.ts +56 -0
- package/dist/force-app/main/default/webapplications/propertyrentalapp/src/api/properties/query/distinctPropertyStatus.graphql +19 -0
- package/dist/force-app/main/default/webapplications/propertyrentalapp/src/api/properties/query/distinctPropertyType.graphql +19 -0
- package/dist/force-app/main/default/webapplications/propertyrentalapp/src/api/properties/query/listingById.graphql +29 -0
- package/dist/force-app/main/default/webapplications/propertyrentalapp/src/api/properties/query/propertyAddressesByIds.graphql +17 -0
- package/dist/force-app/main/default/webapplications/propertyrentalapp/src/api/properties/query/propertyDetailById.graphql +124 -0
- package/dist/force-app/main/default/webapplications/propertyrentalapp/src/api/properties/query/searchProperties.graphql +85 -0
- package/dist/force-app/main/default/webapplications/propertyrentalapp/src/appLayout.tsx +1 -1
- package/dist/force-app/main/default/webapplications/propertyrentalapp/src/components/SkeletonPrimitives.tsx +9 -6
- package/dist/force-app/main/default/webapplications/propertyrentalapp/src/components/dashboard/WeatherWidget.tsx +35 -19
- package/dist/force-app/main/default/webapplications/propertyrentalapp/src/components/maintenanceRequests/MaintenanceRequestList.tsx +7 -5
- package/dist/force-app/main/default/webapplications/propertyrentalapp/src/components/maintenanceRequests/MaintenanceRequestListItem.tsx +11 -10
- package/dist/force-app/main/default/webapplications/propertyrentalapp/src/components/maintenanceRequests/MaintenanceSummaryDetailsModal.tsx +20 -15
- package/dist/force-app/main/default/webapplications/propertyrentalapp/src/components/properties/PropertyListingCard.tsx +11 -24
- package/dist/force-app/main/default/webapplications/propertyrentalapp/src/hooks/useGeocode.ts +23 -39
- package/dist/force-app/main/default/webapplications/propertyrentalapp/src/hooks/useMaintenanceRequests.ts +29 -25
- package/dist/force-app/main/default/webapplications/propertyrentalapp/src/hooks/usePropertyDetail.ts +42 -78
- package/dist/force-app/main/default/webapplications/propertyrentalapp/src/hooks/usePropertyMapMarkers.ts +20 -36
- package/dist/force-app/main/default/webapplications/propertyrentalapp/src/hooks/useWeather.ts +10 -23
- package/dist/force-app/main/default/webapplications/propertyrentalapp/src/pages/Application.tsx +40 -73
- package/dist/force-app/main/default/webapplications/propertyrentalapp/src/pages/Contact.tsx +44 -55
- package/dist/force-app/main/default/webapplications/propertyrentalapp/src/pages/Dashboard.tsx +1 -0
- package/dist/force-app/main/default/webapplications/propertyrentalapp/src/pages/Home.tsx +63 -32
- package/dist/force-app/main/default/webapplications/propertyrentalapp/src/pages/Maintenance.tsx +95 -6
- package/dist/force-app/main/default/webapplications/propertyrentalapp/src/pages/PropertyDetails.tsx +67 -45
- package/dist/force-app/main/default/webapplications/propertyrentalapp/src/pages/PropertySearch.tsx +299 -191
- package/dist/package-lock.json +2 -2
- package/dist/package.json +1 -1
- package/package.json +1 -1
- package/dist/force-app/main/default/webapplications/propertyrentalapp/src/api/properties/propertyListingGraphQL.ts +0 -380
- package/dist/force-app/main/default/webapplications/propertyrentalapp/src/components/properties/PropertyListingSearchPagination.tsx +0 -136
- package/dist/force-app/main/default/webapplications/propertyrentalapp/src/constants/propertyListing.ts +0 -4
- package/dist/force-app/main/default/webapplications/propertyrentalapp/src/hooks/usePropertyAddresses.ts +0 -45
- package/dist/force-app/main/default/webapplications/propertyrentalapp/src/hooks/usePropertyListingAmenities.ts +0 -58
- package/dist/force-app/main/default/webapplications/propertyrentalapp/src/hooks/usePropertyListingSearch.ts +0 -84
- package/dist/force-app/main/default/webapplications/propertyrentalapp/src/hooks/usePropertyPrimaryImages.ts +0 -54
- /package/dist/force-app/main/default/webapplications/propertyrentalapp/src/api/{leadApi.ts → leads/leadApi.ts} +0 -0
|
@@ -1,38 +1,16 @@
|
|
|
1
|
-
import { useState, useCallback,
|
|
1
|
+
import { useState, useCallback, type SubmitEvent } from "react";
|
|
2
2
|
import { Link } from "react-router";
|
|
3
3
|
import { Button } from "@/components/ui/button";
|
|
4
4
|
import { Input } from "@/components/ui/input";
|
|
5
5
|
import { Label } from "@/components/ui/label";
|
|
6
6
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
7
7
|
import { CenteredPageLayout } from "@/features/authentication/layout/centered-page-layout";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import { createContactUsLead } from "@/api/leadApi";
|
|
8
|
+
import { Loader2 } from "lucide-react";
|
|
9
|
+
import { createContactUsLead } from "@/api/leads/leadApi";
|
|
11
10
|
import { useAuth } from "@/features/authentication/context/AuthContext";
|
|
12
11
|
import { fetchUserProfile } from "@/features/authentication/api/userProfileApi";
|
|
13
|
-
import
|
|
14
|
-
|
|
15
|
-
function LoadingCard() {
|
|
16
|
-
return (
|
|
17
|
-
<Card>
|
|
18
|
-
<CardHeader>
|
|
19
|
-
<Skeleton className="h-5 w-40" />
|
|
20
|
-
</CardHeader>
|
|
21
|
-
<CardContent className="space-y-4" role="status">
|
|
22
|
-
<div className="grid gap-4 sm:grid-cols-2">
|
|
23
|
-
<SkeletonField labelWidth="w-24" />
|
|
24
|
-
<SkeletonField labelWidth="w-20" />
|
|
25
|
-
</div>
|
|
26
|
-
<SkeletonField labelWidth="w-12" />
|
|
27
|
-
<SkeletonField labelWidth="w-14" />
|
|
28
|
-
<SkeletonField labelWidth="w-16" />
|
|
29
|
-
<SkeletonField labelWidth="w-20" height="h-[120px]" />
|
|
30
|
-
<Skeleton className="h-9 w-32" />
|
|
31
|
-
<span className="sr-only">Loading contact form…</span>
|
|
32
|
-
</CardContent>
|
|
33
|
-
</Card>
|
|
34
|
-
);
|
|
35
|
-
}
|
|
12
|
+
import { useCachedAsyncData } from "@/features/object-search/hooks/useCachedAsyncData";
|
|
13
|
+
import type { UserInfo } from "@/api/leads/leadApi";
|
|
36
14
|
|
|
37
15
|
function SuccessCard() {
|
|
38
16
|
return (
|
|
@@ -61,6 +39,7 @@ interface ContactFormProps {
|
|
|
61
39
|
onSubmit: (e: SubmitEvent<HTMLFormElement>) => void;
|
|
62
40
|
submitting: boolean;
|
|
63
41
|
submitError: string | null;
|
|
42
|
+
loading?: boolean;
|
|
64
43
|
}
|
|
65
44
|
|
|
66
45
|
function ContactForm({
|
|
@@ -69,6 +48,7 @@ function ContactForm({
|
|
|
69
48
|
onSubmit,
|
|
70
49
|
submitting,
|
|
71
50
|
submitError,
|
|
51
|
+
loading,
|
|
72
52
|
}: ContactFormProps) {
|
|
73
53
|
const [firstName, setFirstName] = useState("");
|
|
74
54
|
const [lastName, setLastName] = useState("");
|
|
@@ -78,12 +58,23 @@ function ContactForm({
|
|
|
78
58
|
const [message, setMessage] = useState("");
|
|
79
59
|
|
|
80
60
|
return (
|
|
81
|
-
<Card>
|
|
61
|
+
<Card className="relative">
|
|
62
|
+
{loading && (
|
|
63
|
+
<div className="absolute inset-0 z-10 flex items-center justify-center rounded-[inherit] bg-muted/60">
|
|
64
|
+
<Loader2 className="size-8 animate-spin text-muted-foreground" />
|
|
65
|
+
<span className="sr-only">Loading contact form…</span>
|
|
66
|
+
</div>
|
|
67
|
+
)}
|
|
82
68
|
<CardHeader>
|
|
83
69
|
<CardTitle className="text-lg">Send a message</CardTitle>
|
|
84
70
|
</CardHeader>
|
|
85
71
|
<CardContent>
|
|
86
|
-
<form
|
|
72
|
+
<form
|
|
73
|
+
onSubmit={onSubmit}
|
|
74
|
+
className="space-y-4"
|
|
75
|
+
aria-disabled={loading}
|
|
76
|
+
inert={loading ? true : undefined}
|
|
77
|
+
>
|
|
87
78
|
<div className="grid gap-4 sm:grid-cols-2">
|
|
88
79
|
<div className="space-y-2">
|
|
89
80
|
<Label htmlFor="contact-first">First name {!isAuthenticated && "*"}</Label>
|
|
@@ -168,25 +159,18 @@ function ContactForm({
|
|
|
168
159
|
|
|
169
160
|
export default function Contact() {
|
|
170
161
|
const { isAuthenticated, loading, user } = useAuth();
|
|
171
|
-
const [userInfo, setUserInfo] = useState<UserInfo | null>(null);
|
|
172
162
|
const [submitting, setSubmitting] = useState(false);
|
|
173
163
|
const [submitError, setSubmitError] = useState<string | null>(null);
|
|
174
164
|
const [submitted, setSubmitted] = useState(false);
|
|
175
165
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
if (mounted) console.error("Failed to load user contact info", err);
|
|
185
|
-
});
|
|
186
|
-
return () => {
|
|
187
|
-
mounted = false;
|
|
188
|
-
};
|
|
189
|
-
}, [isAuthenticated, user]);
|
|
166
|
+
const { data: userInfo } = useCachedAsyncData(
|
|
167
|
+
() => {
|
|
168
|
+
if (!isAuthenticated || !user?.id) return Promise.resolve(null);
|
|
169
|
+
return fetchUserProfile<UserInfo>(user.id);
|
|
170
|
+
},
|
|
171
|
+
[isAuthenticated, user?.id],
|
|
172
|
+
{ key: `contact-user-profile:${user?.id ?? ""}`, ttl: 300_000 },
|
|
173
|
+
);
|
|
190
174
|
|
|
191
175
|
const handleSubmit = useCallback(
|
|
192
176
|
async (e: SubmitEvent<HTMLFormElement>) => {
|
|
@@ -228,17 +212,15 @@ export default function Contact() {
|
|
|
228
212
|
|
|
229
213
|
const isLoading = loading || (isAuthenticated && !userInfo);
|
|
230
214
|
|
|
231
|
-
|
|
232
|
-
if (isLoading) return <LoadingCard />;
|
|
233
|
-
if (submitted) return <SuccessCard />;
|
|
215
|
+
if (submitted) {
|
|
234
216
|
return (
|
|
235
|
-
<
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
217
|
+
<CenteredPageLayout contentMaxWidth="md">
|
|
218
|
+
<h1 className="mb-2 text-2xl font-semibold text-foreground">Contact Us</h1>
|
|
219
|
+
<p className="mb-6 text-muted-foreground">
|
|
220
|
+
Have a question or feedback? Send us a message and we'll respond as soon as we can.
|
|
221
|
+
</p>
|
|
222
|
+
<SuccessCard />
|
|
223
|
+
</CenteredPageLayout>
|
|
242
224
|
);
|
|
243
225
|
}
|
|
244
226
|
|
|
@@ -248,7 +230,14 @@ export default function Contact() {
|
|
|
248
230
|
<p className="mb-6 text-muted-foreground">
|
|
249
231
|
Have a question or feedback? Send us a message and we'll respond as soon as we can.
|
|
250
232
|
</p>
|
|
251
|
-
|
|
233
|
+
<ContactForm
|
|
234
|
+
isAuthenticated={isAuthenticated}
|
|
235
|
+
userInfo={userInfo}
|
|
236
|
+
onSubmit={handleSubmit}
|
|
237
|
+
submitting={submitting}
|
|
238
|
+
submitError={submitError}
|
|
239
|
+
loading={isLoading}
|
|
240
|
+
/>
|
|
252
241
|
</CenteredPageLayout>
|
|
253
242
|
);
|
|
254
243
|
}
|
|
@@ -1,20 +1,23 @@
|
|
|
1
1
|
import { Link, useNavigate } from "react-router";
|
|
2
|
-
import { useRef, useState, type ChangeEvent } from "react";
|
|
2
|
+
import { useRef, useState, useMemo, type ChangeEvent } from "react";
|
|
3
3
|
import { Button } from "@/components/ui/button";
|
|
4
4
|
import { Input } from "@/components/ui/input";
|
|
5
5
|
import { Card, CardContent } from "@/components/ui/card";
|
|
6
|
-
import { usePropertyListingSearch } from "@/hooks/usePropertyListingSearch";
|
|
7
6
|
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
import {
|
|
7
|
+
searchProperties,
|
|
8
|
+
type PropertySearchNode,
|
|
9
|
+
type PropertySearchResult,
|
|
10
|
+
} from "@/api/properties/propertySearchService";
|
|
11
|
+
import {
|
|
12
|
+
extractPrimaryImageUrl,
|
|
13
|
+
extractAmenities,
|
|
14
|
+
extractAddress,
|
|
15
|
+
} from "@/api/properties/propertyNodeUtils";
|
|
16
|
+
import { useCachedAsyncData } from "@/features/object-search/hooks/useCachedAsyncData";
|
|
13
17
|
import PropertyListingCard, {
|
|
14
18
|
PropertyListingCardSkeleton,
|
|
15
19
|
} from "@/components/properties/PropertyListingCard";
|
|
16
|
-
import
|
|
17
|
-
import { createNewsletterLead } from "@/api/leadApi";
|
|
20
|
+
import { createNewsletterLead } from "@/api/leads/leadApi";
|
|
18
21
|
import {
|
|
19
22
|
Phone,
|
|
20
23
|
Send,
|
|
@@ -62,29 +65,24 @@ function FeaturedPropertiesGrid({
|
|
|
62
65
|
propertyAddressMap,
|
|
63
66
|
amenitiesMap,
|
|
64
67
|
}: {
|
|
65
|
-
results:
|
|
66
|
-
primaryImagesMap: Record<string, string
|
|
67
|
-
propertyAddressMap: Record<string, string
|
|
68
|
-
amenitiesMap: Record<string, string
|
|
68
|
+
results: PropertySearchNode[];
|
|
69
|
+
primaryImagesMap: Record<string, string>;
|
|
70
|
+
propertyAddressMap: Record<string, string>;
|
|
71
|
+
amenitiesMap: Record<string, string>;
|
|
69
72
|
}) {
|
|
70
73
|
return (
|
|
71
74
|
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
|
72
|
-
{results.map((
|
|
73
|
-
const
|
|
74
|
-
const
|
|
75
|
-
const
|
|
76
|
-
const address = propertyId ? (propertyAddressMap[propertyId] ?? null) : null;
|
|
77
|
-
const amenities = propertyId ? (amenitiesMap[propertyId] ?? null) : null;
|
|
75
|
+
{results.map((node, index) => {
|
|
76
|
+
const imageUrl = primaryImagesMap[node.Id] ?? null;
|
|
77
|
+
const address = propertyAddressMap[node.Id] ?? null;
|
|
78
|
+
const amenities = amenitiesMap[node.Id] ?? null;
|
|
78
79
|
return (
|
|
79
|
-
<div key={
|
|
80
|
+
<div key={node.Id ?? index} className="min-h-0">
|
|
80
81
|
<PropertyListingCard
|
|
81
|
-
|
|
82
|
+
node={node}
|
|
82
83
|
imageUrl={imageUrl}
|
|
83
84
|
address={address}
|
|
84
85
|
amenities={amenities ?? undefined}
|
|
85
|
-
loading={
|
|
86
|
-
primaryImagesMap.loading || propertyAddressMap.loading || amenitiesMap.loading
|
|
87
|
-
}
|
|
88
86
|
/>
|
|
89
87
|
</div>
|
|
90
88
|
);
|
|
@@ -104,14 +102,47 @@ export default function Home() {
|
|
|
104
102
|
text: string;
|
|
105
103
|
} | null>(null);
|
|
106
104
|
|
|
107
|
-
const {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
"
|
|
105
|
+
const { data: searchResult, loading: featuredLoading } = useCachedAsyncData<PropertySearchResult>(
|
|
106
|
+
() => searchProperties({ first: FEATURED_PAGE_SIZE }),
|
|
107
|
+
[],
|
|
108
|
+
{ key: "featuredProperties" },
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
const featuredResults = useMemo(
|
|
112
|
+
() =>
|
|
113
|
+
(searchResult?.edges ?? []).reduce<PropertySearchNode[]>((acc, edge) => {
|
|
114
|
+
if (edge?.node) acc.push(edge.node);
|
|
115
|
+
return acc;
|
|
116
|
+
}, []),
|
|
117
|
+
[searchResult?.edges],
|
|
111
118
|
);
|
|
112
|
-
|
|
113
|
-
const
|
|
114
|
-
|
|
119
|
+
|
|
120
|
+
const primaryImagesMap = useMemo(() => {
|
|
121
|
+
const map: Record<string, string> = {};
|
|
122
|
+
for (const node of featuredResults) {
|
|
123
|
+
const url = extractPrimaryImageUrl(node);
|
|
124
|
+
if (url) map[node.Id] = url;
|
|
125
|
+
}
|
|
126
|
+
return map;
|
|
127
|
+
}, [featuredResults]);
|
|
128
|
+
|
|
129
|
+
const propertyAddressMap = useMemo(() => {
|
|
130
|
+
const map: Record<string, string> = {};
|
|
131
|
+
for (const node of featuredResults) {
|
|
132
|
+
const addr = extractAddress(node);
|
|
133
|
+
if (addr) map[node.Id] = String(addr);
|
|
134
|
+
}
|
|
135
|
+
return map;
|
|
136
|
+
}, [featuredResults]);
|
|
137
|
+
|
|
138
|
+
const amenitiesMap = useMemo(() => {
|
|
139
|
+
const map: Record<string, string> = {};
|
|
140
|
+
for (const node of featuredResults) {
|
|
141
|
+
const amenities = extractAmenities(node);
|
|
142
|
+
if (amenities) map[node.Id] = amenities;
|
|
143
|
+
}
|
|
144
|
+
return map;
|
|
145
|
+
}, [featuredResults]);
|
|
115
146
|
|
|
116
147
|
const handleFindHome = (e: React.FormEvent) => {
|
|
117
148
|
e.preventDefault();
|
|
@@ -136,7 +167,7 @@ export default function Home() {
|
|
|
136
167
|
}
|
|
137
168
|
};
|
|
138
169
|
|
|
139
|
-
const validFeatured = featuredResults.filter((r) => r?.
|
|
170
|
+
const validFeatured = featuredResults.filter((r) => r?.Id);
|
|
140
171
|
|
|
141
172
|
return (
|
|
142
173
|
<div className="space-y-0">
|
package/dist/force-app/main/default/webapplications/propertyrentalapp/src/pages/Maintenance.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState, useCallback, type ChangeEvent } from "react";
|
|
1
|
+
import { useState, useCallback, useMemo, type ChangeEvent } from "react";
|
|
2
2
|
import { Button } from "@/components/ui/button";
|
|
3
3
|
import { Input } from "@/components/ui/input";
|
|
4
4
|
import { Label } from "@/components/ui/label";
|
|
@@ -7,9 +7,28 @@ import { Skeleton } from "@/components/ui/skeleton";
|
|
|
7
7
|
import { Calendar, ArrowRight } from "lucide-react";
|
|
8
8
|
import MaintenanceRequestList from "@/components/maintenanceRequests/MaintenanceRequestList";
|
|
9
9
|
import { SkeletonListRows, SkeletonField } from "@/components/SkeletonPrimitives";
|
|
10
|
-
import {
|
|
11
|
-
|
|
10
|
+
import {
|
|
11
|
+
searchMaintenanceRequests,
|
|
12
|
+
createMaintenanceRequest,
|
|
13
|
+
type MaintenanceRequestNode,
|
|
14
|
+
} from "@/api/maintenanceRequests/maintenanceRequestApi";
|
|
12
15
|
import { useAuth } from "@/features/authentication/context/AuthContext";
|
|
16
|
+
import {
|
|
17
|
+
useObjectSearchParams,
|
|
18
|
+
type PaginationConfig,
|
|
19
|
+
} from "@/features/object-search/hooks/useObjectSearchParams";
|
|
20
|
+
import {
|
|
21
|
+
useCachedAsyncData,
|
|
22
|
+
clearCacheEntry,
|
|
23
|
+
} from "@/features/object-search/hooks/useCachedAsyncData";
|
|
24
|
+
import PaginationControls from "@/features/object-search/components/PaginationControls";
|
|
25
|
+
import type {
|
|
26
|
+
Maintenance_Request__C_Filter,
|
|
27
|
+
Maintenance_Request__C_OrderBy,
|
|
28
|
+
} from "@/api/graphql-operations-types";
|
|
29
|
+
import { ResultOrder } from "@/api/graphql-operations-types";
|
|
30
|
+
import type { FilterFieldConfig } from "@/features/object-search/utils/filterUtils";
|
|
31
|
+
import type { SortFieldConfig } from "@/features/object-search/utils/sortUtils";
|
|
13
32
|
|
|
14
33
|
const TYPE_OPTIONS = [
|
|
15
34
|
"Plumbing",
|
|
@@ -29,6 +48,15 @@ const PRIORITY_OPTIONS = [
|
|
|
29
48
|
{ value: "Emergency", label: "Emergency (2hr)" },
|
|
30
49
|
] as const;
|
|
31
50
|
|
|
51
|
+
const FILTER_CONFIGS: FilterFieldConfig[] = [];
|
|
52
|
+
|
|
53
|
+
const SORT_CONFIGS: SortFieldConfig[] = [{ field: "Scheduled__c", label: "Scheduled" }];
|
|
54
|
+
|
|
55
|
+
const PAGINATION_CONFIG: PaginationConfig = {
|
|
56
|
+
defaultPageSize: 10,
|
|
57
|
+
validPageSizes: [10, 20, 50],
|
|
58
|
+
};
|
|
59
|
+
|
|
32
60
|
function MaintenanceSkeleton() {
|
|
33
61
|
return (
|
|
34
62
|
<div className="mx-auto max-w-[900px]" role="status">
|
|
@@ -62,7 +90,50 @@ function MaintenanceSkeleton() {
|
|
|
62
90
|
|
|
63
91
|
export default function Maintenance() {
|
|
64
92
|
const { loading: authLoading } = useAuth();
|
|
65
|
-
|
|
93
|
+
|
|
94
|
+
const { query, pagination } = useObjectSearchParams<
|
|
95
|
+
Maintenance_Request__C_Filter,
|
|
96
|
+
Maintenance_Request__C_OrderBy
|
|
97
|
+
>(FILTER_CONFIGS, SORT_CONFIGS, PAGINATION_CONFIG);
|
|
98
|
+
|
|
99
|
+
const [refreshCounter, setRefreshCounter] = useState(0);
|
|
100
|
+
|
|
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
|
+
const {
|
|
109
|
+
data: searchResult,
|
|
110
|
+
loading,
|
|
111
|
+
error,
|
|
112
|
+
} = useCachedAsyncData(
|
|
113
|
+
() =>
|
|
114
|
+
searchMaintenanceRequests({
|
|
115
|
+
where: query.where,
|
|
116
|
+
orderBy: { Scheduled__c: { order: ResultOrder.Desc } },
|
|
117
|
+
first: pagination.pageSize,
|
|
118
|
+
after: pagination.afterCursor,
|
|
119
|
+
}),
|
|
120
|
+
[query.where, query.orderBy, pagination.pageSize, pagination.afterCursor, refreshCounter],
|
|
121
|
+
{ key: searchKey },
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
const requests = useMemo(
|
|
125
|
+
() =>
|
|
126
|
+
(searchResult?.edges ?? []).reduce<MaintenanceRequestNode[]>((acc, edge) => {
|
|
127
|
+
if (edge?.node) acc.push(edge.node);
|
|
128
|
+
return acc;
|
|
129
|
+
}, []),
|
|
130
|
+
[searchResult?.edges],
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
const pageInfo = searchResult?.pageInfo;
|
|
134
|
+
const hasNextPage = pageInfo?.hasNextPage ?? false;
|
|
135
|
+
const hasPreviousPage = pagination.pageIndex > 0;
|
|
136
|
+
|
|
66
137
|
const [title, setTitle] = useState("");
|
|
67
138
|
const [description, setDescription] = useState("");
|
|
68
139
|
const [type, setType] = useState<string>("");
|
|
@@ -101,14 +172,15 @@ export default function Maintenance() {
|
|
|
101
172
|
setType("");
|
|
102
173
|
setPriority("Standard");
|
|
103
174
|
setDateRequested(new Date().toISOString().slice(0, 10));
|
|
104
|
-
|
|
175
|
+
clearCacheEntry(searchKey);
|
|
176
|
+
setRefreshCounter((c) => c + 1);
|
|
105
177
|
} catch (err) {
|
|
106
178
|
setSubmitError(err instanceof Error ? err.message : "Failed to submit request");
|
|
107
179
|
} finally {
|
|
108
180
|
setSubmitting(false);
|
|
109
181
|
}
|
|
110
182
|
},
|
|
111
|
-
[title, description, type, priority, dateRequested,
|
|
183
|
+
[title, description, type, priority, dateRequested, searchKey],
|
|
112
184
|
);
|
|
113
185
|
|
|
114
186
|
if (authLoading) return <MaintenanceSkeleton />;
|
|
@@ -235,6 +307,23 @@ export default function Maintenance() {
|
|
|
235
307
|
emptyMessage="No maintenance requests yet. Submit one above."
|
|
236
308
|
/>
|
|
237
309
|
</CardContent>
|
|
310
|
+
{!loading && !error && requests.length > 0 && (
|
|
311
|
+
<div className="px-6 pb-6">
|
|
312
|
+
<PaginationControls
|
|
313
|
+
pageIndex={pagination.pageIndex}
|
|
314
|
+
hasNextPage={hasNextPage}
|
|
315
|
+
hasPreviousPage={hasPreviousPage}
|
|
316
|
+
pageSize={pagination.pageSize}
|
|
317
|
+
pageSizeOptions={PAGINATION_CONFIG.validPageSizes}
|
|
318
|
+
onNextPage={() => {
|
|
319
|
+
if (pageInfo?.endCursor) pagination.goToNextPage(pageInfo.endCursor);
|
|
320
|
+
}}
|
|
321
|
+
onPreviousPage={pagination.goToPreviousPage}
|
|
322
|
+
onPageSizeChange={pagination.setPageSize}
|
|
323
|
+
disabled={loading || !!error}
|
|
324
|
+
/>
|
|
325
|
+
</div>
|
|
326
|
+
)}
|
|
238
327
|
</Card>
|
|
239
328
|
</div>
|
|
240
329
|
);
|
package/dist/force-app/main/default/webapplications/propertyrentalapp/src/pages/PropertyDetails.tsx
CHANGED
|
@@ -107,20 +107,38 @@ function PropertyDetailsSkeleton() {
|
|
|
107
107
|
|
|
108
108
|
export default function PropertyDetails() {
|
|
109
109
|
const { id } = useParams<{ id: string }>();
|
|
110
|
-
const {
|
|
111
|
-
|
|
110
|
+
const { property, loading, error } = usePropertyDetail(id);
|
|
111
|
+
|
|
112
|
+
// Extract nested relationships from the single property node.
|
|
113
|
+
const images = (property?.Property_Images__r?.edges ?? []).flatMap((e) =>
|
|
114
|
+
e?.node ? [e.node] : [],
|
|
115
|
+
);
|
|
116
|
+
const features = (property?.Property_Features__r?.edges ?? []).flatMap((e) =>
|
|
117
|
+
e?.node ? [e.node] : [],
|
|
118
|
+
);
|
|
119
|
+
const costs = (property?.Property_Costs__r?.edges ?? []).flatMap((e) =>
|
|
120
|
+
e?.node ? [e.node] : [],
|
|
121
|
+
);
|
|
122
|
+
const listing = property?.Property_Listings__r?.edges?.[0]?.node ?? null;
|
|
123
|
+
|
|
124
|
+
const addressForGeocode = property?.Address__c?.value?.replace(/\n/g, ", ") ?? null;
|
|
125
|
+
const hasCoordinates =
|
|
126
|
+
property?.Coordinates__Latitude__s?.value != null &&
|
|
127
|
+
property?.Coordinates__Longitude__s?.value != null;
|
|
112
128
|
// Always call hook in the same order; disable geocoding when coordinates already exist.
|
|
113
|
-
const { coords: geocodedCoords } = useGeocode(
|
|
114
|
-
const addressCoords: GeocodeResult | null =
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
129
|
+
const { coords: geocodedCoords } = useGeocode(hasCoordinates ? null : addressForGeocode);
|
|
130
|
+
const addressCoords: GeocodeResult | null = hasCoordinates
|
|
131
|
+
? {
|
|
132
|
+
lat: Number(property!.Coordinates__Latitude__s!.value),
|
|
133
|
+
lng: Number(property!.Coordinates__Longitude__s!.value),
|
|
134
|
+
}
|
|
135
|
+
: geocodedCoords;
|
|
118
136
|
|
|
119
137
|
if (loading) {
|
|
120
138
|
return <PropertyDetailsSkeleton />;
|
|
121
139
|
}
|
|
122
140
|
|
|
123
|
-
if (error || (!
|
|
141
|
+
if (error || (!property && id)) {
|
|
124
142
|
return (
|
|
125
143
|
<div className="mx-auto max-w-[900px]">
|
|
126
144
|
<div className="mb-4">
|
|
@@ -137,8 +155,8 @@ export default function PropertyDetails() {
|
|
|
137
155
|
);
|
|
138
156
|
}
|
|
139
157
|
|
|
140
|
-
const primaryImage = images.find((i) => i.
|
|
141
|
-
const otherImages = images.filter((i) => i.
|
|
158
|
+
const primaryImage = images.find((i) => i.Image_Type__c?.value === "Primary") ?? images[0];
|
|
159
|
+
const otherImages = images.filter((i) => i.Id !== primaryImage?.Id);
|
|
142
160
|
|
|
143
161
|
return (
|
|
144
162
|
<div className="mx-auto max-w-[900px]">
|
|
@@ -151,10 +169,10 @@ export default function PropertyDetails() {
|
|
|
151
169
|
{/* Hero image + thumbnails */}
|
|
152
170
|
<div className="mb-4 grid grid-cols-1 gap-4 md:grid-cols-2">
|
|
153
171
|
<div className="relative aspect-[4/3] overflow-hidden rounded-xl bg-muted">
|
|
154
|
-
{primaryImage?.
|
|
172
|
+
{primaryImage?.Image_URL__c?.value ? (
|
|
155
173
|
<img
|
|
156
|
-
src={primaryImage.
|
|
157
|
-
alt={primaryImage.
|
|
174
|
+
src={primaryImage.Image_URL__c.value}
|
|
175
|
+
alt={primaryImage.Alt_Text__c?.value ?? primaryImage.Name?.value ?? "Property"}
|
|
158
176
|
className="h-full w-full object-cover"
|
|
159
177
|
/>
|
|
160
178
|
) : (
|
|
@@ -163,11 +181,11 @@ export default function PropertyDetails() {
|
|
|
163
181
|
</div>
|
|
164
182
|
<div className="flex flex-col gap-2">
|
|
165
183
|
{otherImages.slice(0, 5).map((img) => (
|
|
166
|
-
<div key={img.
|
|
167
|
-
{img.
|
|
184
|
+
<div key={img.Id} className="relative h-20 overflow-hidden rounded-lg bg-muted">
|
|
185
|
+
{img.Image_URL__c?.value ? (
|
|
168
186
|
<img
|
|
169
|
-
src={img.
|
|
170
|
-
alt={img.
|
|
187
|
+
src={img.Image_URL__c.value}
|
|
188
|
+
alt={img.Alt_Text__c?.value ?? img.Name?.value ?? "Property"}
|
|
171
189
|
className="h-full w-full object-cover"
|
|
172
190
|
/>
|
|
173
191
|
) : null}
|
|
@@ -186,7 +204,7 @@ export default function PropertyDetails() {
|
|
|
186
204
|
{
|
|
187
205
|
lat: addressCoords.lat,
|
|
188
206
|
lng: addressCoords.lng,
|
|
189
|
-
label: listing?.
|
|
207
|
+
label: listing?.Name?.value ?? property?.Name?.value ?? "Property",
|
|
190
208
|
},
|
|
191
209
|
]}
|
|
192
210
|
className="h-[280px] w-full rounded-xl"
|
|
@@ -194,56 +212,56 @@ export default function PropertyDetails() {
|
|
|
194
212
|
</div>
|
|
195
213
|
)}
|
|
196
214
|
|
|
197
|
-
{/* Name, address, price
|
|
215
|
+
{/* Name, address, price */}
|
|
198
216
|
<Card className="mb-4 rounded-2xl border border-border shadow-sm">
|
|
199
217
|
<CardContent className="pt-3">
|
|
200
218
|
<h1 className="mb-1.5 text-2xl font-semibold text-foreground">
|
|
201
|
-
{listing?.
|
|
219
|
+
{listing?.Name?.value ?? property?.Name?.value ?? "Untitled"}
|
|
202
220
|
</h1>
|
|
203
|
-
{property?.
|
|
221
|
+
{property?.Address__c?.value && (
|
|
204
222
|
<p className="mb-1.5 text-sm text-muted-foreground">
|
|
205
|
-
{property.
|
|
223
|
+
{property.Address__c.value.replace(/\n/g, ", ")}
|
|
206
224
|
</p>
|
|
207
225
|
)}
|
|
208
226
|
<p className="mb-4 text-2xl font-semibold text-primary">
|
|
209
|
-
{listing?.
|
|
210
|
-
? formatListingPrice(listing.
|
|
211
|
-
: property?.
|
|
212
|
-
? formatListingPrice(property.
|
|
227
|
+
{listing?.Listing_Price__c?.value != null
|
|
228
|
+
? formatListingPrice(listing.Listing_Price__c.value)
|
|
229
|
+
: property?.Monthly_Rent__c?.value != null
|
|
230
|
+
? formatListingPrice(property.Monthly_Rent__c.value) + " / Month"
|
|
213
231
|
: "—"}
|
|
214
232
|
</p>
|
|
215
|
-
{/* Stat cards
|
|
233
|
+
{/* Stat cards */}
|
|
216
234
|
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
|
|
217
235
|
<div className="flex flex-col items-center justify-center rounded-xl bg-primary px-4 py-3 text-center">
|
|
218
236
|
<span className="text-xl font-semibold text-primary-foreground">
|
|
219
|
-
{property?.
|
|
237
|
+
{property?.Bedrooms__c?.value ?? "—"}
|
|
220
238
|
</span>
|
|
221
239
|
<span className="text-xs text-primary-foreground/90">Bedrooms</span>
|
|
222
240
|
</div>
|
|
223
241
|
<div className="flex flex-col items-center justify-center rounded-xl bg-primary px-4 py-3 text-center">
|
|
224
242
|
<span className="text-xl font-semibold text-primary-foreground">
|
|
225
|
-
{property?.
|
|
243
|
+
{property?.Bathrooms__c?.value ?? "—"}
|
|
226
244
|
</span>
|
|
227
245
|
<span className="text-xs text-primary-foreground/90">Baths</span>
|
|
228
246
|
</div>
|
|
229
247
|
<div className="flex flex-col items-center justify-center rounded-xl bg-primary px-4 py-3 text-center">
|
|
230
248
|
<span className="text-xl font-semibold text-primary-foreground">
|
|
231
|
-
{property?.
|
|
249
|
+
{property?.Sq_Ft__c?.value ?? "—"}
|
|
232
250
|
</span>
|
|
233
251
|
<span className="text-xs text-primary-foreground/90">Square Feet</span>
|
|
234
252
|
</div>
|
|
235
253
|
<div className="flex flex-col items-center justify-center rounded-xl bg-primary px-4 py-3 text-center">
|
|
236
254
|
<span className="text-xl font-semibold text-primary-foreground">
|
|
237
|
-
{listing?.
|
|
255
|
+
{listing?.Listing_Status__c?.value ?? "Now"}
|
|
238
256
|
</span>
|
|
239
257
|
<span className="text-xs text-primary-foreground/90">Available</span>
|
|
240
258
|
</div>
|
|
241
259
|
</div>
|
|
242
|
-
{property?.
|
|
243
|
-
<p className="mt-3 text-sm text-muted-foreground">{property.
|
|
260
|
+
{property?.Type__c?.value && (
|
|
261
|
+
<p className="mt-3 text-sm text-muted-foreground">{property.Type__c.value}</p>
|
|
244
262
|
)}
|
|
245
|
-
{property?.
|
|
246
|
-
<p className="mt-4 text-sm text-foreground">{property.
|
|
263
|
+
{property?.Description__c?.value && (
|
|
264
|
+
<p className="mt-4 text-sm text-foreground">{property.Description__c.value}</p>
|
|
247
265
|
)}
|
|
248
266
|
</CardContent>
|
|
249
267
|
</Card>
|
|
@@ -258,17 +276,21 @@ export default function PropertyDetails() {
|
|
|
258
276
|
<ul className="space-y-2">
|
|
259
277
|
{costs.slice(0, 10).map((c) => (
|
|
260
278
|
<li
|
|
261
|
-
key={c.
|
|
279
|
+
key={c.Id}
|
|
262
280
|
className="flex flex-wrap items-baseline justify-between gap-2 border-b border-border/50 pb-2 last:border-0"
|
|
263
281
|
>
|
|
264
|
-
<span className="text-sm font-medium">{c.
|
|
265
|
-
<span className="text-sm text-muted-foreground">
|
|
266
|
-
|
|
282
|
+
<span className="text-sm font-medium">{c.Cost_Category__c?.value ?? "Cost"}</span>
|
|
283
|
+
<span className="text-sm text-muted-foreground">
|
|
284
|
+
{formatCurrency(c.Cost_Amount__c?.value ?? null)}
|
|
285
|
+
</span>
|
|
286
|
+
{c.Cost_Date__c?.value && (
|
|
267
287
|
<span className="w-full text-xs text-muted-foreground">
|
|
268
|
-
{formatDate(c.
|
|
288
|
+
{formatDate(c.Cost_Date__c.value)}
|
|
269
289
|
</span>
|
|
270
290
|
)}
|
|
271
|
-
{c.
|
|
291
|
+
{c.Description__c?.value && (
|
|
292
|
+
<span className="w-full text-xs">{c.Description__c.value}</span>
|
|
293
|
+
)}
|
|
272
294
|
</li>
|
|
273
295
|
))}
|
|
274
296
|
</ul>
|
|
@@ -289,11 +311,11 @@ export default function PropertyDetails() {
|
|
|
289
311
|
<div className="flex flex-wrap gap-1.5">
|
|
290
312
|
{features.map((f) => (
|
|
291
313
|
<span
|
|
292
|
-
key={f.
|
|
314
|
+
key={f.Id}
|
|
293
315
|
className="rounded-full border border-border bg-muted/60 px-2.5 py-0.5 text-xs font-medium text-muted-foreground"
|
|
294
316
|
>
|
|
295
|
-
{f.
|
|
296
|
-
{f.
|
|
317
|
+
{f.Feature_Category__c?.value ? `${f.Feature_Category__c.value}: ` : ""}
|
|
318
|
+
{f.Description__c?.value ?? f.Name?.value ?? "—"}
|
|
297
319
|
</span>
|
|
298
320
|
))}
|
|
299
321
|
</div>
|
|
@@ -307,7 +329,7 @@ export default function PropertyDetails() {
|
|
|
307
329
|
size="sm"
|
|
308
330
|
className="w-full cursor-pointer rounded-xl bg-primary px-5 py-5 text-lg font-medium transition-colors duration-200 hover:bg-primary/90"
|
|
309
331
|
>
|
|
310
|
-
<Link to={`/application?
|
|
332
|
+
<Link to={`/application?propertyId=${encodeURIComponent(id ?? "")}`}>
|
|
311
333
|
Fill out an application
|
|
312
334
|
</Link>
|
|
313
335
|
</Button>
|