@salesforce/webapp-template-app-react-sample-b2x-experimental 1.116.7 → 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 +16 -0
- package/dist/force-app/main/default/webapplications/propertyrentalapp/eslint.config.js +13 -2
- 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/components/properties/PropertyMap.tsx +2 -1
- package/dist/force-app/main/default/webapplications/propertyrentalapp/src/features/authentication/context/AuthContext.tsx +1 -1
- package/dist/force-app/main/default/webapplications/propertyrentalapp/src/features/authentication/hooks/useCountdownTimer.ts +1 -1
- package/dist/force-app/main/default/webapplications/propertyrentalapp/src/features/authentication/pages/Profile.tsx +3 -3
- package/dist/force-app/main/default/webapplications/propertyrentalapp/src/features/authentication/pages/Register.tsx +1 -1
- package/dist/force-app/main/default/webapplications/propertyrentalapp/src/features/authentication/sessionTimeout/SessionTimeoutValidator.tsx +12 -18
- package/dist/force-app/main/default/webapplications/propertyrentalapp/src/features/object-search/components/FilterContext.tsx +1 -1
- package/dist/force-app/main/default/webapplications/propertyrentalapp/src/features/object-search/hooks/useObjectSearchParams.ts +10 -5
- package/dist/force-app/main/default/webapplications/propertyrentalapp/src/hooks/useGeocode.ts +23 -38
- 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 +34 -41
- package/dist/force-app/main/default/webapplications/propertyrentalapp/src/hooks/useWeather.ts +14 -30
- package/dist/force-app/main/default/webapplications/propertyrentalapp/src/pages/Application.tsx +41 -74
- 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 +97 -8
- 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 +4 -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 -57
- 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 -53
- /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
|
|
@@ -63,9 +63,10 @@ interface PropertyMapProps {
|
|
|
63
63
|
|
|
64
64
|
function MapCenterUpdater({ center, zoom = 13 }: { center: [number, number]; zoom?: number }) {
|
|
65
65
|
const map = useMap() as { setView: (center: [number, number], zoom: number) => void };
|
|
66
|
+
const [lat, lng] = center;
|
|
66
67
|
useEffect(() => {
|
|
67
68
|
map.setView(center, zoom);
|
|
68
|
-
}, [map, center
|
|
69
|
+
}, [map, center, lat, lng, zoom]);
|
|
69
70
|
return null;
|
|
70
71
|
}
|
|
71
72
|
|
|
@@ -86,7 +86,7 @@ export function useAuth(): AuthContextType {
|
|
|
86
86
|
* @returns {User} The authenticated user object
|
|
87
87
|
* @throws {Error} If not used within AuthProvider or user is not authenticated
|
|
88
88
|
*/
|
|
89
|
-
export function
|
|
89
|
+
export function useUser(): User {
|
|
90
90
|
const context = useAuth();
|
|
91
91
|
if (!context.user) {
|
|
92
92
|
throw new Error("Authenticated context not established");
|
|
@@ -107,7 +107,7 @@ function formatAccessibilityAnnouncement(seconds: number): string {
|
|
|
107
107
|
// @ts-expect-error - DurationFormat is not yet in TypeScript lib
|
|
108
108
|
const formatter = new Intl.DurationFormat(navigator.language, { style: "long" });
|
|
109
109
|
return formatter.format({ minutes, seconds: secs });
|
|
110
|
-
} catch
|
|
110
|
+
} catch {
|
|
111
111
|
// Fallback to manual formatting
|
|
112
112
|
}
|
|
113
113
|
}
|
|
@@ -8,7 +8,7 @@ import { useAppForm } from "../hooks/form";
|
|
|
8
8
|
import { ROUTES } from "../authenticationConfig";
|
|
9
9
|
import { emailSchema } from "../authHelpers";
|
|
10
10
|
import { getErrorMessage } from "../utils/helpers";
|
|
11
|
-
import {
|
|
11
|
+
import { useUser } from "../context/AuthContext";
|
|
12
12
|
import { fetchUserProfile, updateUserProfile } from "../api/userProfileApi";
|
|
13
13
|
|
|
14
14
|
const optionalString = z
|
|
@@ -33,7 +33,7 @@ const profileSchema = z.object({
|
|
|
33
33
|
type ProfileFormValues = z.infer<typeof profileSchema>;
|
|
34
34
|
|
|
35
35
|
export default function Profile() {
|
|
36
|
-
const user =
|
|
36
|
+
const user = useUser();
|
|
37
37
|
const [profile, setProfile] = useState<ProfileFormValues | null>(null);
|
|
38
38
|
const [loadError, setLoadError] = useState<string | null>(null);
|
|
39
39
|
const [success, setSuccess] = useState(false);
|
|
@@ -104,7 +104,7 @@ export default function Profile() {
|
|
|
104
104
|
const formData = profileSchema.parse(profile);
|
|
105
105
|
form.reset(formData);
|
|
106
106
|
}
|
|
107
|
-
}, [profile]);
|
|
107
|
+
}, [profile, form]);
|
|
108
108
|
|
|
109
109
|
if (!profile && !loadError) {
|
|
110
110
|
return <CardSkeleton contentMaxWidth="md" loadingText="Loading profile…" />;
|
|
@@ -46,7 +46,7 @@ export default function Register() {
|
|
|
46
46
|
// "/services/apexrest/auth/register" refers to a custom Apex Class exposed as a REST resource.
|
|
47
47
|
// You must ensure this Apex class exists in your org and handles registration
|
|
48
48
|
// (e.g., duplicate checks and user creation such as Site.createExternalUser).
|
|
49
|
-
const { confirmPassword, ...request } = formFieldValues;
|
|
49
|
+
const { confirmPassword: _confirmPassword, ...request } = formFieldValues;
|
|
50
50
|
const sdk = await createDataSDK();
|
|
51
51
|
const response = await sdk.fetch!("/services/apexrest/auth/register", {
|
|
52
52
|
method: "POST",
|