@salesforce/webapp-template-app-react-sample-b2x-experimental 1.68.0 → 1.69.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/CHANGELOG.md +16 -0
- package/dist/force-app/main/default/data/Lease__c.json +13 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/package.json +13 -8
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/applicationApi.ts +78 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/graphqlClient.ts +17 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/index.ts +19 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/leadApi.ts +69 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/maintenanceRequestApi.ts +177 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/objectDetailService.ts +125 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/objectInfoGraphQLService.ts +194 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/objectInfoService.ts +199 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/propertyDetailGraphQL.ts +497 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/propertyListingGraphQL.ts +190 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/recordListGraphQLService.ts +365 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/appLayout.tsx +20 -30
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/FiltersPanel.tsx +375 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/LoadingFallback.tsx +61 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/PropertyListingCard.tsx +164 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/PropertyMap.tsx +113 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/SearchResultCard.tsx +131 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/alerts/status-alert.tsx +45 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/DetailFields.tsx +55 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/DetailForm.tsx +146 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/DetailHeader.tsx +34 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/DetailLayoutSections.tsx +80 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/Section.tsx +108 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/SectionRow.tsx +20 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/UiApiDetailForm.tsx +140 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/formatted/FieldValueDisplay.tsx +73 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/formatted/FormattedAddress.tsx +29 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/formatted/FormattedEmail.tsx +17 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/formatted/FormattedPhone.tsx +24 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/formatted/FormattedText.tsx +11 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/formatted/FormattedUrl.tsx +29 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/formatted/index.ts +6 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/filters/FilterField.tsx +54 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/filters/FilterInput.tsx +55 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/filters/FilterSelect.tsx +72 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/forms/filters-form.tsx +114 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/forms/submit-button.tsx +47 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/layout/card-layout.tsx +19 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/search/ResultCardFields.tsx +71 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/search/SearchHeader.tsx +31 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/search/SearchPagination.tsx +144 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/search/SearchResultsPanel.tsx +197 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/shared/GlobalSearchInput.tsx +114 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/constants.ts +39 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/index.ts +33 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/form.tsx +204 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/index.ts +22 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/useGeocode.ts +35 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/useMaintenanceRequests.ts +39 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/useObjectInfoBatch.ts +65 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/useObjectSearchData.ts +395 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyAddresses.ts +36 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyDetail.ts +99 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyListingSearch.ts +75 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyMapMarkers.ts +100 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyPrimaryImages.ts +51 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/useRecordDetailLayout.ts +156 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/useRecordListGraphQL.ts +135 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/useWeather.ts +173 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Application.tsx +263 -76
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Contact.tsx +158 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Dashboard.tsx +137 -65
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/DetailPage.tsx +109 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/GlobalSearch.tsx +229 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Home.tsx +469 -21
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Maintenance.tsx +244 -95
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/PropertyDetails.tsx +211 -39
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/PropertyListings.tsx +26 -10
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/PropertySearch.tsx +165 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/PropertySearchPlaceholder.tsx +49 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-01.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-02.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-03.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-04.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-05.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-06.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-07.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-08.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-09.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-10.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-11.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-12.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-13.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-14.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-15.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-16.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-17.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-18.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-19.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-20.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-21.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-22.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-23.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-24.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-25.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/routes.tsx +32 -6
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/styles/global.css +23 -63
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/types/filters/filters.ts +120 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/types/filters/picklist.ts +32 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/types/index.ts +4 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/types/leaflet.d.ts +17 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/types/objectInfo/objectInfo.ts +166 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/types/recordDetail/recordDetail.ts +61 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/types/search/searchResults.ts +229 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/apiUtils.ts +125 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/cacheUtils.ts +76 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/debounce.ts +89 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/fieldUtils.ts +354 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/fieldValueExtractor.ts +67 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/filterUtils.ts +32 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/formDataTransformUtils.ts +260 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/formUtils.ts +142 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/geocode.ts +65 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/graphQLNodeFieldUtils.ts +186 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/graphQLObjectInfoAdapter.ts +319 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/graphQLRecordAdapter.ts +90 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/index.ts +59 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/layoutTransformUtils.ts +236 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/linkUtils.ts +14 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/paginationUtils.ts +49 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/recordUtils.ts +159 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/sanitizationUtils.ts +49 -0
- package/dist/package.json +1 -1
- package/package.json +2 -2
- package/dist/force-app/main/default/classes/MaintenanceRequestListAction.cls +0 -111
- package/dist/force-app/main/default/classes/MaintenanceRequestListAction.cls-meta.xml +0 -6
- package/dist/force-app/main/default/classes/MaintenanceRequestUpdatePriorityAction.cls +0 -93
- package/dist/force-app/main/default/classes/MaintenanceRequestUpdatePriorityAction.cls-meta.xml +0 -6
package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Maintenance.tsx
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useState, useCallback } from "react";
|
|
1
2
|
import { Button } from "../components/ui/button";
|
|
2
3
|
import { Input } from "../components/ui/input";
|
|
3
4
|
import { Label } from "../components/ui/label";
|
|
@@ -10,121 +11,269 @@ import {
|
|
|
10
11
|
TableHeader,
|
|
11
12
|
TableRow,
|
|
12
13
|
} from "../components/ui/table";
|
|
13
|
-
import { Calendar,
|
|
14
|
+
import { Calendar, ArrowRight } from "lucide-react";
|
|
15
|
+
import { useMaintenanceRequests } from "@/hooks/useMaintenanceRequests";
|
|
16
|
+
import { createMaintenanceRequest } from "@/api/maintenanceRequestApi";
|
|
17
|
+
|
|
18
|
+
const TYPE_OPTIONS = [
|
|
19
|
+
"Plumbing",
|
|
20
|
+
"Electrical",
|
|
21
|
+
"HVAC",
|
|
22
|
+
"Appliance",
|
|
23
|
+
"Structural",
|
|
24
|
+
"Cleaning",
|
|
25
|
+
"Security",
|
|
26
|
+
"Pest",
|
|
27
|
+
"Other",
|
|
28
|
+
] as const;
|
|
29
|
+
|
|
30
|
+
const PRIORITY_OPTIONS = [
|
|
31
|
+
{ value: "Standard", label: "Standard" },
|
|
32
|
+
{ value: "High", label: "High (Same Day)" },
|
|
33
|
+
{ value: "Emergency", label: "Emergency (2hr)" },
|
|
34
|
+
] as const;
|
|
35
|
+
|
|
36
|
+
function formatDate(iso: string | null): string {
|
|
37
|
+
if (!iso) return "—";
|
|
38
|
+
try {
|
|
39
|
+
const d = new Date(iso);
|
|
40
|
+
return d.toLocaleDateString("en-US", {
|
|
41
|
+
month: "short",
|
|
42
|
+
day: "numeric",
|
|
43
|
+
year: "numeric",
|
|
44
|
+
});
|
|
45
|
+
} catch {
|
|
46
|
+
return iso;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function statusBadgeClass(status: string | null): string {
|
|
51
|
+
if (!status) return "bg-muted text-muted-foreground";
|
|
52
|
+
switch (status) {
|
|
53
|
+
case "Resolved":
|
|
54
|
+
return "bg-green-100 text-green-700";
|
|
55
|
+
case "In Progress":
|
|
56
|
+
case "Assigned":
|
|
57
|
+
return "bg-blue-100 text-blue-700";
|
|
58
|
+
case "On Hold":
|
|
59
|
+
return "bg-amber-100 text-amber-700";
|
|
60
|
+
case "New":
|
|
61
|
+
default:
|
|
62
|
+
return "bg-muted text-muted-foreground";
|
|
63
|
+
}
|
|
64
|
+
}
|
|
14
65
|
|
|
15
66
|
export default function Maintenance() {
|
|
67
|
+
const { requests, loading, error, refetch } = useMaintenanceRequests();
|
|
68
|
+
const [title, setTitle] = useState("");
|
|
69
|
+
const [description, setDescription] = useState("");
|
|
70
|
+
const [type, setType] = useState<string>("");
|
|
71
|
+
const [priority, setPriority] = useState<string>("Standard");
|
|
72
|
+
const [dateRequested, setDateRequested] = useState(() => {
|
|
73
|
+
const d = new Date();
|
|
74
|
+
return d.toISOString().slice(0, 10);
|
|
75
|
+
});
|
|
76
|
+
const [submitting, setSubmitting] = useState(false);
|
|
77
|
+
const [submitError, setSubmitError] = useState<string | null>(null);
|
|
78
|
+
const [submitSuccess, setSubmitSuccess] = useState(false);
|
|
79
|
+
|
|
80
|
+
const handleSubmit = useCallback(
|
|
81
|
+
async (e: React.FormEvent) => {
|
|
82
|
+
e.preventDefault();
|
|
83
|
+
const t = title.trim();
|
|
84
|
+
if (!t) {
|
|
85
|
+
setSubmitError("Title is required");
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
setSubmitting(true);
|
|
89
|
+
setSubmitError(null);
|
|
90
|
+
setSubmitSuccess(false);
|
|
91
|
+
try {
|
|
92
|
+
await createMaintenanceRequest({
|
|
93
|
+
Title__c: t,
|
|
94
|
+
Description__c: description.trim() || undefined,
|
|
95
|
+
Type__c: type.trim() || undefined,
|
|
96
|
+
Priority__c: priority,
|
|
97
|
+
Status__c: "New",
|
|
98
|
+
Date_Requested__c: dateRequested || undefined,
|
|
99
|
+
});
|
|
100
|
+
setSubmitSuccess(true);
|
|
101
|
+
setTitle("");
|
|
102
|
+
setDescription("");
|
|
103
|
+
setType("");
|
|
104
|
+
setPriority("Standard");
|
|
105
|
+
setDateRequested(new Date().toISOString().slice(0, 10));
|
|
106
|
+
await refetch();
|
|
107
|
+
} catch (err) {
|
|
108
|
+
setSubmitError(err instanceof Error ? err.message : "Failed to submit request");
|
|
109
|
+
} finally {
|
|
110
|
+
setSubmitting(false);
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
[title, description, type, priority, dateRequested, refetch],
|
|
114
|
+
);
|
|
115
|
+
|
|
16
116
|
return (
|
|
17
117
|
<div className="mx-auto max-w-[900px]">
|
|
18
|
-
<Card className="mb-6">
|
|
118
|
+
<Card className="mb-6 rounded-2xl shadow-md">
|
|
19
119
|
<CardHeader>
|
|
20
|
-
<CardTitle className="text-2xl text-primary">
|
|
120
|
+
<CardTitle className="text-2xl text-primary">New maintenance request</CardTitle>
|
|
21
121
|
</CardHeader>
|
|
22
122
|
<CardContent className="space-y-4">
|
|
23
|
-
<
|
|
24
|
-
<div className="
|
|
25
|
-
<
|
|
26
|
-
|
|
123
|
+
<form onSubmit={handleSubmit} className="space-y-4">
|
|
124
|
+
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
|
125
|
+
<div className="space-y-2">
|
|
126
|
+
<Label htmlFor="maintenance-title">Title *</Label>
|
|
27
127
|
<Input
|
|
128
|
+
id="maintenance-title"
|
|
28
129
|
type="text"
|
|
29
|
-
|
|
30
|
-
value
|
|
31
|
-
|
|
32
|
-
aria-label="
|
|
130
|
+
value={title}
|
|
131
|
+
onChange={(e) => setTitle(e.target.value)}
|
|
132
|
+
placeholder="e.g. Kitchen faucet leak"
|
|
133
|
+
aria-label="Title"
|
|
134
|
+
required
|
|
33
135
|
/>
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
</
|
|
136
|
+
</div>
|
|
137
|
+
<div className="space-y-2">
|
|
138
|
+
<Label htmlFor="maintenance-priority">Priority</Label>
|
|
139
|
+
<select
|
|
140
|
+
id="maintenance-priority"
|
|
141
|
+
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"
|
|
142
|
+
aria-label="Priority"
|
|
143
|
+
value={priority}
|
|
144
|
+
onChange={(e) => setPriority(e.target.value)}
|
|
145
|
+
>
|
|
146
|
+
{PRIORITY_OPTIONS.map((o) => (
|
|
147
|
+
<option key={o.value} value={o.value}>
|
|
148
|
+
{o.label}
|
|
149
|
+
</option>
|
|
150
|
+
))}
|
|
151
|
+
</select>
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
|
155
|
+
<div className="space-y-2">
|
|
156
|
+
<Label htmlFor="maintenance-date">Date reported</Label>
|
|
157
|
+
<div className="relative">
|
|
158
|
+
<Input
|
|
159
|
+
id="maintenance-date"
|
|
160
|
+
type="date"
|
|
161
|
+
value={dateRequested}
|
|
162
|
+
onChange={(e) => setDateRequested(e.target.value)}
|
|
163
|
+
className="pr-10"
|
|
164
|
+
aria-label="Date reported"
|
|
165
|
+
/>
|
|
166
|
+
<span className="absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none">
|
|
167
|
+
<Calendar className="size-[18px] text-muted-foreground" aria-hidden />
|
|
168
|
+
</span>
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
<div className="space-y-2">
|
|
172
|
+
<Label htmlFor="maintenance-type">Type</Label>
|
|
173
|
+
<select
|
|
174
|
+
id="maintenance-type"
|
|
175
|
+
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"
|
|
176
|
+
aria-label="Type"
|
|
177
|
+
value={type}
|
|
178
|
+
onChange={(e) => setType(e.target.value)}
|
|
179
|
+
>
|
|
180
|
+
<option value="">—</option>
|
|
181
|
+
{TYPE_OPTIONS.map((o) => (
|
|
182
|
+
<option key={o} value={o}>
|
|
183
|
+
{o}
|
|
184
|
+
</option>
|
|
185
|
+
))}
|
|
186
|
+
</select>
|
|
37
187
|
</div>
|
|
38
188
|
</div>
|
|
39
189
|
<div className="space-y-2">
|
|
40
|
-
<Label>
|
|
41
|
-
<
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
190
|
+
<Label htmlFor="maintenance-description">Description</Label>
|
|
191
|
+
<textarea
|
|
192
|
+
id="maintenance-description"
|
|
193
|
+
rows={4}
|
|
194
|
+
placeholder="Describe the issue"
|
|
195
|
+
className="min-h-[100px] w-full resize-y rounded-xl border border-input bg-transparent px-3 py-2 text-sm shadow-sm outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:border-primary transition-colors duration-200"
|
|
196
|
+
aria-label="Description"
|
|
197
|
+
value={description}
|
|
198
|
+
onChange={(e) => setDescription(e.target.value)}
|
|
199
|
+
/>
|
|
50
200
|
</div>
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
<select
|
|
55
|
-
className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
56
|
-
aria-label="Room"
|
|
57
|
-
defaultValue="living-room"
|
|
58
|
-
>
|
|
59
|
-
<option value="living-room">Living Room</option>
|
|
60
|
-
<option value="bathroom">Bathroom</option>
|
|
61
|
-
<option value="bedroom">Bedroom</option>
|
|
62
|
-
<option value="kitchen">Kitchen</option>
|
|
63
|
-
</select>
|
|
64
|
-
</div>
|
|
65
|
-
<div className="space-y-2">
|
|
66
|
-
<Label>Description</Label>
|
|
67
|
-
<textarea
|
|
68
|
-
rows={4}
|
|
69
|
-
placeholder="Input text"
|
|
70
|
-
className="min-h-[100px] w-full resize-y rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-xs outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
71
|
-
aria-label="Description"
|
|
72
|
-
/>
|
|
73
|
-
</div>
|
|
74
|
-
<div className="grid grid-cols-2 gap-4">
|
|
75
|
-
<div />
|
|
76
|
-
<div className="flex flex-col items-center justify-center rounded-xl border-2 border-dashed border-border bg-muted/30 p-6 text-center">
|
|
77
|
-
<Upload className="mx-auto mb-2 size-10 text-muted-foreground" />
|
|
78
|
-
<p className="m-0 text-xs text-muted-foreground">
|
|
79
|
-
Choose a file or drag & drop for reference here.
|
|
201
|
+
{submitError && (
|
|
202
|
+
<p className="text-sm text-destructive" role="alert">
|
|
203
|
+
{submitError}
|
|
80
204
|
</p>
|
|
81
|
-
|
|
82
|
-
|
|
205
|
+
)}
|
|
206
|
+
{submitSuccess && (
|
|
207
|
+
<p className="text-sm text-green-600" role="status">
|
|
208
|
+
Request submitted. It will appear in the list below.
|
|
83
209
|
</p>
|
|
210
|
+
)}
|
|
211
|
+
<div className="flex justify-end">
|
|
212
|
+
<Button
|
|
213
|
+
type="submit"
|
|
214
|
+
className="cursor-pointer gap-2 rounded-xl transition-colors duration-200"
|
|
215
|
+
disabled={submitting}
|
|
216
|
+
>
|
|
217
|
+
{submitting ? "Submitting…" : "Submit Request"}
|
|
218
|
+
<ArrowRight className="size-[18px]" aria-hidden />
|
|
219
|
+
</Button>
|
|
84
220
|
</div>
|
|
85
|
-
</
|
|
86
|
-
<div className="flex justify-end">
|
|
87
|
-
<Button className="gap-2">
|
|
88
|
-
Submit Request
|
|
89
|
-
<ArrowRight className="size-[18px]" />
|
|
90
|
-
</Button>
|
|
91
|
-
</div>
|
|
221
|
+
</form>
|
|
92
222
|
</CardContent>
|
|
93
223
|
</Card>
|
|
94
|
-
<Card>
|
|
224
|
+
<Card className="rounded-2xl shadow-md">
|
|
225
|
+
<CardHeader>
|
|
226
|
+
<CardTitle className="text-xl text-primary">Your requests</CardTitle>
|
|
227
|
+
</CardHeader>
|
|
95
228
|
<CardContent className="p-0">
|
|
96
|
-
|
|
97
|
-
<
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
<
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
229
|
+
{error && (
|
|
230
|
+
<p className="px-6 py-4 text-sm text-destructive" role="alert">
|
|
231
|
+
{error}
|
|
232
|
+
</p>
|
|
233
|
+
)}
|
|
234
|
+
{loading && <p className="px-6 py-4 text-sm text-muted-foreground">Loading…</p>}
|
|
235
|
+
{!loading && !error && (
|
|
236
|
+
<div className="overflow-x-auto">
|
|
237
|
+
<Table>
|
|
238
|
+
<TableHeader>
|
|
239
|
+
<TableRow>
|
|
240
|
+
<TableHead className="font-semibold text-primary">Title</TableHead>
|
|
241
|
+
<TableHead className="font-semibold text-primary">Work order</TableHead>
|
|
242
|
+
<TableHead className="font-semibold text-primary">Type</TableHead>
|
|
243
|
+
<TableHead className="font-semibold text-primary">Date</TableHead>
|
|
244
|
+
<TableHead className="font-semibold text-primary">Status</TableHead>
|
|
245
|
+
</TableRow>
|
|
246
|
+
</TableHeader>
|
|
247
|
+
<TableBody>
|
|
248
|
+
{requests.length === 0 ? (
|
|
249
|
+
<TableRow>
|
|
250
|
+
<TableCell colSpan={5} className="text-center text-muted-foreground py-8">
|
|
251
|
+
No maintenance requests yet. Submit one above.
|
|
252
|
+
</TableCell>
|
|
253
|
+
</TableRow>
|
|
254
|
+
) : (
|
|
255
|
+
requests.map((r) => (
|
|
256
|
+
<TableRow key={r.id}>
|
|
257
|
+
<TableCell className="font-medium">
|
|
258
|
+
{r.title ?? r.description ?? "—"}
|
|
259
|
+
</TableCell>
|
|
260
|
+
<TableCell>{r.name ?? "—"}</TableCell>
|
|
261
|
+
<TableCell>{r.type ?? "—"}</TableCell>
|
|
262
|
+
<TableCell>{formatDate(r.dateRequested)}</TableCell>
|
|
263
|
+
<TableCell>
|
|
264
|
+
<span
|
|
265
|
+
className={`inline-block rounded-full px-3 py-1 text-xs font-medium ${statusBadgeClass(r.status)}`}
|
|
266
|
+
>
|
|
267
|
+
{r.status ?? "—"}
|
|
268
|
+
</span>
|
|
269
|
+
</TableCell>
|
|
270
|
+
</TableRow>
|
|
271
|
+
))
|
|
272
|
+
)}
|
|
273
|
+
</TableBody>
|
|
274
|
+
</Table>
|
|
275
|
+
</div>
|
|
276
|
+
)}
|
|
128
277
|
</CardContent>
|
|
129
278
|
</Card>
|
|
130
279
|
</div>
|
package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/PropertyDetails.tsx
CHANGED
|
@@ -1,9 +1,68 @@
|
|
|
1
1
|
import { useParams, Link } from "react-router";
|
|
2
2
|
import { Button } from "../components/ui/button";
|
|
3
|
-
import { Card, CardHeader, CardContent } from "../components/ui/card";
|
|
3
|
+
import { Card, CardHeader, CardTitle, CardContent } from "../components/ui/card";
|
|
4
|
+
import PropertyMap from "@/components/PropertyMap";
|
|
5
|
+
import { usePropertyDetail } from "@/hooks/usePropertyDetail";
|
|
6
|
+
import { useGeocode } from "@/hooks/useGeocode";
|
|
7
|
+
|
|
8
|
+
function formatCurrency(val: number | string | null): string {
|
|
9
|
+
if (val == null) return "—";
|
|
10
|
+
const n = typeof val === "number" ? val : Number(val);
|
|
11
|
+
return Number.isNaN(n)
|
|
12
|
+
? String(val)
|
|
13
|
+
: new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" }).format(n);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function formatDate(val: string | null): string {
|
|
17
|
+
if (!val) return "—";
|
|
18
|
+
try {
|
|
19
|
+
return new Date(val).toLocaleDateString();
|
|
20
|
+
} catch {
|
|
21
|
+
return val;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
4
24
|
|
|
5
25
|
export default function PropertyDetails() {
|
|
6
|
-
const { id } = useParams();
|
|
26
|
+
const { id } = useParams<{ id: string }>();
|
|
27
|
+
const { listing, property, images, costs, features, loading, error } = usePropertyDetail(id);
|
|
28
|
+
const addressForGeocode = property?.address?.replace(/\n/g, ", ") ?? null;
|
|
29
|
+
const { coords: addressCoords } = useGeocode(addressForGeocode);
|
|
30
|
+
|
|
31
|
+
if (loading) {
|
|
32
|
+
return (
|
|
33
|
+
<div className="mx-auto max-w-[900px]">
|
|
34
|
+
<div className="mb-4 h-4 w-32 animate-pulse rounded bg-muted" />
|
|
35
|
+
<div className="mb-4 grid grid-cols-1 gap-4 md:grid-cols-2">
|
|
36
|
+
<div className="h-72 animate-pulse rounded-xl bg-muted" />
|
|
37
|
+
<div className="flex flex-col gap-2">
|
|
38
|
+
{[1, 2, 3, 4, 5].map((i) => (
|
|
39
|
+
<div key={i} className="h-12 animate-pulse rounded-lg bg-muted" />
|
|
40
|
+
))}
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (error || (!listing && id)) {
|
|
48
|
+
return (
|
|
49
|
+
<div className="mx-auto max-w-[900px]">
|
|
50
|
+
<div className="mb-4">
|
|
51
|
+
<Link to="/properties" className="text-sm text-primary no-underline hover:underline">
|
|
52
|
+
← Back to listings
|
|
53
|
+
</Link>
|
|
54
|
+
</div>
|
|
55
|
+
<Card className="rounded-2xl shadow-md">
|
|
56
|
+
<CardContent className="pt-6">
|
|
57
|
+
<p className="text-destructive">{error ?? "Listing not found."}</p>
|
|
58
|
+
</CardContent>
|
|
59
|
+
</Card>
|
|
60
|
+
</div>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const primaryImage = images.find((i) => i.imageType === "Primary") ?? images[0];
|
|
65
|
+
const otherImages = images.filter((i) => i.id !== primaryImage?.id);
|
|
7
66
|
|
|
8
67
|
return (
|
|
9
68
|
<div className="mx-auto max-w-[900px]">
|
|
@@ -12,57 +71,170 @@ export default function PropertyDetails() {
|
|
|
12
71
|
← Back to listings
|
|
13
72
|
</Link>
|
|
14
73
|
</div>
|
|
74
|
+
|
|
75
|
+
{/* Hero image + thumbnails */}
|
|
15
76
|
<div className="mb-4 grid grid-cols-1 gap-4 md:grid-cols-2">
|
|
16
|
-
<div className="
|
|
77
|
+
<div className="relative aspect-[4/3] overflow-hidden rounded-xl bg-muted">
|
|
78
|
+
{primaryImage?.imageUrl ? (
|
|
79
|
+
<img
|
|
80
|
+
src={primaryImage.imageUrl}
|
|
81
|
+
alt={primaryImage.altText ?? primaryImage.name ?? "Property"}
|
|
82
|
+
className="h-full w-full object-cover"
|
|
83
|
+
/>
|
|
84
|
+
) : (
|
|
85
|
+
<div className="flex h-full items-center justify-center text-muted-foreground">
|
|
86
|
+
No image
|
|
87
|
+
</div>
|
|
88
|
+
)}
|
|
89
|
+
</div>
|
|
17
90
|
<div className="flex flex-col gap-2">
|
|
18
|
-
{
|
|
19
|
-
<div key={
|
|
91
|
+
{otherImages.slice(0, 5).map((img) => (
|
|
92
|
+
<div key={img.id} className="relative h-20 overflow-hidden rounded-lg bg-muted">
|
|
93
|
+
{img.imageUrl ? (
|
|
94
|
+
<img
|
|
95
|
+
src={img.imageUrl}
|
|
96
|
+
alt={img.altText ?? img.name ?? "Property"}
|
|
97
|
+
className="h-full w-full object-cover"
|
|
98
|
+
/>
|
|
99
|
+
) : null}
|
|
100
|
+
</div>
|
|
20
101
|
))}
|
|
21
102
|
</div>
|
|
22
103
|
</div>
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
104
|
+
|
|
105
|
+
{/* Map - geocoded from property address */}
|
|
106
|
+
{addressCoords && (
|
|
107
|
+
<div className="mb-4">
|
|
108
|
+
<PropertyMap
|
|
109
|
+
center={[addressCoords.lat, addressCoords.lng]}
|
|
110
|
+
zoom={15}
|
|
111
|
+
markers={[
|
|
112
|
+
{
|
|
113
|
+
lat: addressCoords.lat,
|
|
114
|
+
lng: addressCoords.lng,
|
|
115
|
+
label: listing?.name ?? property?.name ?? "Property",
|
|
116
|
+
},
|
|
117
|
+
]}
|
|
118
|
+
className="h-[280px] w-full rounded-xl"
|
|
119
|
+
/>
|
|
120
|
+
</div>
|
|
121
|
+
)}
|
|
122
|
+
|
|
123
|
+
{/* Listing + address */}
|
|
124
|
+
<Card className="mb-4 rounded-2xl shadow-md">
|
|
33
125
|
<CardContent className="pt-6">
|
|
34
|
-
|
|
35
|
-
|
|
126
|
+
{property?.address && (
|
|
127
|
+
<p className="mb-2 text-sm text-muted-foreground">
|
|
128
|
+
{property.address.replace(/\n/g, ", ")}
|
|
129
|
+
</p>
|
|
130
|
+
)}
|
|
131
|
+
<p className="mb-1 text-2xl font-bold text-foreground">
|
|
132
|
+
{listing?.listingPrice != null
|
|
133
|
+
? formatCurrency(listing.listingPrice)
|
|
134
|
+
: property?.monthlyRent != null
|
|
135
|
+
? formatCurrency(property.monthlyRent) + " / Month"
|
|
136
|
+
: "—"}
|
|
36
137
|
</p>
|
|
37
|
-
<p className="mb-1 text-2xl font-bold text-foreground">$4,600 / Month</p>
|
|
38
138
|
<p className="mb-4 text-sm text-muted-foreground">
|
|
39
|
-
|
|
139
|
+
{listing?.name ?? property?.name ?? "Untitled"}
|
|
40
140
|
</p>
|
|
41
141
|
<div className="flex flex-wrap gap-3">
|
|
42
|
-
{
|
|
43
|
-
<span
|
|
44
|
-
{s}
|
|
142
|
+
{property?.bedrooms != null && (
|
|
143
|
+
<span className="rounded-lg bg-primary/10 px-3 py-1.5 text-sm text-primary">
|
|
144
|
+
{property.bedrooms} Bedroom{Number(property.bedrooms) !== 1 ? "s" : ""}
|
|
45
145
|
</span>
|
|
46
|
-
)
|
|
146
|
+
)}
|
|
147
|
+
{property?.bathrooms != null && (
|
|
148
|
+
<span className="rounded-lg bg-primary/10 px-3 py-1.5 text-sm text-primary">
|
|
149
|
+
{property.bathrooms} Bath{Number(property.bathrooms) !== 1 ? "s" : ""}
|
|
150
|
+
</span>
|
|
151
|
+
)}
|
|
152
|
+
{property?.squareFootage != null && (
|
|
153
|
+
<span className="rounded-lg bg-primary/10 px-3 py-1.5 text-sm text-primary">
|
|
154
|
+
{property.squareFootage} sq ft
|
|
155
|
+
</span>
|
|
156
|
+
)}
|
|
157
|
+
{listing?.listingStatus && (
|
|
158
|
+
<span className="rounded-lg bg-primary/10 px-3 py-1.5 text-sm text-primary">
|
|
159
|
+
{listing.listingStatus}
|
|
160
|
+
</span>
|
|
161
|
+
)}
|
|
162
|
+
{property?.propertyType && (
|
|
163
|
+
<span className="rounded-lg bg-primary/10 px-3 py-1.5 text-sm text-primary">
|
|
164
|
+
{property.propertyType}
|
|
165
|
+
</span>
|
|
166
|
+
)}
|
|
47
167
|
</div>
|
|
168
|
+
{property?.description && (
|
|
169
|
+
<p className="mt-4 text-sm text-foreground">{property.description}</p>
|
|
170
|
+
)}
|
|
48
171
|
</CardContent>
|
|
49
172
|
</Card>
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
173
|
+
|
|
174
|
+
{/* Related: Costs */}
|
|
175
|
+
{costs.length > 0 && (
|
|
176
|
+
<Card className="mb-4 rounded-2xl shadow-md">
|
|
177
|
+
<CardHeader>
|
|
178
|
+
<CardTitle className="text-base">Related costs</CardTitle>
|
|
179
|
+
</CardHeader>
|
|
180
|
+
<CardContent>
|
|
181
|
+
<ul className="space-y-2">
|
|
182
|
+
{costs.slice(0, 10).map((c) => (
|
|
183
|
+
<li
|
|
184
|
+
key={c.id}
|
|
185
|
+
className="flex flex-wrap items-baseline justify-between gap-2 border-b border-border/50 pb-2 last:border-0"
|
|
186
|
+
>
|
|
187
|
+
<span className="text-sm font-medium">{c.category ?? "Cost"}</span>
|
|
188
|
+
<span className="text-sm text-muted-foreground">{formatCurrency(c.amount)}</span>
|
|
189
|
+
{c.date && (
|
|
190
|
+
<span className="w-full text-xs text-muted-foreground">
|
|
191
|
+
{formatDate(c.date)}
|
|
192
|
+
</span>
|
|
193
|
+
)}
|
|
194
|
+
{c.description && <span className="w-full text-xs">{c.description}</span>}
|
|
195
|
+
</li>
|
|
196
|
+
))}
|
|
197
|
+
</ul>
|
|
198
|
+
{costs.length > 10 && (
|
|
199
|
+
<p className="mt-2 text-xs text-muted-foreground">+ {costs.length - 10} more</p>
|
|
200
|
+
)}
|
|
201
|
+
</CardContent>
|
|
202
|
+
</Card>
|
|
203
|
+
)}
|
|
204
|
+
|
|
205
|
+
{/* Related: Features */}
|
|
206
|
+
{features.length > 0 && (
|
|
207
|
+
<Card className="mb-4 rounded-2xl shadow-md">
|
|
208
|
+
<CardHeader>
|
|
209
|
+
<CardTitle className="text-base">Features & amenities</CardTitle>
|
|
210
|
+
</CardHeader>
|
|
211
|
+
<CardContent>
|
|
212
|
+
<ul className="flex flex-wrap gap-2">
|
|
213
|
+
{features.map((f) => (
|
|
214
|
+
<li
|
|
215
|
+
key={f.id}
|
|
216
|
+
className="rounded-md border border-border bg-muted/50 px-3 py-1.5 text-sm"
|
|
217
|
+
>
|
|
218
|
+
{f.category && <span className="text-muted-foreground">{f.category}: </span>}
|
|
219
|
+
{f.description ?? f.name ?? "—"}
|
|
220
|
+
</li>
|
|
221
|
+
))}
|
|
222
|
+
</ul>
|
|
223
|
+
</CardContent>
|
|
224
|
+
</Card>
|
|
225
|
+
)}
|
|
226
|
+
|
|
227
|
+
<div className="mb-4">
|
|
228
|
+
<Button
|
|
229
|
+
asChild
|
|
230
|
+
size="lg"
|
|
231
|
+
className="w-full cursor-pointer rounded-xl bg-violet-600 text-base font-semibold transition-colors duration-200 hover:bg-violet-700"
|
|
232
|
+
>
|
|
233
|
+
<Link to={`/application?listingId=${encodeURIComponent(id ?? "")}`}>
|
|
234
|
+
Fill out an application
|
|
235
|
+
</Link>
|
|
236
|
+
</Button>
|
|
237
|
+
</div>
|
|
66
238
|
</div>
|
|
67
239
|
);
|
|
68
240
|
}
|