@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
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
query PropertyDetailById($propertyId: ID!) {
|
|
2
|
+
uiapi {
|
|
3
|
+
query {
|
|
4
|
+
Property__c(where: { Id: { eq: $propertyId } }, first: 1) {
|
|
5
|
+
edges {
|
|
6
|
+
node {
|
|
7
|
+
Id
|
|
8
|
+
Name @optional {
|
|
9
|
+
value
|
|
10
|
+
displayValue
|
|
11
|
+
}
|
|
12
|
+
Address__c @optional {
|
|
13
|
+
value
|
|
14
|
+
displayValue
|
|
15
|
+
}
|
|
16
|
+
Coordinates__Latitude__s @optional {
|
|
17
|
+
value
|
|
18
|
+
}
|
|
19
|
+
Coordinates__Longitude__s @optional {
|
|
20
|
+
value
|
|
21
|
+
}
|
|
22
|
+
Type__c @optional {
|
|
23
|
+
value
|
|
24
|
+
displayValue
|
|
25
|
+
}
|
|
26
|
+
Monthly_Rent__c @optional {
|
|
27
|
+
value
|
|
28
|
+
displayValue
|
|
29
|
+
}
|
|
30
|
+
Bedrooms__c @optional {
|
|
31
|
+
value
|
|
32
|
+
displayValue
|
|
33
|
+
}
|
|
34
|
+
Bathrooms__c @optional {
|
|
35
|
+
value
|
|
36
|
+
displayValue
|
|
37
|
+
}
|
|
38
|
+
Sq_Ft__c @optional {
|
|
39
|
+
value
|
|
40
|
+
displayValue
|
|
41
|
+
}
|
|
42
|
+
Description__c @optional {
|
|
43
|
+
value
|
|
44
|
+
displayValue
|
|
45
|
+
}
|
|
46
|
+
Property_Images__r(first: 50, orderBy: { Display_Order__c: { order: ASC } }) {
|
|
47
|
+
edges {
|
|
48
|
+
node {
|
|
49
|
+
Id
|
|
50
|
+
Name @optional {
|
|
51
|
+
value
|
|
52
|
+
}
|
|
53
|
+
Image_URL__c @optional {
|
|
54
|
+
value
|
|
55
|
+
}
|
|
56
|
+
Image_Type__c @optional {
|
|
57
|
+
value
|
|
58
|
+
}
|
|
59
|
+
Display_Order__c @optional {
|
|
60
|
+
value
|
|
61
|
+
}
|
|
62
|
+
Alt_Text__c @optional {
|
|
63
|
+
value
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
Property_Features__r(first: 100) {
|
|
69
|
+
edges {
|
|
70
|
+
node {
|
|
71
|
+
Id
|
|
72
|
+
Name @optional {
|
|
73
|
+
value
|
|
74
|
+
}
|
|
75
|
+
Feature_Category__c @optional {
|
|
76
|
+
value
|
|
77
|
+
}
|
|
78
|
+
Description__c @optional {
|
|
79
|
+
value
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
Property_Costs__r(first: 100) {
|
|
85
|
+
edges {
|
|
86
|
+
node {
|
|
87
|
+
Id
|
|
88
|
+
Cost_Category__c @optional {
|
|
89
|
+
value
|
|
90
|
+
}
|
|
91
|
+
Cost_Amount__c @optional {
|
|
92
|
+
value
|
|
93
|
+
}
|
|
94
|
+
Cost_Date__c @optional {
|
|
95
|
+
value
|
|
96
|
+
}
|
|
97
|
+
Description__c @optional {
|
|
98
|
+
value
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
Property_Listings__r(first: 10) {
|
|
104
|
+
edges {
|
|
105
|
+
node {
|
|
106
|
+
Id
|
|
107
|
+
Name @optional {
|
|
108
|
+
value
|
|
109
|
+
}
|
|
110
|
+
Listing_Price__c @optional {
|
|
111
|
+
value
|
|
112
|
+
}
|
|
113
|
+
Listing_Status__c @optional {
|
|
114
|
+
value
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
query SearchProperties(
|
|
2
|
+
$first: Int
|
|
3
|
+
$after: String
|
|
4
|
+
$where: Property__c_Filter
|
|
5
|
+
$orderBy: Property__c_OrderBy
|
|
6
|
+
) {
|
|
7
|
+
uiapi {
|
|
8
|
+
query {
|
|
9
|
+
Property__c(first: $first, after: $after, where: $where, orderBy: $orderBy) {
|
|
10
|
+
edges {
|
|
11
|
+
node {
|
|
12
|
+
Id
|
|
13
|
+
Name {
|
|
14
|
+
value
|
|
15
|
+
displayValue
|
|
16
|
+
}
|
|
17
|
+
Address__c {
|
|
18
|
+
value
|
|
19
|
+
displayValue
|
|
20
|
+
}
|
|
21
|
+
Status__c {
|
|
22
|
+
value
|
|
23
|
+
displayValue
|
|
24
|
+
}
|
|
25
|
+
Type__c {
|
|
26
|
+
value
|
|
27
|
+
displayValue
|
|
28
|
+
}
|
|
29
|
+
Monthly_Rent__c {
|
|
30
|
+
value
|
|
31
|
+
displayValue
|
|
32
|
+
}
|
|
33
|
+
Bedrooms__c {
|
|
34
|
+
value
|
|
35
|
+
displayValue
|
|
36
|
+
}
|
|
37
|
+
Coordinates__Latitude__s {
|
|
38
|
+
value
|
|
39
|
+
}
|
|
40
|
+
Coordinates__Longitude__s {
|
|
41
|
+
value
|
|
42
|
+
}
|
|
43
|
+
CreatedDate {
|
|
44
|
+
value
|
|
45
|
+
displayValue
|
|
46
|
+
}
|
|
47
|
+
Property_Images__r(first: 5, orderBy: { Display_Order__c: { order: ASC } }) {
|
|
48
|
+
edges {
|
|
49
|
+
node {
|
|
50
|
+
Id
|
|
51
|
+
Image_URL__c {
|
|
52
|
+
value
|
|
53
|
+
}
|
|
54
|
+
Image_Type__c {
|
|
55
|
+
value
|
|
56
|
+
}
|
|
57
|
+
Display_Order__c {
|
|
58
|
+
value
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
Property_Features__r(first: 20) {
|
|
64
|
+
edges {
|
|
65
|
+
node {
|
|
66
|
+
Id
|
|
67
|
+
Description__c {
|
|
68
|
+
value
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
pageInfo {
|
|
76
|
+
hasNextPage
|
|
77
|
+
hasPreviousPage
|
|
78
|
+
endCursor
|
|
79
|
+
startCursor
|
|
80
|
+
}
|
|
81
|
+
totalCount
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -8,7 +8,7 @@ export default function AppLayout() {
|
|
|
8
8
|
const [isNavOpen, setIsNavOpen] = useState(false);
|
|
9
9
|
|
|
10
10
|
return (
|
|
11
|
-
<div className="flex flex-col">
|
|
11
|
+
<div className="flex min-h-screen flex-col bg-gray-50">
|
|
12
12
|
<Toaster />
|
|
13
13
|
<TopBar onMenuClick={() => setIsNavOpen(true)} />
|
|
14
14
|
|
|
@@ -19,13 +19,16 @@ export function SkeletonListRows({ count = 3 }: { count?: number }) {
|
|
|
19
19
|
return (
|
|
20
20
|
<>
|
|
21
21
|
{Array.from({ length: count }, (_, i) => (
|
|
22
|
-
<div key={i} className="flex items-center
|
|
23
|
-
<Skeleton className="
|
|
24
|
-
<div className="min-w-0
|
|
25
|
-
<
|
|
26
|
-
|
|
22
|
+
<div key={i} className="flex items-center rounded-lg bg-gray-50 p-4">
|
|
23
|
+
<Skeleton className="h-12 w-12 shrink-0 rounded-lg" />
|
|
24
|
+
<div className="ml-4 min-w-0 grow space-y-1">
|
|
25
|
+
<div className="flex items-center gap-2">
|
|
26
|
+
<Skeleton className="h-5 w-24" />
|
|
27
|
+
<Skeleton className="h-4 w-20" />
|
|
28
|
+
</div>
|
|
29
|
+
<Skeleton className="h-5 w-3/5" />
|
|
27
30
|
</div>
|
|
28
|
-
<Skeleton className="h-
|
|
31
|
+
<Skeleton className="ml-4 h-7 w-24 shrink-0 rounded-full" />
|
|
29
32
|
</div>
|
|
30
33
|
))}
|
|
31
34
|
</>
|
|
@@ -52,40 +52,56 @@ const Divider = () => <div className="my-5 border-t border-gray-200" />;
|
|
|
52
52
|
|
|
53
53
|
function WeatherSkeleton() {
|
|
54
54
|
return (
|
|
55
|
-
<div className="mt-5
|
|
56
|
-
|
|
57
|
-
<div className="flex items-
|
|
58
|
-
<
|
|
59
|
-
|
|
60
|
-
|
|
55
|
+
<div className="mt-5" aria-hidden="true">
|
|
56
|
+
{/* CurrentConditions: date row + city */}
|
|
57
|
+
<div className="flex items-baseline justify-between">
|
|
58
|
+
<Skeleton className="h-5 w-32" />
|
|
59
|
+
<Skeleton className="h-5 w-24" />
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
{/* CurrentConditions: description + temperature + icon */}
|
|
63
|
+
<div className="mt-2 flex items-center justify-between">
|
|
64
|
+
<div>
|
|
65
|
+
<Skeleton className="h-5 w-24" />
|
|
66
|
+
<Skeleton className="mt-1 h-[72px] w-36" />
|
|
61
67
|
</div>
|
|
62
68
|
<Skeleton className="h-20 w-20 rounded-full" />
|
|
63
69
|
</div>
|
|
64
|
-
|
|
65
|
-
|
|
70
|
+
|
|
71
|
+
{/* Divider */}
|
|
72
|
+
<div className="my-5 border-t border-gray-200" />
|
|
73
|
+
|
|
74
|
+
{/* CurrentConditions: stats grid */}
|
|
75
|
+
<div className="grid grid-cols-3 gap-2 text-center">
|
|
66
76
|
{[0, 1, 2].map((i) => (
|
|
67
|
-
<div key={i} className="flex flex-col items-center gap-1
|
|
77
|
+
<div key={i} className="flex flex-col items-center gap-1">
|
|
68
78
|
<Skeleton className="h-5 w-5 rounded-full" />
|
|
69
|
-
<Skeleton className="h-
|
|
70
|
-
<Skeleton className="h-
|
|
79
|
+
<Skeleton className="h-5 w-16" />
|
|
80
|
+
<Skeleton className="h-4 w-12" />
|
|
71
81
|
</div>
|
|
72
82
|
))}
|
|
73
83
|
</div>
|
|
74
|
-
|
|
84
|
+
|
|
85
|
+
{/* Divider */}
|
|
86
|
+
<div className="my-5 border-t border-gray-200" />
|
|
87
|
+
|
|
88
|
+
{/* ForecastTabs */}
|
|
75
89
|
<div className="flex gap-6">
|
|
76
|
-
<Skeleton className="h-
|
|
77
|
-
<Skeleton className="h-
|
|
78
|
-
<Skeleton className="h-
|
|
90
|
+
<Skeleton className="h-5 w-12" />
|
|
91
|
+
<Skeleton className="h-5 w-16" />
|
|
92
|
+
<Skeleton className="h-5 w-20" />
|
|
79
93
|
</div>
|
|
80
|
-
|
|
94
|
+
|
|
95
|
+
{/* HourlyForecast */}
|
|
96
|
+
<div className="mt-4 flex gap-3 overflow-x-auto pb-1">
|
|
81
97
|
{[0, 1, 2, 3].map((i) => (
|
|
82
98
|
<div
|
|
83
99
|
key={i}
|
|
84
|
-
className="flex w-[70px] flex-col items-center gap-1.5 rounded-2xl border border-gray-100 px-3 py-3"
|
|
100
|
+
className="flex min-w-[70px] flex-col items-center gap-1.5 rounded-2xl border border-gray-100 bg-gray-50/80 px-3 py-3"
|
|
85
101
|
>
|
|
86
|
-
<Skeleton className="h-
|
|
102
|
+
<Skeleton className="h-4 w-10" />
|
|
87
103
|
<Skeleton className="h-5 w-5 rounded-full" />
|
|
88
|
-
<Skeleton className="h-
|
|
104
|
+
<Skeleton className="h-5 w-8" />
|
|
89
105
|
</div>
|
|
90
106
|
))}
|
|
91
107
|
</div>
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { useState } from "react";
|
|
2
|
-
import type {
|
|
2
|
+
import type { MaintenanceRequestNode } from "@/api/maintenanceRequests/maintenanceRequestApi";
|
|
3
3
|
import MaintenanceRequestListItem from "@/components/maintenanceRequests/MaintenanceRequestListItem";
|
|
4
4
|
import MaintenanceSummaryDetailsModal from "@/components/maintenanceRequests/MaintenanceSummaryDetailsModal";
|
|
5
5
|
import { SkeletonListRows } from "@/components/SkeletonPrimitives";
|
|
6
6
|
|
|
7
7
|
interface MaintenanceRequestListProps {
|
|
8
|
-
requests:
|
|
8
|
+
requests: MaintenanceRequestNode[];
|
|
9
9
|
loading: boolean;
|
|
10
10
|
error: string | null;
|
|
11
11
|
emptyMessage?: string;
|
|
12
|
+
skeletonCount?: number;
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
export default function MaintenanceRequestList({
|
|
@@ -16,8 +17,9 @@ export default function MaintenanceRequestList({
|
|
|
16
17
|
loading,
|
|
17
18
|
error,
|
|
18
19
|
emptyMessage = "No maintenance requests",
|
|
20
|
+
skeletonCount = 3,
|
|
19
21
|
}: MaintenanceRequestListProps) {
|
|
20
|
-
const [selectedRequest, setSelectedRequest] = useState<
|
|
22
|
+
const [selectedRequest, setSelectedRequest] = useState<MaintenanceRequestNode | null>(null);
|
|
21
23
|
|
|
22
24
|
return (
|
|
23
25
|
<>
|
|
@@ -27,7 +29,7 @@ export default function MaintenanceRequestList({
|
|
|
27
29
|
onClose={() => setSelectedRequest(null)}
|
|
28
30
|
/>
|
|
29
31
|
)}
|
|
30
|
-
{loading && <SkeletonListRows count={
|
|
32
|
+
{loading && <SkeletonListRows count={skeletonCount} />}
|
|
31
33
|
{error && (
|
|
32
34
|
<p className="py-4 text-sm text-destructive" role="alert">
|
|
33
35
|
{error}
|
|
@@ -40,7 +42,7 @@ export default function MaintenanceRequestList({
|
|
|
40
42
|
!error &&
|
|
41
43
|
requests.map((request) => (
|
|
42
44
|
<MaintenanceRequestListItem
|
|
43
|
-
key={request.
|
|
45
|
+
key={request.Id}
|
|
44
46
|
request={request}
|
|
45
47
|
onClick={setSelectedRequest}
|
|
46
48
|
/>
|
|
@@ -2,25 +2,26 @@
|
|
|
2
2
|
* Single maintenance request row: icon (teal) | Type & address + title | tenant (gray circle) [| status].
|
|
3
3
|
*/
|
|
4
4
|
import { useCallback } from "react";
|
|
5
|
-
import type {
|
|
5
|
+
import type { MaintenanceRequestNode } from "@/api/maintenanceRequests/maintenanceRequestApi";
|
|
6
6
|
import { MaintenanceRequestIcon } from "@/components/maintenanceRequests/MaintenanceRequestIcon";
|
|
7
7
|
import { StatusBadge } from "@/components/maintenanceRequests/StatusBadge";
|
|
8
8
|
|
|
9
9
|
export interface MaintenanceRequestListItemProps {
|
|
10
|
-
request:
|
|
10
|
+
request: MaintenanceRequestNode;
|
|
11
11
|
/** When set, row is clickable and opens details (e.g. modal). */
|
|
12
|
-
onClick?: (request:
|
|
12
|
+
onClick?: (request: MaintenanceRequestNode) => void;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
export default function MaintenanceRequestListItem({
|
|
16
16
|
request,
|
|
17
17
|
onClick,
|
|
18
18
|
}: MaintenanceRequestListItemProps) {
|
|
19
|
-
const issueType = request.
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
19
|
+
const issueType = request.Type__c?.value ?? "General";
|
|
20
|
+
const propertyAddress = request.Property__r?.Address__c?.value ?? null;
|
|
21
|
+
const addressFirstPart = propertyAddress
|
|
22
|
+
? propertyAddress.split(",")[0].trim()
|
|
23
|
+
: (request.Name?.value ?? "—");
|
|
24
|
+
const title = request.Description__c?.value?.trim() || request.Name?.value?.trim() || "—";
|
|
24
25
|
|
|
25
26
|
const handleClick = useCallback(() => {
|
|
26
27
|
onClick?.(request);
|
|
@@ -51,7 +52,7 @@ export default function MaintenanceRequestListItem({
|
|
|
51
52
|
: undefined
|
|
52
53
|
}
|
|
53
54
|
>
|
|
54
|
-
<MaintenanceRequestIcon type={request.
|
|
55
|
+
<MaintenanceRequestIcon type={request.Type__c?.value ?? null} />
|
|
55
56
|
|
|
56
57
|
{/* Issue Type and Address - Fixed width; title below to save space (avoids clipping) */}
|
|
57
58
|
<div className="ml-4 min-w-0 grow">
|
|
@@ -66,7 +67,7 @@ export default function MaintenanceRequestListItem({
|
|
|
66
67
|
</div>
|
|
67
68
|
|
|
68
69
|
<div className="ml-4 flex flex-shrink-0 items-center">
|
|
69
|
-
<StatusBadge status={request.
|
|
70
|
+
<StatusBadge status={request.Status__c?.value ?? "—"} />
|
|
70
71
|
</div>
|
|
71
72
|
</div>
|
|
72
73
|
);
|
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
import { useEffect } from "react";
|
|
5
5
|
import { X } from "lucide-react";
|
|
6
6
|
import { StatusBadge } from "@/components/maintenanceRequests/StatusBadge";
|
|
7
|
-
import type {
|
|
7
|
+
import type { MaintenanceRequestNode } from "@/api/maintenanceRequests/maintenanceRequestApi";
|
|
8
8
|
|
|
9
9
|
export interface MaintenanceSummaryDetailsModalProps {
|
|
10
|
-
request:
|
|
10
|
+
request: MaintenanceRequestNode;
|
|
11
11
|
onClose: () => void;
|
|
12
12
|
}
|
|
13
13
|
|
|
@@ -49,35 +49,40 @@ export default function MaintenanceSummaryDetailsModal({
|
|
|
49
49
|
className="relative max-h-[90vh] w-full max-w-lg overflow-y-auto rounded-lg bg-white shadow-xl"
|
|
50
50
|
>
|
|
51
51
|
<div className="flex items-center justify-between border-b p-4">
|
|
52
|
-
<h2 className="text-lg font-semibold">
|
|
52
|
+
<h2 className="text-lg font-semibold">
|
|
53
|
+
{request.Description__c?.value ?? request.Name?.value ?? "Request"}
|
|
54
|
+
</h2>
|
|
53
55
|
<button type="button" onClick={onClose} className="text-gray-500 hover:text-gray-800">
|
|
54
56
|
<X className="h-5 w-5" />
|
|
55
57
|
</button>
|
|
56
58
|
</div>
|
|
57
59
|
<div className="space-y-3 p-4 text-sm">
|
|
58
|
-
{request.
|
|
60
|
+
{request.Description__c?.value && (
|
|
61
|
+
<p className="text-gray-700">{request.Description__c.value}</p>
|
|
62
|
+
)}
|
|
59
63
|
<div className="flex flex-wrap gap-2">
|
|
60
|
-
{request.
|
|
61
|
-
<span className="rounded bg-gray-100 px-2 py-0.5">{request.
|
|
64
|
+
{request.Type__c?.value && (
|
|
65
|
+
<span className="rounded bg-gray-100 px-2 py-0.5">{request.Type__c.value}</span>
|
|
62
66
|
)}
|
|
63
|
-
{request.
|
|
64
|
-
<span className="rounded bg-gray-100 px-2 py-0.5">{request.
|
|
67
|
+
{request.Priority__c?.value && (
|
|
68
|
+
<span className="rounded bg-gray-100 px-2 py-0.5">{request.Priority__c.value}</span>
|
|
65
69
|
)}
|
|
66
|
-
{request.
|
|
70
|
+
{request.Status__c?.value && <StatusBadge status={request.Status__c.value} />}
|
|
67
71
|
</div>
|
|
68
|
-
{request.
|
|
72
|
+
{request.Property__r?.Address__c?.value && (
|
|
69
73
|
<p>
|
|
70
|
-
<span className="font-medium">Property:</span> {request.
|
|
74
|
+
<span className="font-medium">Property:</span> {request.Property__r.Address__c.value}
|
|
71
75
|
</p>
|
|
72
76
|
)}
|
|
73
|
-
{request.
|
|
77
|
+
{request.User__r?.Name?.value && (
|
|
74
78
|
<p>
|
|
75
|
-
<span className="font-medium">Tenant:</span> {request.
|
|
79
|
+
<span className="font-medium">Tenant:</span> {request.User__r.Name.value}
|
|
76
80
|
</p>
|
|
77
81
|
)}
|
|
78
|
-
{request.
|
|
82
|
+
{request.Scheduled__c?.value && (
|
|
79
83
|
<p>
|
|
80
|
-
<span className="font-medium">Requested:</span>
|
|
84
|
+
<span className="font-medium">Requested:</span>{" "}
|
|
85
|
+
{formatDate(request.Scheduled__c.value)}
|
|
81
86
|
</p>
|
|
82
87
|
)}
|
|
83
88
|
</div>
|
|
@@ -6,20 +6,9 @@ import { useNavigate } from "react-router";
|
|
|
6
6
|
import { useCallback, type MouseEvent } from "react";
|
|
7
7
|
import { Button } from "@/components/ui/button";
|
|
8
8
|
import { Skeleton } from "@/components/ui/skeleton";
|
|
9
|
-
import type {
|
|
9
|
+
import type { PropertySearchNode } from "@/api/properties/propertySearchService";
|
|
10
10
|
|
|
11
|
-
function
|
|
12
|
-
fields: Record<string, { value?: unknown; displayValue?: string | null }> | undefined,
|
|
13
|
-
apiName: string,
|
|
14
|
-
): string | null {
|
|
15
|
-
const f = fields?.[apiName];
|
|
16
|
-
if (!f || typeof f !== "object") return null;
|
|
17
|
-
if (f.displayValue != null && f.displayValue !== "") return String(f.displayValue);
|
|
18
|
-
if (f.value != null) return typeof f.value === "object" ? null : String(f.value);
|
|
19
|
-
return null;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function formatPrice(val: string | number | null): string {
|
|
11
|
+
function formatPrice(val: string | number | null | undefined): string {
|
|
23
12
|
if (val == null) return "—";
|
|
24
13
|
const n = typeof val === "number" ? val : Number(val);
|
|
25
14
|
if (Number.isNaN(n)) return String(val);
|
|
@@ -32,8 +21,8 @@ function formatPrice(val: string | number | null): string {
|
|
|
32
21
|
);
|
|
33
22
|
}
|
|
34
23
|
|
|
35
|
-
interface PropertyListingCardProps {
|
|
36
|
-
|
|
24
|
+
export interface PropertyListingCardProps {
|
|
25
|
+
node: PropertySearchNode;
|
|
37
26
|
imageUrl: string | null;
|
|
38
27
|
address?: string | null;
|
|
39
28
|
amenities?: string | null;
|
|
@@ -65,23 +54,21 @@ export function PropertyListingCardSkeleton() {
|
|
|
65
54
|
}
|
|
66
55
|
|
|
67
56
|
export default function PropertyListingCard({
|
|
68
|
-
|
|
57
|
+
node,
|
|
69
58
|
imageUrl,
|
|
70
59
|
address,
|
|
71
60
|
amenities,
|
|
72
61
|
loading = false,
|
|
73
62
|
}: PropertyListingCardProps) {
|
|
74
63
|
const navigate = useNavigate();
|
|
75
|
-
const name =
|
|
76
|
-
const price =
|
|
77
|
-
const
|
|
78
|
-
const bedroomsRaw = fieldDisplay(record.fields, "Property__r.Bedrooms__c");
|
|
79
|
-
const bedroomsNum = bedroomsRaw != null && bedroomsRaw !== "" ? Number(bedroomsRaw) : NaN;
|
|
64
|
+
const name = node.Name?.displayValue ?? node.Name?.value ?? "Untitled";
|
|
65
|
+
const price = node.Monthly_Rent__c?.value;
|
|
66
|
+
const bedroomsNum = typeof node.Bedrooms__c?.value === "number" ? node.Bedrooms__c.value : NaN;
|
|
80
67
|
const bedroomsLabel =
|
|
81
68
|
!Number.isNaN(bedroomsNum) && bedroomsNum >= 0
|
|
82
69
|
? `${bedroomsNum} Bedroom${bedroomsNum !== 1 ? "s" : ""}`
|
|
83
70
|
: null;
|
|
84
|
-
const detailPath = `/property/${
|
|
71
|
+
const detailPath = `/property/${node.Id}`;
|
|
85
72
|
|
|
86
73
|
const handleClick = useCallback(() => {
|
|
87
74
|
navigate(detailPath);
|
|
@@ -101,7 +88,7 @@ export default function PropertyListingCard({
|
|
|
101
88
|
return <PropertyListingCardSkeleton />;
|
|
102
89
|
}
|
|
103
90
|
|
|
104
|
-
const displayAddress = (address ??
|
|
91
|
+
const displayAddress = (address ?? "").trim().replace(/\n/g, ", ") || null;
|
|
105
92
|
const amenityLabels = (amenities ?? "")
|
|
106
93
|
.split(/\s*\|\s*/)
|
|
107
94
|
.map((s) => s.trim())
|
|
@@ -180,7 +167,7 @@ export default function PropertyListingCard({
|
|
|
180
167
|
className="mt-4 w-full cursor-pointer rounded-xl bg-primary px-5 py-5 text-lg font-medium transition-colors duration-200 hover:bg-primary/90"
|
|
181
168
|
onClick={(e: MouseEvent<HTMLButtonElement>) => {
|
|
182
169
|
e.stopPropagation();
|
|
183
|
-
navigate(`/application?
|
|
170
|
+
navigate(`/application?propertyId=${encodeURIComponent(node.Id)}`);
|
|
184
171
|
}}
|
|
185
172
|
>
|
|
186
173
|
Apply
|
package/dist/force-app/main/default/webapplications/propertyrentalapp/src/hooks/useGeocode.ts
CHANGED
|
@@ -1,49 +1,33 @@
|
|
|
1
|
-
import { useState, useEffect } from "react";
|
|
2
1
|
import { geocodeAddress, getStateZipFromAddress, type GeocodeResult } from "@/utils/geocode";
|
|
2
|
+
import { useCachedAsyncData } from "@/features/object-search/hooks/useCachedAsyncData";
|
|
3
|
+
|
|
4
|
+
async function geocodeWithFallback(address: string): Promise<GeocodeResult | null> {
|
|
5
|
+
const normalized = address.replace(/\n/g, ", ").trim();
|
|
6
|
+
const result = await geocodeAddress(normalized);
|
|
7
|
+
if (result != null) return result;
|
|
8
|
+
|
|
9
|
+
// Fallback: try state + zip if full address failed
|
|
10
|
+
const stateZip = getStateZipFromAddress(normalized);
|
|
11
|
+
if (stateZip !== normalized) {
|
|
12
|
+
return geocodeAddress(stateZip);
|
|
13
|
+
}
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
3
16
|
|
|
4
17
|
export function useGeocode(address: string | null | undefined): {
|
|
5
18
|
coords: GeocodeResult | null;
|
|
6
19
|
loading: boolean;
|
|
7
20
|
} {
|
|
8
|
-
const
|
|
9
|
-
const [loading, setLoading] = useState(false);
|
|
10
|
-
const trimmedAddress = address?.trim() ?? "";
|
|
11
|
-
|
|
12
|
-
useEffect(() => {
|
|
13
|
-
if (!trimmedAddress) {
|
|
14
|
-
setCoords(null);
|
|
15
|
-
setLoading(false);
|
|
16
|
-
return;
|
|
17
|
-
}
|
|
18
|
-
let cancelled = false;
|
|
19
|
-
setLoading(true);
|
|
20
|
-
const normalized = address!.replace(/\n/g, ", ").trim();
|
|
21
|
-
|
|
22
|
-
(async () => {
|
|
23
|
-
try {
|
|
24
|
-
let result = await geocodeAddress(normalized);
|
|
25
|
-
if (cancelled) return;
|
|
26
|
-
if (result != null) {
|
|
27
|
-
setCoords(result);
|
|
28
|
-
return;
|
|
29
|
-
}
|
|
30
|
-
// Fallback: same as property search – try state + zip if full address failed
|
|
31
|
-
const stateZip = getStateZipFromAddress(normalized);
|
|
32
|
-
if (stateZip !== normalized) {
|
|
33
|
-
result = await geocodeAddress(stateZip);
|
|
34
|
-
if (!cancelled) setCoords(result);
|
|
35
|
-
}
|
|
36
|
-
} catch {
|
|
37
|
-
if (!cancelled) setCoords(null);
|
|
38
|
-
} finally {
|
|
39
|
-
if (!cancelled) setLoading(false);
|
|
40
|
-
}
|
|
41
|
-
})();
|
|
21
|
+
const trimmed = address?.trim() ?? "";
|
|
42
22
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
23
|
+
const { data: coords, loading } = useCachedAsyncData(
|
|
24
|
+
() => {
|
|
25
|
+
if (!trimmed) return Promise.resolve(null);
|
|
26
|
+
return geocodeWithFallback(trimmed);
|
|
27
|
+
},
|
|
28
|
+
[trimmed],
|
|
29
|
+
{ key: `geocode:${trimmed}`, ttl: 600_000 },
|
|
30
|
+
);
|
|
47
31
|
|
|
48
32
|
return { coords, loading };
|
|
49
33
|
}
|