@salesforce/ui-bundle-template-app-react-sample-b2e 11.19.0 → 11.21.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/uiBundles/propertymanagementapp/package.json +4 -4
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/api/applications/applicationSearchService.ts +25 -43
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/api/maintenanceRequests/maintenanceRequestSearchService.ts +31 -65
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/api/maintenanceWorkers/maintenanceWorkerSearchService.ts +21 -43
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/api/properties/propertySearchService.ts +33 -53
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/components/applications/ApplicationCard.tsx +80 -0
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/components/dashboard/GlobalSearchBar.tsx +2 -2
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/components/layout/VerticalNav.tsx +28 -18
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/components/maintenanceRequests/MaintenanceRequestCard.tsx +79 -52
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/components/maintenanceWorkers/WorkerCard.tsx +87 -0
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/components/search/UnifiedSearch.tsx +213 -0
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/search/api/distinctValuesService.ts +84 -0
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/search/api/searchService.ts +71 -0
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/search/components/MergedSearchResults.tsx +263 -0
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/search/components/Search.tsx +185 -0
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/search/components/SearchResults.tsx +77 -0
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/search/components/SourceSection.tsx +166 -0
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/search/components/controls/PaginationControls.tsx +185 -0
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/search/components/controls/ScopeSelector.tsx +55 -0
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/search/components/controls/SearchBar.tsx +55 -0
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/search/components/controls/SortControl.tsx +62 -0
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/search/components/filters/ActiveFilters.tsx +61 -0
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/search/components/filters/DefaultFilterPanel.tsx +125 -0
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/{object-search/components → search/components/filters}/FilterContext.tsx +7 -20
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/search/components/filters/inputs/BooleanFilter.tsx +50 -0
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/search/components/filters/inputs/DateRangeFilter.tsx +160 -0
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/search/components/filters/inputs/FilterFieldWrapper.tsx +26 -0
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/search/components/filters/inputs/MultiSelectFilter.tsx +88 -0
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/search/components/filters/inputs/NumericRangeFilter.tsx +78 -0
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/search/components/filters/inputs/SelectFilter.tsx +46 -0
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/search/components/filters/inputs/TextFilter.tsx +52 -0
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/search/components/results/DefaultResultRow.tsx +109 -0
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/search/config.json +83 -0
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/{object-search → search}/hooks/useAsyncData.ts +3 -10
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/search/hooks/useDistinctValues.ts +59 -0
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/search/hooks/useSearch.ts +703 -0
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/search/index.ts +90 -0
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/search/loadConfig.ts +14 -0
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/search/queryBuilder.ts +156 -0
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/search/types.ts +307 -0
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/search/utils/debounce.ts +17 -0
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/search/utils/fieldUtils.ts +14 -0
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/search/utils/filterUtils.ts +285 -0
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/search/utils/sortUtils.ts +34 -0
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/lib/constants.ts +16 -29
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/lib/routeConfig.ts +1 -0
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/lib/searchConfig.ts +190 -0
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/pages/ApplicationSearch.tsx +8 -385
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/pages/Home.tsx +4 -2
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/pages/MaintenanceRequestSearch.tsx +8 -458
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/pages/MaintenanceWorkerSearch.tsx +8 -341
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/pages/PropertySearch.tsx +7 -243
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/pages/Search.tsx +16 -0
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/routes.tsx +6 -0
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/tsconfig.tsbuildinfo +1 -1
- package/dist/package-lock.json +2 -2
- package/dist/package.json +1 -1
- package/package.json +2 -2
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/api/applications/query/distinctApplicationStatus.graphql +0 -19
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/api/applications/query/searchApplications.graphql +0 -65
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/api/maintenanceRequests/query/distinctMaintenanceRequestPriority.graphql +0 -19
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/api/maintenanceRequests/query/distinctMaintenanceRequestStatus.graphql +0 -19
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/api/maintenanceRequests/query/distinctMaintenanceRequestType.graphql +0 -19
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/api/maintenanceRequests/query/searchMaintenanceRequests.graphql +0 -82
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/api/maintenanceWorkers/query/distinctMaintenanceWorkerEmploymentType.graphql +0 -19
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/api/maintenanceWorkers/query/searchMaintenanceWorkers.graphql +0 -64
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/api/properties/query/distinctPropertyStatus.graphql +0 -19
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/api/properties/query/distinctPropertyType.graphql +0 -19
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/api/properties/query/searchProperties.graphql +0 -105
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/components/shared/ObjectSearchErrorState.tsx +0 -45
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/components/shared/UserAvatar.tsx +0 -35
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/object-search/__examples__/api/accountSearchService.ts +0 -46
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/object-search/__examples__/api/query/distinctAccountIndustries.graphql +0 -19
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/object-search/__examples__/api/query/distinctAccountTypes.graphql +0 -19
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/object-search/__examples__/api/query/getAccountDetail.graphql +0 -121
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/object-search/__examples__/api/query/searchAccounts.graphql +0 -51
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/object-search/__examples__/pages/AccountObjectDetailPage.tsx +0 -355
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/object-search/__examples__/pages/AccountSearch.tsx +0 -304
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/object-search/__examples__/pages/Home.tsx +0 -34
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/object-search/api/objectSearchService.ts +0 -82
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/object-search/components/ActiveFilters.tsx +0 -89
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/object-search/components/ObjectBreadcrumb.tsx +0 -66
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/object-search/components/PaginationControls.tsx +0 -109
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/object-search/components/SearchBar.tsx +0 -41
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/object-search/components/SortControl.tsx +0 -143
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/object-search/components/filters/BooleanFilter.tsx +0 -78
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/object-search/components/filters/DateFilter.tsx +0 -128
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/object-search/components/filters/DateRangeFilter.tsx +0 -70
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/object-search/components/filters/FilterFieldWrapper.tsx +0 -43
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/object-search/components/filters/MultiSelectFilter.tsx +0 -105
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/object-search/components/filters/NumericRangeFilter.tsx +0 -171
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/object-search/components/filters/SearchFilter.tsx +0 -52
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/object-search/components/filters/SelectFilter.tsx +0 -97
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/object-search/components/filters/TextFilter.tsx +0 -93
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/object-search/hooks/useDebouncedCallback.ts +0 -34
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/object-search/hooks/useObjectSearchParams.ts +0 -254
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/object-search/utils/debounce.ts +0 -25
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/object-search/utils/fieldUtils.ts +0 -29
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/object-search/utils/filterUtils.ts +0 -404
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/object-search/utils/sortUtils.ts +0 -38
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/lib/filterUtils.ts +0 -11
|
@@ -1,80 +1,107 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import {
|
|
3
|
-
import { Button } from "../ui/button";
|
|
2
|
+
import { Wrench, MapPin, User, CalendarClock } from "lucide-react";
|
|
4
3
|
import type { MaintenanceRequestSearchNode } from "../../api/maintenanceRequests/maintenanceRequestSearchService";
|
|
5
4
|
|
|
6
5
|
interface MaintenanceRequestCardProps {
|
|
7
6
|
request: MaintenanceRequestSearchNode;
|
|
8
|
-
|
|
7
|
+
onClick?: (request: MaintenanceRequestSearchNode) => void;
|
|
9
8
|
}
|
|
10
9
|
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
Emergency: { color: "text-red-500", label: "EMERGENCY (2HR)", emoji: "🔴" },
|
|
16
|
-
High: { color: "text-orange-500", label: "HIGH (SAME DAY)", emoji: "🟠" },
|
|
17
|
-
Standard: { color: "text-blue-500", label: "STANDARD", emoji: "🔵" },
|
|
10
|
+
const priorityStyles: Record<string, string> = {
|
|
11
|
+
Emergency: "bg-red-100 text-red-700",
|
|
12
|
+
High: "bg-orange-100 text-orange-700",
|
|
13
|
+
Standard: "bg-blue-100 text-blue-700",
|
|
18
14
|
};
|
|
19
15
|
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
16
|
+
const statusStyles: Record<string, string> = {
|
|
17
|
+
New: "bg-purple-100 text-purple-700",
|
|
18
|
+
Assigned: "bg-blue-100 text-blue-700",
|
|
19
|
+
"In Progress": "bg-yellow-100 text-yellow-700",
|
|
20
|
+
"On Hold": "bg-gray-100 text-gray-700",
|
|
21
|
+
Resolved: "bg-green-100 text-green-700",
|
|
26
22
|
};
|
|
27
23
|
|
|
28
24
|
export const MaintenanceRequestCard: React.FC<MaintenanceRequestCardProps> = ({
|
|
29
25
|
request,
|
|
30
|
-
|
|
26
|
+
onClick,
|
|
31
27
|
}) => {
|
|
32
|
-
const
|
|
33
|
-
const priority =
|
|
34
|
-
priorityConfig[priorityValue as keyof typeof priorityConfig] || priorityConfig.Standard;
|
|
28
|
+
const name = request.Name?.value || "Request";
|
|
35
29
|
const issueType = request.Type__c?.value || "General";
|
|
36
|
-
const
|
|
37
|
-
const propertyAddress = request.Property__r?.Address__c?.value || "Unknown Address";
|
|
30
|
+
const priority = request.Priority__c?.value || "Standard";
|
|
38
31
|
const status = request.Status__c?.value || "New";
|
|
39
|
-
const description = request.Description__c?.value || "";
|
|
40
|
-
const
|
|
41
|
-
|
|
32
|
+
const description = request.Description__c?.value || "No description provided.";
|
|
33
|
+
const propertyAddress = request.Property__r?.Address__c?.value || "Unknown address";
|
|
34
|
+
const assignedWorker = request.Assigned_Worker__r?.Name?.value;
|
|
35
|
+
const scheduled = request.Scheduled__c?.value
|
|
36
|
+
? new Date(request.Scheduled__c.value).toLocaleString(undefined, {
|
|
37
|
+
month: "short",
|
|
38
|
+
day: "numeric",
|
|
39
|
+
hour: "numeric",
|
|
40
|
+
minute: "2-digit",
|
|
41
|
+
})
|
|
42
42
|
: undefined;
|
|
43
|
-
|
|
43
|
+
|
|
44
|
+
const handleClick = () => onClick?.(request);
|
|
44
45
|
|
|
45
46
|
return (
|
|
46
|
-
<
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
47
|
+
<div
|
|
48
|
+
role="button"
|
|
49
|
+
tabIndex={0}
|
|
50
|
+
aria-label={name}
|
|
51
|
+
className="h-full bg-white rounded-lg shadow-md p-5 hover:shadow-lg transition-shadow cursor-pointer flex flex-col gap-3 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-purple focus-visible:ring-offset-2"
|
|
52
|
+
onClick={handleClick}
|
|
53
|
+
onKeyDown={(e) => {
|
|
54
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
55
|
+
e.preventDefault();
|
|
56
|
+
handleClick();
|
|
57
|
+
}
|
|
58
|
+
}}
|
|
59
|
+
>
|
|
60
|
+
<div className="flex items-start justify-between gap-2">
|
|
61
|
+
<div className="flex items-center gap-2 min-w-0">
|
|
62
|
+
<Wrench className="w-5 h-5 text-purple-600 shrink-0" />
|
|
63
|
+
<h3 className="text-base font-semibold text-gray-900 truncate">{name}</h3>
|
|
56
64
|
</div>
|
|
65
|
+
<span
|
|
66
|
+
className={`shrink-0 px-2.5 py-0.5 rounded-full text-xs font-medium ${
|
|
67
|
+
statusStyles[status] ?? "bg-gray-100 text-gray-700"
|
|
68
|
+
}`}
|
|
69
|
+
>
|
|
70
|
+
{status}
|
|
71
|
+
</span>
|
|
72
|
+
</div>
|
|
57
73
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
74
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
75
|
+
<span
|
|
76
|
+
className={`px-2.5 py-0.5 rounded-full text-xs font-medium ${
|
|
77
|
+
priorityStyles[priority] ?? priorityStyles.Standard
|
|
78
|
+
}`}
|
|
79
|
+
>
|
|
80
|
+
{priority}
|
|
81
|
+
</span>
|
|
82
|
+
<span className="px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-700">
|
|
83
|
+
{issueType}
|
|
84
|
+
</span>
|
|
85
|
+
</div>
|
|
64
86
|
|
|
65
|
-
|
|
66
|
-
<div className="text-sm text-gray-600">👷 Assigned to: {assignedWorker}</div>
|
|
67
|
-
)}
|
|
87
|
+
<p className="text-sm text-gray-700 line-clamp-2">{description}</p>
|
|
68
88
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
89
|
+
<div className="mt-auto space-y-1 text-sm text-gray-600">
|
|
90
|
+
<div className="flex items-center gap-2">
|
|
91
|
+
<MapPin className="w-4 h-4 text-gray-400 shrink-0" />
|
|
92
|
+
<span className="truncate">{propertyAddress}</span>
|
|
93
|
+
</div>
|
|
94
|
+
<div className="flex items-center gap-2">
|
|
95
|
+
<User className="w-4 h-4 text-gray-400 shrink-0" />
|
|
96
|
+
<span className="truncate">{assignedWorker || "Unassigned"}</span>
|
|
97
|
+
</div>
|
|
98
|
+
{scheduled && (
|
|
99
|
+
<div className="flex items-center gap-2">
|
|
100
|
+
<CalendarClock className="w-4 h-4 text-gray-400 shrink-0" />
|
|
101
|
+
<span className="truncate">{scheduled}</span>
|
|
75
102
|
</div>
|
|
76
103
|
)}
|
|
77
104
|
</div>
|
|
78
|
-
</
|
|
105
|
+
</div>
|
|
79
106
|
);
|
|
80
107
|
};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { HardHat, Phone, MapPin, Star, DollarSign } from "lucide-react";
|
|
3
|
+
import type { MaintenanceWorkerSearchNode } from "../../api/maintenanceWorkers/maintenanceWorkerSearchService";
|
|
4
|
+
|
|
5
|
+
interface WorkerCardProps {
|
|
6
|
+
worker: MaintenanceWorkerSearchNode;
|
|
7
|
+
onClick?: (worker: MaintenanceWorkerSearchNode) => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const WorkerCard: React.FC<WorkerCardProps> = ({ worker, onClick }) => {
|
|
11
|
+
const name = worker.Name?.value || "Worker";
|
|
12
|
+
const employmentType = worker.Employment_Type__c?.value || worker.Type__c?.value;
|
|
13
|
+
const phone = worker.Phone__c?.value;
|
|
14
|
+
const location = worker.Location__c?.value;
|
|
15
|
+
const rating = worker.Rating__c?.value;
|
|
16
|
+
const hourlyRate = worker.Hourly_Rate__c?.value;
|
|
17
|
+
const isActive = worker.IsActive__c?.value;
|
|
18
|
+
const active = typeof isActive === "boolean" ? isActive : undefined;
|
|
19
|
+
|
|
20
|
+
const handleClick = () => onClick?.(worker);
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<div
|
|
24
|
+
role="button"
|
|
25
|
+
tabIndex={0}
|
|
26
|
+
aria-label={name}
|
|
27
|
+
className="h-full bg-white rounded-lg shadow-md p-5 hover:shadow-lg transition-shadow cursor-pointer flex flex-col gap-3 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-purple focus-visible:ring-offset-2"
|
|
28
|
+
onClick={handleClick}
|
|
29
|
+
onKeyDown={(e) => {
|
|
30
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
31
|
+
e.preventDefault();
|
|
32
|
+
handleClick();
|
|
33
|
+
}
|
|
34
|
+
}}
|
|
35
|
+
>
|
|
36
|
+
<div className="flex items-start justify-between gap-2">
|
|
37
|
+
<div className="flex items-center gap-3 min-w-0">
|
|
38
|
+
<div className="w-10 h-10 rounded-full bg-purple-100 flex items-center justify-center shrink-0">
|
|
39
|
+
<HardHat className="w-5 h-5 text-purple-700" />
|
|
40
|
+
</div>
|
|
41
|
+
<div className="min-w-0">
|
|
42
|
+
<h3 className="text-base font-semibold text-gray-900 truncate">{name}</h3>
|
|
43
|
+
{employmentType && <p className="text-sm text-gray-500 truncate">{employmentType}</p>}
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
{active !== undefined && (
|
|
47
|
+
<span
|
|
48
|
+
className={`shrink-0 px-2.5 py-0.5 rounded-full text-xs font-medium ${
|
|
49
|
+
active ? "bg-green-100 text-green-700" : "bg-gray-100 text-gray-600"
|
|
50
|
+
}`}
|
|
51
|
+
>
|
|
52
|
+
{active ? "Active" : "Inactive"}
|
|
53
|
+
</span>
|
|
54
|
+
)}
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<div className="mt-auto space-y-1 text-sm text-gray-600">
|
|
58
|
+
{phone && (
|
|
59
|
+
<div className="flex items-center gap-2">
|
|
60
|
+
<Phone className="w-4 h-4 text-gray-400 shrink-0" />
|
|
61
|
+
<span className="truncate">{phone}</span>
|
|
62
|
+
</div>
|
|
63
|
+
)}
|
|
64
|
+
{location && (
|
|
65
|
+
<div className="flex items-center gap-2">
|
|
66
|
+
<MapPin className="w-4 h-4 text-gray-400 shrink-0" />
|
|
67
|
+
<span className="truncate">{location}</span>
|
|
68
|
+
</div>
|
|
69
|
+
)}
|
|
70
|
+
<div className="flex items-center gap-4">
|
|
71
|
+
{rating != null && (
|
|
72
|
+
<div className="flex items-center gap-1">
|
|
73
|
+
<Star className="w-4 h-4 text-yellow-500 shrink-0" />
|
|
74
|
+
<span>{rating}</span>
|
|
75
|
+
</div>
|
|
76
|
+
)}
|
|
77
|
+
{hourlyRate != null && (
|
|
78
|
+
<div className="flex items-center gap-1">
|
|
79
|
+
<DollarSign className="w-4 h-4 text-gray-400 shrink-0" />
|
|
80
|
+
<span>{hourlyRate}/hr</span>
|
|
81
|
+
</div>
|
|
82
|
+
)}
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
);
|
|
87
|
+
};
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UnifiedSearch — the single search surface for the whole app.
|
|
3
|
+
*
|
|
4
|
+
* A thin wrapper over feature-react-search's drop-in `<Search>` (see
|
|
5
|
+
* `lib/searchConfig.ts` for the source definitions and the `pagination.mode:
|
|
6
|
+
* "merged"` that selects the combined-grid layout). The feature owns all the
|
|
7
|
+
* search chrome — search bar, scope selector, single-source filter/sort panel,
|
|
8
|
+
* the results count, the merged card grid, and one global pager over the
|
|
9
|
+
* cumulative result set. This component supplies only what's app-specific:
|
|
10
|
+
*
|
|
11
|
+
* - **Cards** — one per object type, via `renderResult` keyed by source.
|
|
12
|
+
* - **Detail modals** — opened on card click; maintenance requests and
|
|
13
|
+
* applications also support an inline status update. Because the search
|
|
14
|
+
* hook exposes no refetch, saved statuses are reflected optimistically via
|
|
15
|
+
* `statusOverrides`.
|
|
16
|
+
*
|
|
17
|
+
* The per-card source badge in the mixed ("All") grid is supplied by the feature
|
|
18
|
+
* automatically (each source's `label`, auto-colored) — no app code needed.
|
|
19
|
+
*
|
|
20
|
+
* One implementation serves both the unified `/search` page (no `restrictTo`)
|
|
21
|
+
* and each single-object page (`restrictTo="properties"` etc.): passing
|
|
22
|
+
* `restrictTo` locks the scope, hides the selector, and collapses the grid to
|
|
23
|
+
* that one source.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { useCallback, useState } from "react";
|
|
27
|
+
import { Search, type SearchConfig } from "../../features/search";
|
|
28
|
+
import { toast } from "../ui/sonner";
|
|
29
|
+
import { searchConfig, SOURCE_KEYS, type SourceKey } from "../../lib/searchConfig";
|
|
30
|
+
import { PropertyCard } from "../properties/PropertyCard";
|
|
31
|
+
import { MaintenanceRequestCard } from "../maintenanceRequests/MaintenanceRequestCard";
|
|
32
|
+
import { WorkerCard } from "../maintenanceWorkers/WorkerCard";
|
|
33
|
+
import { ApplicationCard } from "../applications/ApplicationCard";
|
|
34
|
+
import { PropertyDetailsModal } from "../properties/PropertyDetailsModal";
|
|
35
|
+
import { MaintenanceDetailsModal } from "../maintenanceRequests/MaintenanceDetailsModal";
|
|
36
|
+
import { WorkerDetailsModal } from "../maintenanceWorkers/WorkerDetailsModal";
|
|
37
|
+
import { ApplicationDetailsModal } from "../applications/ApplicationDetailsModal";
|
|
38
|
+
import type { PropertySearchNode } from "../../api/properties/propertySearchService";
|
|
39
|
+
import type { MaintenanceRequestSearchNode } from "../../api/maintenanceRequests/maintenanceRequestSearchService";
|
|
40
|
+
import type { MaintenanceWorkerSearchNode } from "../../api/maintenanceWorkers/maintenanceWorkerSearchService";
|
|
41
|
+
import type { ApplicationSearchNode } from "../../api/applications/applicationSearchService";
|
|
42
|
+
import { updateMaintenanceStatus } from "../../api/maintenanceRequests/maintenanceRequests";
|
|
43
|
+
import { updateApplicationStatus } from "../../api/applications/applications";
|
|
44
|
+
|
|
45
|
+
const config: SearchConfig = searchConfig;
|
|
46
|
+
|
|
47
|
+
export interface UnifiedSearchProps {
|
|
48
|
+
title?: string;
|
|
49
|
+
subtitle?: string;
|
|
50
|
+
searchPlaceholder?: string;
|
|
51
|
+
/** Lock the search to a single source; hides the scope selector. */
|
|
52
|
+
restrictTo?: SourceKey;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Discriminated handle on the record whose detail modal is open. */
|
|
56
|
+
type Selected =
|
|
57
|
+
| { key: typeof SOURCE_KEYS.PROPERTIES; node: PropertySearchNode }
|
|
58
|
+
| { key: typeof SOURCE_KEYS.MAINTENANCE; node: MaintenanceRequestSearchNode }
|
|
59
|
+
| { key: typeof SOURCE_KEYS.WORKERS; node: MaintenanceWorkerSearchNode }
|
|
60
|
+
| { key: typeof SOURCE_KEYS.APPLICATIONS; node: ApplicationSearchNode };
|
|
61
|
+
|
|
62
|
+
type StatusNode = {
|
|
63
|
+
Id: string;
|
|
64
|
+
Status__c?: { value?: string | null; displayValue?: string | null } | null;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/** Returns a copy of `node` with its status replaced by an optimistic override, if any. */
|
|
68
|
+
function withStatusOverride<T extends StatusNode>(node: T, overrides: Record<string, string>): T {
|
|
69
|
+
const next = overrides[node.Id];
|
|
70
|
+
if (next == null) return node;
|
|
71
|
+
return { ...node, Status__c: { ...(node.Status__c ?? {}), value: next, displayValue: next } };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function UnifiedSearch({
|
|
75
|
+
title = "Search",
|
|
76
|
+
subtitle,
|
|
77
|
+
searchPlaceholder = "Search…",
|
|
78
|
+
restrictTo,
|
|
79
|
+
}: UnifiedSearchProps) {
|
|
80
|
+
const [selected, setSelected] = useState<Selected | null>(null);
|
|
81
|
+
const [statusOverrides, setStatusOverrides] = useState<Record<string, string>>({});
|
|
82
|
+
|
|
83
|
+
// -- Optimistic status save -------------------------------------------------
|
|
84
|
+
|
|
85
|
+
const applyOptimisticStatus = useCallback((key: Selected["key"], id: string, status: string) => {
|
|
86
|
+
setStatusOverrides((prev) => ({ ...prev, [id]: status }));
|
|
87
|
+
setSelected((prev) => {
|
|
88
|
+
if (!prev || prev.key !== key || prev.node.Id !== id) return prev;
|
|
89
|
+
// Only the status-bearing sources (maintenance, applications) reach
|
|
90
|
+
// here; cast through StatusNode so the union's non-status members
|
|
91
|
+
// don't trip the type checker.
|
|
92
|
+
const current = prev.node as StatusNode;
|
|
93
|
+
return {
|
|
94
|
+
...prev,
|
|
95
|
+
node: {
|
|
96
|
+
...prev.node,
|
|
97
|
+
Status__c: { ...(current.Status__c ?? {}), value: status, displayValue: status },
|
|
98
|
+
},
|
|
99
|
+
} as Selected;
|
|
100
|
+
});
|
|
101
|
+
}, []);
|
|
102
|
+
|
|
103
|
+
const handleMaintenanceSave = useCallback(
|
|
104
|
+
async (id: string, status: string) => {
|
|
105
|
+
const ok = await updateMaintenanceStatus(id, status);
|
|
106
|
+
if (!ok) {
|
|
107
|
+
toast.error("Update failed", {
|
|
108
|
+
description: "Could not update the maintenance request. Please try again.",
|
|
109
|
+
});
|
|
110
|
+
throw new Error("Failed to update maintenance status");
|
|
111
|
+
}
|
|
112
|
+
applyOptimisticStatus(SOURCE_KEYS.MAINTENANCE, id, status);
|
|
113
|
+
toast.success("Status updated", {
|
|
114
|
+
description: "Maintenance request status has been updated.",
|
|
115
|
+
});
|
|
116
|
+
},
|
|
117
|
+
[applyOptimisticStatus],
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
const handleApplicationSave = useCallback(
|
|
121
|
+
async (id: string, status: string) => {
|
|
122
|
+
const ok = await updateApplicationStatus(id, status);
|
|
123
|
+
if (!ok) {
|
|
124
|
+
toast.error("Update failed", {
|
|
125
|
+
description: "Could not update the application. Please try again.",
|
|
126
|
+
});
|
|
127
|
+
throw new Error("Failed to update application status");
|
|
128
|
+
}
|
|
129
|
+
applyOptimisticStatus(SOURCE_KEYS.APPLICATIONS, id, status);
|
|
130
|
+
toast.success("Status updated", {
|
|
131
|
+
description: "Application status has been updated.",
|
|
132
|
+
});
|
|
133
|
+
},
|
|
134
|
+
[applyOptimisticStatus],
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
// -- Card rendering ---------------------------------------------------------
|
|
138
|
+
|
|
139
|
+
const renderResult = {
|
|
140
|
+
[SOURCE_KEYS.PROPERTIES]: (node: unknown) => {
|
|
141
|
+
const property = node as PropertySearchNode;
|
|
142
|
+
return (
|
|
143
|
+
<PropertyCard
|
|
144
|
+
property={property}
|
|
145
|
+
onClick={() => setSelected({ key: SOURCE_KEYS.PROPERTIES, node: property })}
|
|
146
|
+
/>
|
|
147
|
+
);
|
|
148
|
+
},
|
|
149
|
+
[SOURCE_KEYS.MAINTENANCE]: (node: unknown) => {
|
|
150
|
+
const request = withStatusOverride(node as MaintenanceRequestSearchNode, statusOverrides);
|
|
151
|
+
return (
|
|
152
|
+
<MaintenanceRequestCard
|
|
153
|
+
request={request}
|
|
154
|
+
onClick={() => setSelected({ key: SOURCE_KEYS.MAINTENANCE, node: request })}
|
|
155
|
+
/>
|
|
156
|
+
);
|
|
157
|
+
},
|
|
158
|
+
[SOURCE_KEYS.WORKERS]: (node: unknown) => {
|
|
159
|
+
const worker = node as MaintenanceWorkerSearchNode;
|
|
160
|
+
return (
|
|
161
|
+
<WorkerCard
|
|
162
|
+
worker={worker}
|
|
163
|
+
onClick={() => setSelected({ key: SOURCE_KEYS.WORKERS, node: worker })}
|
|
164
|
+
/>
|
|
165
|
+
);
|
|
166
|
+
},
|
|
167
|
+
[SOURCE_KEYS.APPLICATIONS]: (node: unknown) => {
|
|
168
|
+
const application = withStatusOverride(node as ApplicationSearchNode, statusOverrides);
|
|
169
|
+
return (
|
|
170
|
+
<ApplicationCard
|
|
171
|
+
application={application}
|
|
172
|
+
onClick={() => setSelected({ key: SOURCE_KEYS.APPLICATIONS, node: application })}
|
|
173
|
+
/>
|
|
174
|
+
);
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
return (
|
|
179
|
+
<>
|
|
180
|
+
<Search
|
|
181
|
+
config={config}
|
|
182
|
+
title={title}
|
|
183
|
+
subtitle={subtitle}
|
|
184
|
+
searchPlaceholder={searchPlaceholder}
|
|
185
|
+
restrictTo={restrictTo ? { kind: "sobject", key: restrictTo } : undefined}
|
|
186
|
+
renderResult={renderResult}
|
|
187
|
+
/>
|
|
188
|
+
|
|
189
|
+
{selected?.key === SOURCE_KEYS.PROPERTIES && (
|
|
190
|
+
<PropertyDetailsModal property={selected.node} isOpen onClose={() => setSelected(null)} />
|
|
191
|
+
)}
|
|
192
|
+
{selected?.key === SOURCE_KEYS.MAINTENANCE && (
|
|
193
|
+
<MaintenanceDetailsModal
|
|
194
|
+
request={selected.node}
|
|
195
|
+
isOpen
|
|
196
|
+
onClose={() => setSelected(null)}
|
|
197
|
+
onSave={handleMaintenanceSave}
|
|
198
|
+
/>
|
|
199
|
+
)}
|
|
200
|
+
{selected?.key === SOURCE_KEYS.WORKERS && (
|
|
201
|
+
<WorkerDetailsModal worker={selected.node} isOpen onClose={() => setSelected(null)} />
|
|
202
|
+
)}
|
|
203
|
+
{selected?.key === SOURCE_KEYS.APPLICATIONS && (
|
|
204
|
+
<ApplicationDetailsModal
|
|
205
|
+
application={selected.node}
|
|
206
|
+
isOpen
|
|
207
|
+
onClose={() => setSelected(null)}
|
|
208
|
+
onSave={handleApplicationSave}
|
|
209
|
+
/>
|
|
210
|
+
)}
|
|
211
|
+
</>
|
|
212
|
+
);
|
|
213
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fetches distinct values for a picklist field via the uiapi aggregate
|
|
3
|
+
* `groupBy` query.
|
|
4
|
+
*
|
|
5
|
+
* Used by the auto-render pathway when a picklist filter does not declare
|
|
6
|
+
* inline `options`. Each call hits the GraphQL endpoint once and returns the
|
|
7
|
+
* sorted set of distinct values plus their display labels.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { createDataSDK } from "@salesforce/platform-sdk";
|
|
11
|
+
|
|
12
|
+
export interface PicklistOption {
|
|
13
|
+
value: string;
|
|
14
|
+
label: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const KEY_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
18
|
+
|
|
19
|
+
function assertValidIdentifier(name: string, kind: string): void {
|
|
20
|
+
if (!KEY_PATTERN.test(name)) {
|
|
21
|
+
throw new Error(
|
|
22
|
+
`Invalid ${kind} "${name}". Must match /^[A-Za-z_][A-Za-z0-9_]*$/ to be a valid GraphQL identifier.`,
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Builds and executes a `uiapi.aggregate.<objectName>(groupBy: { <field>: { group: true } })`
|
|
29
|
+
* query and extracts the distinct values.
|
|
30
|
+
*/
|
|
31
|
+
export async function fetchDistinctValues(
|
|
32
|
+
objectName: string,
|
|
33
|
+
fieldName: string,
|
|
34
|
+
): Promise<PicklistOption[]> {
|
|
35
|
+
assertValidIdentifier(objectName, "objectName");
|
|
36
|
+
assertValidIdentifier(fieldName, "fieldName");
|
|
37
|
+
|
|
38
|
+
const document = `query DistinctValues {
|
|
39
|
+
uiapi {
|
|
40
|
+
aggregate {
|
|
41
|
+
${objectName}(groupBy: { ${fieldName}: { group: true } }) {
|
|
42
|
+
edges {
|
|
43
|
+
node {
|
|
44
|
+
aggregate @optional {
|
|
45
|
+
${fieldName} @optional {
|
|
46
|
+
value
|
|
47
|
+
displayValue
|
|
48
|
+
label
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}`;
|
|
57
|
+
|
|
58
|
+
const data = await createDataSDK();
|
|
59
|
+
const response = await data.graphql!.query<unknown>({ query: document });
|
|
60
|
+
|
|
61
|
+
if (response.errors?.length) {
|
|
62
|
+
throw new Error(response.errors.map((e) => e.message).join("; "));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const root = response.data as Record<string, unknown> | undefined;
|
|
66
|
+
const aggregate = (root?.uiapi as Record<string, unknown> | undefined)?.aggregate as
|
|
67
|
+
| Record<string, unknown>
|
|
68
|
+
| undefined;
|
|
69
|
+
const objectAgg = aggregate?.[objectName] as
|
|
70
|
+
| { edges?: Array<{ node?: { aggregate?: Record<string, unknown> } }> }
|
|
71
|
+
| undefined;
|
|
72
|
+
const edges = objectAgg?.edges ?? [];
|
|
73
|
+
|
|
74
|
+
return edges
|
|
75
|
+
.map((edge) => {
|
|
76
|
+
const field = edge?.node?.aggregate?.[fieldName] as
|
|
77
|
+
| { value?: string | null; displayValue?: string | null; label?: string | null }
|
|
78
|
+
| undefined;
|
|
79
|
+
const value = field?.value;
|
|
80
|
+
if (value == null || value === "") return null;
|
|
81
|
+
return { value, label: field?.label ?? field?.displayValue ?? value };
|
|
82
|
+
})
|
|
83
|
+
.filter((opt): opt is PicklistOption => opt !== null);
|
|
84
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Executes a search request and parses results into a per-source map.
|
|
3
|
+
*
|
|
4
|
+
* Today there's only one adapter — SObjects via the platform-sdk uiapi GraphQL
|
|
5
|
+
* bridge. Future adapters (CMS, REST, etc.) can run alongside this one and
|
|
6
|
+
* have their results merged into the same `Record<sourceKey, SourceResult>`
|
|
7
|
+
* by the hook.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { createDataSDK } from "@salesforce/platform-sdk";
|
|
11
|
+
import { buildSearchQuery, type SourceRequest } from "../queryBuilder";
|
|
12
|
+
import type { SourceResult } from "../types";
|
|
13
|
+
|
|
14
|
+
interface RawSourceResult {
|
|
15
|
+
edges?: Array<{ node?: unknown }> | null;
|
|
16
|
+
pageInfo?: {
|
|
17
|
+
hasNextPage?: boolean | null;
|
|
18
|
+
hasPreviousPage?: boolean | null;
|
|
19
|
+
startCursor?: string | null;
|
|
20
|
+
endCursor?: string | null;
|
|
21
|
+
} | null;
|
|
22
|
+
totalCount?: number | null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Runs a single multi-aliased GraphQL request against uiapi.query.
|
|
27
|
+
*
|
|
28
|
+
* Returns a plain object keyed by source.key. A source whose alias is missing
|
|
29
|
+
* from the response (e.g. due to a partial GraphQL error) is omitted; the
|
|
30
|
+
* caller can detect this by `result[sourceKey] == null`.
|
|
31
|
+
*/
|
|
32
|
+
export async function runSearch(requests: SourceRequest[]): Promise<Record<string, SourceResult>> {
|
|
33
|
+
const { document, variables } = buildSearchQuery(requests);
|
|
34
|
+
|
|
35
|
+
const data = await createDataSDK();
|
|
36
|
+
const response = await data.graphql!.query<unknown, Record<string, unknown>>({
|
|
37
|
+
query: document,
|
|
38
|
+
variables,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
if (response.errors?.length) {
|
|
42
|
+
throw new Error(response.errors.map((e) => e.message).join("; "));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const root = response.data as Record<string, unknown> | undefined;
|
|
46
|
+
const queryRoot = (root?.uiapi as Record<string, unknown> | undefined)?.query as
|
|
47
|
+
| Record<string, RawSourceResult | null | undefined>
|
|
48
|
+
| undefined;
|
|
49
|
+
|
|
50
|
+
const out: Record<string, SourceResult> = {};
|
|
51
|
+
for (const request of requests) {
|
|
52
|
+
const raw = queryRoot?.[request.source.key];
|
|
53
|
+
if (raw == null) continue;
|
|
54
|
+
const nodes = (raw.edges ?? [])
|
|
55
|
+
.map((edge) => edge?.node)
|
|
56
|
+
.filter((node): node is unknown => node != null);
|
|
57
|
+
out[request.source.key] = {
|
|
58
|
+
nodes,
|
|
59
|
+
pageInfo: raw.pageInfo
|
|
60
|
+
? {
|
|
61
|
+
hasNextPage: raw.pageInfo.hasNextPage ?? false,
|
|
62
|
+
hasPreviousPage: raw.pageInfo.hasPreviousPage ?? false,
|
|
63
|
+
startCursor: raw.pageInfo.startCursor ?? null,
|
|
64
|
+
endCursor: raw.pageInfo.endCursor ?? null,
|
|
65
|
+
}
|
|
66
|
+
: null,
|
|
67
|
+
totalCount: raw.totalCount ?? null,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
return out;
|
|
71
|
+
}
|