@salesforce/webapp-template-app-react-sample-b2e-experimental 1.73.0 → 1.74.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/objects/Maintenance_Request__c/Maintenance_Request__c.object-meta.xml +11 -1
- package/dist/force-app/main/default/objects/Maintenance_Worker__c/Maintenance_Worker__c.object-meta.xml +6 -1
- package/dist/force-app/main/default/objects/Property__c/Property__c.object-meta.xml +6 -1
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/package.json +7 -5
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/api/maintenanceWorkers.ts +60 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/ApplicationsTable.tsx +59 -62
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/FiltersFromApi.tsx +200 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/ListPageFilters.tsx +97 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/MaintenanceTable.tsx +2 -1
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/ObjectSelect.tsx +39 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/VerticalNav.tsx +6 -4
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/dashboard/GlobalSearchBar.tsx +125 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/feedback/FilterErrorAlert.tsx +15 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/feedback/PageErrorState.tsx +19 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/feedback/PageLoadingState.tsx +18 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/filters/FilterFieldRange.tsx +40 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/filters/FilterFieldSelect.tsx +190 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/filters/FilterFieldText.tsx +32 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/filters/ListPageFilterRow.tsx +100 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/layout/PageContainer.tsx +9 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/layout/PageHeader.tsx +21 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/list/ListPageWithFilters.tsx +70 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/constants.ts +39 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/api/index.ts +19 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/api/objectDetailService.ts +125 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/api/objectInfoGraphQLService.ts +194 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/api/objectInfoService.ts +199 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/api/recordListGraphQLService.ts +364 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/DetailFields.tsx +55 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/DetailForm.tsx +146 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/DetailHeader.tsx +34 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/DetailLayoutSections.tsx +80 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/Section.tsx +108 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/SectionRow.tsx +20 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/UiApiDetailForm.tsx +140 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/formatted/FieldValueDisplay.tsx +73 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/formatted/FormattedAddress.tsx +29 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/formatted/FormattedEmail.tsx +17 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/formatted/FormattedPhone.tsx +24 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/formatted/FormattedText.tsx +11 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/formatted/FormattedUrl.tsx +29 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/formatted/index.ts +6 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/filters/FilterField.tsx +54 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/filters/FilterInput.tsx +55 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/filters/FilterSelect.tsx +72 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/filters/FiltersPanel.tsx +380 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/forms/filters-form.tsx +114 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/forms/submit-button.tsx +47 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/search/GlobalSearchInput.tsx +114 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/search/ResultCardFields.tsx +71 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/search/SearchHeader.tsx +31 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/search/SearchPagination.tsx +144 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/search/SearchResultCard.tsx +136 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/search/SearchResultsPanel.tsx +197 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/shared/LoadingFallback.tsx +61 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/filters/FilterInput.tsx +55 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/filters/FilterSelect.tsx +72 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/hooks/form.tsx +209 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/hooks/index.ts +22 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/hooks/useObjectInfoBatch.ts +65 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/hooks/useObjectSearchData.ts +395 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/hooks/useRecordDetailLayout.ts +156 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/hooks/useRecordListGraphQL.ts +135 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/pages/DetailPage.tsx +109 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/pages/GlobalSearch.tsx +229 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/types/filters/filters.ts +121 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/types/filters/picklist.ts +32 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/types/index.ts +5 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/types/objectInfo/objectInfo.ts +166 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/types/recordDetail/recordDetail.ts +61 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/types/search/searchResults.ts +229 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/apiUtils.ts +125 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/cacheUtils.ts +76 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/debounce.ts +89 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/fieldUtils.ts +354 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/fieldValueExtractor.ts +67 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/filterUtils.ts +32 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/formDataTransformUtils.ts +260 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/formUtils.ts +142 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/graphQLNodeFieldUtils.ts +186 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/graphQLObjectInfoAdapter.ts +319 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/graphQLRecordAdapter.ts +90 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/index.ts +59 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/layoutTransformUtils.ts +236 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/linkUtils.ts +14 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/paginationUtils.ts +49 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/recordUtils.ts +159 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/sanitizationUtils.ts +49 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/hooks/useAccumulatedListPages.ts +29 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/hooks/useListPage.ts +167 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/index.ts +8 -4
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/applicationAdapter.ts +33 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/applicationColumns.ts +28 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/constants.ts +24 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/fieldMappers.ts +71 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/filterUtils.ts +165 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/globalSearchConstants.ts +40 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/listFilters.ts +152 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/listPageConfig.ts +65 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/maintenanceAdapter.ts +110 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/maintenanceColumns.ts +24 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/maintenanceWorkerAdapter.ts +29 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/maintenanceWorkerColumns.ts +25 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/objectApiNames.ts +13 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/propertyAdapter.ts +68 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/propertyColumns.ts +17 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/routeConfig.ts +35 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/types.ts +10 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/pages/Applications.tsx +47 -62
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/pages/Home.tsx +130 -98
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/pages/Maintenance.tsx +74 -91
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/pages/MaintenanceWorkers.tsx +138 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/pages/Properties.tsx +166 -85
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/routes.tsx +41 -2
- package/dist/package.json +1 -1
- package/package.json +5 -1
package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/propertyAdapter.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adapter: maps Property__c GraphQL node (from feature-global-search) to app Property type.
|
|
3
|
+
*/
|
|
4
|
+
import type { Property } from "./types.js";
|
|
5
|
+
|
|
6
|
+
interface PropertyNode {
|
|
7
|
+
Id?: string;
|
|
8
|
+
Name?: { value?: string };
|
|
9
|
+
Address__c?: { value?: string };
|
|
10
|
+
Description__c?: { value?: string };
|
|
11
|
+
Type__c?: { value?: string };
|
|
12
|
+
Status__c?: { value?: string };
|
|
13
|
+
Monthly_Rent__c?: { value?: number };
|
|
14
|
+
Bedrooms__c?: { value?: number };
|
|
15
|
+
Bathrooms__c?: { value?: number };
|
|
16
|
+
Sq_Ft__c?: { value?: number };
|
|
17
|
+
Year_Built__c?: { value?: number };
|
|
18
|
+
Hero_Image__c?: { value?: string };
|
|
19
|
+
Deposit__c?: { value?: number };
|
|
20
|
+
Parking__c?: { value?: string };
|
|
21
|
+
Pet_Friendly__c?: { value?: boolean };
|
|
22
|
+
Available_Date__c?: { value?: string };
|
|
23
|
+
Lease_Term__c?: { value?: number };
|
|
24
|
+
Features__c?: { value?: string };
|
|
25
|
+
Utilities__c?: { value?: string };
|
|
26
|
+
Tour_URL__c?: { value?: string };
|
|
27
|
+
CreatedDate?: { value?: string };
|
|
28
|
+
[key: string]: unknown;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function nodeToProperty(node: Record<string, unknown> | undefined): Property {
|
|
32
|
+
const n = (node ?? {}) as PropertyNode;
|
|
33
|
+
const createdYear = n.CreatedDate?.value
|
|
34
|
+
? new Date(n.CreatedDate.value).getFullYear().toString()
|
|
35
|
+
: undefined;
|
|
36
|
+
const features = n.Features__c?.value ? n.Features__c.value.split(";") : undefined;
|
|
37
|
+
const utilities = n.Utilities__c?.value ? n.Utilities__c.value.split(";") : undefined;
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
id: n.Id ?? "",
|
|
41
|
+
name: n.Name?.value ?? "Unnamed Property",
|
|
42
|
+
address: n.Address__c?.value ?? "Address not available",
|
|
43
|
+
type: (n.Type__c?.value?.toLowerCase() as "apartment" | "house" | "commercial") ?? "apartment",
|
|
44
|
+
status:
|
|
45
|
+
(n.Status__c?.value?.toLowerCase() as "available" | "rented" | "maintenance") ?? "available",
|
|
46
|
+
monthlyRent: n.Monthly_Rent__c?.value ?? 0,
|
|
47
|
+
bedrooms: n.Bedrooms__c?.value,
|
|
48
|
+
bathrooms: n.Bathrooms__c?.value,
|
|
49
|
+
heroImage: n.Hero_Image__c?.value,
|
|
50
|
+
description: n.Description__c?.value,
|
|
51
|
+
sqFt: n.Sq_Ft__c?.value,
|
|
52
|
+
yearBuilt: n.Year_Built__c?.value,
|
|
53
|
+
deposit: n.Deposit__c?.value,
|
|
54
|
+
parking:
|
|
55
|
+
typeof n.Parking__c?.value === "number"
|
|
56
|
+
? n.Parking__c.value
|
|
57
|
+
: n.Parking__c?.value != null
|
|
58
|
+
? Number(n.Parking__c.value)
|
|
59
|
+
: undefined,
|
|
60
|
+
petFriendly: n.Pet_Friendly__c?.value,
|
|
61
|
+
availableDate: n.Available_Date__c?.value,
|
|
62
|
+
leaseTerm: n.Lease_Term__c?.value,
|
|
63
|
+
features,
|
|
64
|
+
utilities,
|
|
65
|
+
tourUrl: n.Tour_URL__c?.value,
|
|
66
|
+
createdDate: createdYear,
|
|
67
|
+
};
|
|
68
|
+
}
|
package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/propertyColumns.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Property list column config for useRecordListGraphQL.
|
|
3
|
+
*/
|
|
4
|
+
import type { Column } from "@salesforce/webapp-template-feature-react-global-search-experimental";
|
|
5
|
+
|
|
6
|
+
const PROPERTY_EXTRA_COLUMNS: Column[] = [
|
|
7
|
+
{ fieldApiName: "Description__c", label: "Description", searchable: true, sortable: false },
|
|
8
|
+
{ fieldApiName: "Hero_Image__c", label: "Hero Image", searchable: true, sortable: false },
|
|
9
|
+
{ fieldApiName: "Address__c", label: "Address", searchable: true, sortable: false },
|
|
10
|
+
{ fieldApiName: "CreatedDate", label: "Created Date", searchable: true, sortable: false },
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
export function getPropertyListColumns(columns: Column[]): Column[] {
|
|
14
|
+
const existing = new Set(columns.map((c) => c.fieldApiName));
|
|
15
|
+
const toAdd = PROPERTY_EXTRA_COLUMNS.filter((c) => !existing.has(c.fieldApiName));
|
|
16
|
+
return toAdd.length === 0 ? columns : [...columns, ...toAdd];
|
|
17
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Central route configuration for list pages and navigation.
|
|
3
|
+
* Use these paths for links and search redirects to avoid duplication.
|
|
4
|
+
*/
|
|
5
|
+
export const PATHS = {
|
|
6
|
+
HOME: "/",
|
|
7
|
+
PROPERTIES: "/properties",
|
|
8
|
+
MAINTENANCE_REQUESTS: "/maintenance/requests",
|
|
9
|
+
MAINTENANCE_WORKERS: "/maintenance/workers",
|
|
10
|
+
APPLICATIONS: "/applications",
|
|
11
|
+
} as const;
|
|
12
|
+
|
|
13
|
+
export interface ListPageRoute {
|
|
14
|
+
path: string;
|
|
15
|
+
label: string;
|
|
16
|
+
searchParamKey?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** List pages that appear in the Home search object dropdown and in nav */
|
|
20
|
+
export const LIST_PAGE_ROUTES: Record<string, ListPageRoute> = {
|
|
21
|
+
properties: { path: PATHS.PROPERTIES, label: "Properties", searchParamKey: "q" },
|
|
22
|
+
maintenance_requests: {
|
|
23
|
+
path: PATHS.MAINTENANCE_REQUESTS,
|
|
24
|
+
label: "Maintenance Requests",
|
|
25
|
+
searchParamKey: "q",
|
|
26
|
+
},
|
|
27
|
+
maintenance_workers: {
|
|
28
|
+
path: PATHS.MAINTENANCE_WORKERS,
|
|
29
|
+
label: "Maintenance Workers",
|
|
30
|
+
searchParamKey: "q",
|
|
31
|
+
},
|
|
32
|
+
applications: { path: PATHS.APPLICATIONS, label: "Applications", searchParamKey: "q" },
|
|
33
|
+
} as const;
|
|
34
|
+
|
|
35
|
+
export type ListPageKey = keyof typeof LIST_PAGE_ROUTES;
|
|
@@ -59,3 +59,13 @@ export interface Property {
|
|
|
59
59
|
tourUrl?: string;
|
|
60
60
|
createdDate?: string;
|
|
61
61
|
}
|
|
62
|
+
|
|
63
|
+
export interface MaintenanceWorker {
|
|
64
|
+
id: string;
|
|
65
|
+
name: string;
|
|
66
|
+
email?: string;
|
|
67
|
+
phone?: string;
|
|
68
|
+
organization?: string;
|
|
69
|
+
activeRequestsCount?: number;
|
|
70
|
+
status?: string;
|
|
71
|
+
}
|
package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/pages/Applications.tsx
CHANGED
|
@@ -1,45 +1,28 @@
|
|
|
1
1
|
import { useEffect, useState } from "react";
|
|
2
|
+
import { useListPage } from "../hooks/useListPage.js";
|
|
3
|
+
import { applicationsListConfig } from "../lib/listPageConfig.js";
|
|
4
|
+
import { ListPageWithFilters } from "../components/list/ListPageWithFilters.js";
|
|
5
|
+
import { PageHeader } from "../components/layout/PageHeader.js";
|
|
2
6
|
import { ApplicationsTable } from "../components/ApplicationsTable.js";
|
|
3
7
|
import { ApplicationDetailsModal } from "../components/ApplicationDetailsModal.js";
|
|
4
|
-
import {
|
|
8
|
+
import { updateApplicationStatus } from "../api/applications.js";
|
|
5
9
|
import type { Application } from "../lib/types.js";
|
|
6
10
|
|
|
7
11
|
export default function Applications() {
|
|
8
|
-
const
|
|
9
|
-
const [loading, setLoading] = useState(true);
|
|
12
|
+
const list = useListPage(applicationsListConfig);
|
|
10
13
|
const [selectedApplication, setSelectedApplication] = useState<Application | null>(null);
|
|
11
14
|
const [notification, setNotification] = useState<{
|
|
12
15
|
message: string;
|
|
13
16
|
type: "success" | "error";
|
|
14
17
|
} | null>(null);
|
|
15
18
|
|
|
16
|
-
useEffect(() => {
|
|
17
|
-
loadApplications();
|
|
18
|
-
}, []);
|
|
19
|
-
|
|
20
|
-
// Clear notification after 5 seconds
|
|
21
19
|
useEffect(() => {
|
|
22
20
|
if (notification) {
|
|
23
|
-
const timer = setTimeout(() =>
|
|
24
|
-
setNotification(null);
|
|
25
|
-
}, 5000);
|
|
21
|
+
const timer = setTimeout(() => setNotification(null), 5000);
|
|
26
22
|
return () => clearTimeout(timer);
|
|
27
23
|
}
|
|
28
24
|
}, [notification]);
|
|
29
25
|
|
|
30
|
-
const loadApplications = async () => {
|
|
31
|
-
try {
|
|
32
|
-
setLoading(true);
|
|
33
|
-
const data = await getApplications();
|
|
34
|
-
setApplications(data);
|
|
35
|
-
} catch (error) {
|
|
36
|
-
console.error("Error loading applications:", error);
|
|
37
|
-
showNotification("Failed to load applications", "error");
|
|
38
|
-
} finally {
|
|
39
|
-
setLoading(false);
|
|
40
|
-
}
|
|
41
|
-
};
|
|
42
|
-
|
|
43
26
|
const handleRowClick = (application: Application) => {
|
|
44
27
|
setSelectedApplication(application);
|
|
45
28
|
};
|
|
@@ -51,43 +34,22 @@ export default function Applications() {
|
|
|
51
34
|
const handleSaveStatus = async (applicationId: string, status: string) => {
|
|
52
35
|
try {
|
|
53
36
|
const success = await updateApplicationStatus(applicationId, status);
|
|
54
|
-
|
|
55
37
|
if (success) {
|
|
56
|
-
// Update the local state
|
|
57
|
-
setApplications((prev) =>
|
|
58
|
-
prev.map((app) => (app.id === applicationId ? { ...app, status } : app)),
|
|
59
|
-
);
|
|
60
|
-
|
|
61
|
-
// Update selected application if it's the one being edited
|
|
62
38
|
if (selectedApplication?.id === applicationId) {
|
|
63
39
|
setSelectedApplication({ ...selectedApplication, status });
|
|
64
40
|
}
|
|
65
|
-
|
|
66
|
-
showNotification("Application status updated successfully!", "success");
|
|
41
|
+
setNotification({ message: "Application status updated successfully!", type: "success" });
|
|
67
42
|
} else {
|
|
68
|
-
|
|
43
|
+
setNotification({ message: "Failed to update application status", type: "error" });
|
|
69
44
|
}
|
|
70
45
|
} catch (error) {
|
|
71
46
|
console.error("Error updating application:", error);
|
|
72
|
-
|
|
47
|
+
setNotification({ message: "An error occurred while updating the status", type: "error" });
|
|
73
48
|
}
|
|
74
49
|
};
|
|
75
50
|
|
|
76
|
-
const showNotification = (message: string, type: "success" | "error") => {
|
|
77
|
-
setNotification({ message, type });
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
if (loading) {
|
|
81
|
-
return (
|
|
82
|
-
<div className="flex items-center justify-center h-screen bg-gray-50">
|
|
83
|
-
<div className="text-lg text-gray-600">Loading applications...</div>
|
|
84
|
-
</div>
|
|
85
|
-
);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
51
|
return (
|
|
89
|
-
|
|
90
|
-
{/* Notification */}
|
|
52
|
+
<>
|
|
91
53
|
{notification && (
|
|
92
54
|
<div className="fixed top-4 right-4 z-50 animate-slide-in">
|
|
93
55
|
<div
|
|
@@ -103,19 +65,42 @@ export default function Applications() {
|
|
|
103
65
|
</div>
|
|
104
66
|
)}
|
|
105
67
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
68
|
+
<PageHeader title="Applications" description="Manage and review rental applications" />
|
|
69
|
+
|
|
70
|
+
<ListPageWithFilters
|
|
71
|
+
filterProps={{
|
|
72
|
+
filters: list.filters,
|
|
73
|
+
picklistValues: list.picklistValues,
|
|
74
|
+
formValues: list.formValues,
|
|
75
|
+
onFormValueChange: list.onFormValueChange,
|
|
76
|
+
onApply: list.onApplyFilters,
|
|
77
|
+
onReset: list.onResetFilters,
|
|
78
|
+
ariaLabel: applicationsListConfig.filtersAriaLabel,
|
|
79
|
+
}}
|
|
80
|
+
filterError={list.filterError}
|
|
81
|
+
loading={list.loading}
|
|
82
|
+
error={list.error}
|
|
83
|
+
loadingMessage={applicationsListConfig.loadingMessage}
|
|
84
|
+
isEmpty={list.items.length === 0}
|
|
85
|
+
searchPlaceholder="Search by applicant, property, status..."
|
|
86
|
+
searchAriaLabel="Search applications"
|
|
87
|
+
>
|
|
88
|
+
<ApplicationsTable applications={list.items} onRowClick={handleRowClick} />
|
|
89
|
+
|
|
90
|
+
{list.canLoadMore && (
|
|
91
|
+
<div className="flex justify-center mt-6">
|
|
92
|
+
<button
|
|
93
|
+
type="button"
|
|
94
|
+
onClick={list.onLoadMore}
|
|
95
|
+
disabled={list.loadMoreLoading}
|
|
96
|
+
className="px-6 py-2 bg-purple-700 hover:bg-purple-800 text-white rounded-lg text-sm font-medium disabled:opacity-50"
|
|
97
|
+
>
|
|
98
|
+
{list.loadMoreLoading ? "Loading..." : "Load More"}
|
|
99
|
+
</button>
|
|
100
|
+
</div>
|
|
101
|
+
)}
|
|
102
|
+
</ListPageWithFilters>
|
|
117
103
|
|
|
118
|
-
{/* Application Details Modal */}
|
|
119
104
|
{selectedApplication && (
|
|
120
105
|
<ApplicationDetailsModal
|
|
121
106
|
application={selectedApplication}
|
|
@@ -124,6 +109,6 @@ export default function Applications() {
|
|
|
124
109
|
onSave={handleSaveStatus}
|
|
125
110
|
/>
|
|
126
111
|
)}
|
|
127
|
-
|
|
112
|
+
</>
|
|
128
113
|
);
|
|
129
114
|
}
|
|
@@ -1,12 +1,55 @@
|
|
|
1
|
-
import { useEffect, useState } from "react";
|
|
2
|
-
import {
|
|
1
|
+
import { useEffect, useState, useCallback, useMemo } from "react";
|
|
2
|
+
import { useNavigate } from "react-router";
|
|
3
|
+
import {
|
|
4
|
+
useObjectInfoBatch,
|
|
5
|
+
useObjectListMetadata,
|
|
6
|
+
useRecordListGraphQL,
|
|
7
|
+
} from "@salesforce/webapp-template-feature-react-global-search-experimental";
|
|
3
8
|
import { IssuesDonutChart } from "../components/IssuesDonutChart.js";
|
|
4
9
|
import { MaintenanceTable } from "../components/MaintenanceTable.js";
|
|
10
|
+
import { GlobalSearchBar } from "../components/dashboard/GlobalSearchBar.js";
|
|
11
|
+
import { StatCard } from "../components/StatCard.js";
|
|
12
|
+
import { PageContainer } from "../components/layout/PageContainer.js";
|
|
13
|
+
import { PageLoadingState } from "../components/feedback/PageLoadingState.js";
|
|
5
14
|
import { getDashboardMetrics, calculateMetrics } from "../api/dashboard.js";
|
|
6
|
-
import { getMaintenanceRequests } from "../api/maintenance.js";
|
|
7
15
|
import type { DashboardMetrics, MaintenanceRequest } from "../lib/types.js";
|
|
16
|
+
import {
|
|
17
|
+
GLOBAL_SEARCH_OBJECT_API_NAME,
|
|
18
|
+
SEARCHABLE_OBJECTS,
|
|
19
|
+
MAINTENANCE_OBJECT_API_NAME,
|
|
20
|
+
type SearchableObjectConfig,
|
|
21
|
+
} from "../lib/globalSearchConstants.js";
|
|
22
|
+
import { getMaintenanceColumns } from "../lib/maintenanceColumns.js";
|
|
23
|
+
import { nodeToMaintenanceRequest } from "../lib/maintenanceAdapter.js";
|
|
24
|
+
import { DASHBOARD_MAINTENANCE_LIMIT } from "../lib/constants.js";
|
|
25
|
+
import { PATHS } from "../lib/routeConfig.js";
|
|
26
|
+
|
|
27
|
+
const CHART_ISSUE_TYPES = ["Plumbing", "HVAC", "Electrical"] as const;
|
|
28
|
+
const CHART_OTHER_LABEL = "Other";
|
|
29
|
+
const CHART_COLORS = ["#7C3AED", "#EC4899", "#14B8A6", "#06B6D4"] as const;
|
|
8
30
|
|
|
9
31
|
export default function Home() {
|
|
32
|
+
const navigate = useNavigate();
|
|
33
|
+
const objectApiNames = useMemo(() => SEARCHABLE_OBJECTS.map((o) => o.objectApiName), []);
|
|
34
|
+
const { objectInfos } = useObjectInfoBatch(objectApiNames);
|
|
35
|
+
|
|
36
|
+
const [searchQuery, setSearchQuery] = useState("");
|
|
37
|
+
const [selectedObjectApiName, setSelectedObjectApiName] = useState<
|
|
38
|
+
SearchableObjectConfig["objectApiName"]
|
|
39
|
+
>(GLOBAL_SEARCH_OBJECT_API_NAME);
|
|
40
|
+
|
|
41
|
+
const selectedConfig = useMemo(
|
|
42
|
+
() => SEARCHABLE_OBJECTS.find((o) => o.objectApiName === selectedObjectApiName),
|
|
43
|
+
[selectedObjectApiName],
|
|
44
|
+
);
|
|
45
|
+
const labelPlural = useMemo(() => {
|
|
46
|
+
const idx = objectApiNames.indexOf(selectedObjectApiName);
|
|
47
|
+
const info = idx >= 0 ? objectInfos[idx] : null;
|
|
48
|
+
return (
|
|
49
|
+
(info?.labelPlural as string | undefined) ?? selectedConfig?.fallbackLabelPlural ?? "Records"
|
|
50
|
+
);
|
|
51
|
+
}, [selectedObjectApiName, objectApiNames, objectInfos, selectedConfig?.fallbackLabelPlural]);
|
|
52
|
+
|
|
10
53
|
const [metrics, setMetrics] = useState<DashboardMetrics>({
|
|
11
54
|
totalProperties: 0,
|
|
12
55
|
unitsAvailable: 0,
|
|
@@ -14,67 +57,91 @@ export default function Home() {
|
|
|
14
57
|
topMaintenanceIssue: "",
|
|
15
58
|
topMaintenanceIssueCount: 0,
|
|
16
59
|
});
|
|
17
|
-
const [
|
|
18
|
-
|
|
60
|
+
const [metricsLoading, setMetricsLoading] = useState(true);
|
|
61
|
+
|
|
62
|
+
const listMeta = useObjectListMetadata(MAINTENANCE_OBJECT_API_NAME);
|
|
63
|
+
const columns = useMemo(() => getMaintenanceColumns(listMeta.columns), [listMeta.columns]);
|
|
64
|
+
const { edges, loading: maintenanceLoading } = useRecordListGraphQL({
|
|
65
|
+
objectApiName: MAINTENANCE_OBJECT_API_NAME,
|
|
66
|
+
columns,
|
|
67
|
+
columnsLoading: listMeta.loading,
|
|
68
|
+
columnsError: listMeta.error,
|
|
69
|
+
first: DASHBOARD_MAINTENANCE_LIMIT,
|
|
70
|
+
after: null,
|
|
71
|
+
searchQuery: undefined,
|
|
72
|
+
sortBy: "Priority__c DESC",
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const maintenanceRequests: MaintenanceRequest[] = useMemo(
|
|
76
|
+
() => edges.map((e) => nodeToMaintenanceRequest(e.node as Record<string, unknown>)),
|
|
77
|
+
[edges],
|
|
78
|
+
);
|
|
19
79
|
|
|
20
80
|
useEffect(() => {
|
|
21
|
-
|
|
81
|
+
let cancelled = false;
|
|
82
|
+
(async () => {
|
|
83
|
+
try {
|
|
84
|
+
setMetricsLoading(true);
|
|
85
|
+
const { properties } = await getDashboardMetrics();
|
|
86
|
+
if (!cancelled) setMetrics(calculateMetrics(properties));
|
|
87
|
+
} catch (error) {
|
|
88
|
+
if (!cancelled) console.error("Error loading dashboard metrics:", error);
|
|
89
|
+
} finally {
|
|
90
|
+
if (!cancelled) setMetricsLoading(false);
|
|
91
|
+
}
|
|
92
|
+
})();
|
|
93
|
+
return () => {
|
|
94
|
+
cancelled = true;
|
|
95
|
+
};
|
|
22
96
|
}, []);
|
|
23
97
|
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const maintenanceData = await getMaintenanceRequests(5);
|
|
34
|
-
setMaintenanceRequests(maintenanceData);
|
|
35
|
-
} catch (error) {
|
|
36
|
-
console.error("Error loading dashboard data:", error);
|
|
37
|
-
} finally {
|
|
38
|
-
setLoading(false);
|
|
98
|
+
const loading = metricsLoading || listMeta.loading || maintenanceLoading;
|
|
99
|
+
|
|
100
|
+
const handleSearchSubmit = useCallback(() => {
|
|
101
|
+
const trimmed = searchQuery.trim();
|
|
102
|
+
const path = selectedConfig?.path ?? SEARCHABLE_OBJECTS[0].path;
|
|
103
|
+
if (trimmed) {
|
|
104
|
+
navigate(`${path}?q=${encodeURIComponent(trimmed)}`);
|
|
105
|
+
} else {
|
|
106
|
+
navigate(path);
|
|
39
107
|
}
|
|
40
|
-
};
|
|
108
|
+
}, [searchQuery, navigate, selectedConfig?.path]);
|
|
109
|
+
|
|
110
|
+
const handleBrowseAll = useCallback(() => {
|
|
111
|
+
navigate(selectedConfig?.path ?? SEARCHABLE_OBJECTS[0].path);
|
|
112
|
+
}, [navigate, selectedConfig?.path]);
|
|
41
113
|
|
|
42
|
-
const handleViewMaintenance = (
|
|
43
|
-
|
|
44
|
-
};
|
|
114
|
+
const handleViewMaintenance = useCallback(() => {
|
|
115
|
+
navigate(PATHS.MAINTENANCE_REQUESTS);
|
|
116
|
+
}, [navigate]);
|
|
45
117
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const issueCounts: Record<string, number> = {
|
|
118
|
+
const chartData = useMemo(() => {
|
|
119
|
+
const counts: Record<string, number> = {
|
|
49
120
|
Plumbing: 0,
|
|
50
121
|
HVAC: 0,
|
|
51
122
|
Electrical: 0,
|
|
52
|
-
|
|
123
|
+
[CHART_OTHER_LABEL]: 0,
|
|
53
124
|
};
|
|
54
|
-
|
|
55
125
|
maintenanceRequests.forEach((request) => {
|
|
56
126
|
const type = request.issueType;
|
|
57
|
-
if (type
|
|
58
|
-
|
|
127
|
+
if (CHART_ISSUE_TYPES.includes(type as (typeof CHART_ISSUE_TYPES)[number])) {
|
|
128
|
+
counts[type]++;
|
|
59
129
|
} else {
|
|
60
|
-
|
|
130
|
+
counts[CHART_OTHER_LABEL]++;
|
|
61
131
|
}
|
|
62
132
|
});
|
|
63
|
-
|
|
64
133
|
return [
|
|
65
|
-
{ name: "Plumbing", value:
|
|
66
|
-
{ name: "HVAC", value:
|
|
67
|
-
{ name: "Electrical", value:
|
|
68
|
-
{ name:
|
|
134
|
+
{ name: "Plumbing", value: counts.Plumbing, color: CHART_COLORS[0] },
|
|
135
|
+
{ name: "HVAC", value: counts.HVAC, color: CHART_COLORS[1] },
|
|
136
|
+
{ name: "Electrical", value: counts.Electrical, color: CHART_COLORS[2] },
|
|
137
|
+
{ name: CHART_OTHER_LABEL, value: counts[CHART_OTHER_LABEL], color: CHART_COLORS[3] },
|
|
69
138
|
];
|
|
70
|
-
};
|
|
139
|
+
}, [maintenanceRequests]);
|
|
71
140
|
|
|
72
|
-
|
|
73
|
-
const calculateTrends = () => {
|
|
141
|
+
const trends = useMemo(() => {
|
|
74
142
|
const totalPropertiesTrend = Math.round(metrics.totalProperties * 0.1);
|
|
75
143
|
const unitsAvailableTrend = Math.round(metrics.unitsAvailable * 0.1);
|
|
76
144
|
const occupiedUnitsTrend = Math.round(metrics.occupiedUnits * 0.1);
|
|
77
|
-
|
|
78
145
|
return {
|
|
79
146
|
totalProperties: {
|
|
80
147
|
trend: totalPropertiesTrend,
|
|
@@ -89,92 +156,57 @@ export default function Home() {
|
|
|
89
156
|
previous: metrics.occupiedUnits - occupiedUnitsTrend,
|
|
90
157
|
},
|
|
91
158
|
};
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
const trends = calculateTrends();
|
|
159
|
+
}, [metrics]);
|
|
95
160
|
|
|
96
161
|
if (loading) {
|
|
97
|
-
return
|
|
98
|
-
<div className="flex items-center justify-center h-screen bg-gray-50">
|
|
99
|
-
<div className="text-lg text-gray-600">Loading dashboard...</div>
|
|
100
|
-
</div>
|
|
101
|
-
);
|
|
162
|
+
return <PageLoadingState message="Loading dashboard..." />;
|
|
102
163
|
}
|
|
103
164
|
|
|
104
165
|
return (
|
|
105
|
-
<
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
fill="none"
|
|
120
|
-
stroke="currentColor"
|
|
121
|
-
viewBox="0 0 24 24"
|
|
122
|
-
>
|
|
123
|
-
<path
|
|
124
|
-
strokeLinecap="round"
|
|
125
|
-
strokeLinejoin="round"
|
|
126
|
-
strokeWidth={2}
|
|
127
|
-
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
|
128
|
-
/>
|
|
129
|
-
</svg>
|
|
130
|
-
</div>
|
|
131
|
-
</div>
|
|
166
|
+
<PageContainer>
|
|
167
|
+
<div className="max-w-7xl mx-auto space-y-6">
|
|
168
|
+
<GlobalSearchBar
|
|
169
|
+
objectApiNames={objectApiNames}
|
|
170
|
+
objectInfos={objectInfos}
|
|
171
|
+
searchableObjects={SEARCHABLE_OBJECTS}
|
|
172
|
+
selectedObjectApiName={selectedObjectApiName}
|
|
173
|
+
onSelectedObjectChange={setSelectedObjectApiName}
|
|
174
|
+
searchQuery={searchQuery}
|
|
175
|
+
onSearchQueryChange={setSearchQuery}
|
|
176
|
+
onSearchSubmit={handleSearchSubmit}
|
|
177
|
+
onBrowseAll={handleBrowseAll}
|
|
178
|
+
labelPlural={labelPlural}
|
|
179
|
+
/>
|
|
132
180
|
|
|
133
|
-
{/* Main Layout: 70/30 split */}
|
|
134
181
|
<div className="grid grid-cols-1 lg:grid-cols-[70%_30%] gap-6">
|
|
135
|
-
{/* Left Column: Stat Cards + Maintenance Table */}
|
|
136
182
|
<div className="space-y-6">
|
|
137
|
-
{/* Stat Cards Row */}
|
|
138
183
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
139
184
|
<StatCard
|
|
140
185
|
title="Total Properties"
|
|
141
186
|
value={metrics.totalProperties}
|
|
142
|
-
trend={{
|
|
143
|
-
value: trends.totalProperties.trend,
|
|
144
|
-
isPositive: true,
|
|
145
|
-
}}
|
|
187
|
+
trend={{ value: trends.totalProperties.trend, isPositive: true }}
|
|
146
188
|
subtitle={`Last month total ${trends.totalProperties.previous}`}
|
|
147
189
|
/>
|
|
148
190
|
<StatCard
|
|
149
191
|
title="Units Available"
|
|
150
192
|
value={metrics.unitsAvailable}
|
|
151
|
-
trend={{
|
|
152
|
-
value: trends.unitsAvailable.trend,
|
|
153
|
-
isPositive: false,
|
|
154
|
-
}}
|
|
193
|
+
trend={{ value: trends.unitsAvailable.trend, isPositive: false }}
|
|
155
194
|
subtitle={`Last month total ${trends.unitsAvailable.previous}/${metrics.totalProperties}`}
|
|
156
195
|
/>
|
|
157
196
|
<StatCard
|
|
158
197
|
title="Occupied Units"
|
|
159
198
|
value={metrics.occupiedUnits}
|
|
160
|
-
trend={{
|
|
161
|
-
value: trends.occupiedUnits.trend,
|
|
162
|
-
isPositive: true,
|
|
163
|
-
}}
|
|
199
|
+
trend={{ value: trends.occupiedUnits.trend, isPositive: true }}
|
|
164
200
|
subtitle={`Last month total ${trends.occupiedUnits.previous}`}
|
|
165
201
|
/>
|
|
166
202
|
</div>
|
|
167
|
-
|
|
168
|
-
{/* Maintenance Requests Table */}
|
|
169
203
|
<MaintenanceTable requests={maintenanceRequests} onView={handleViewMaintenance} />
|
|
170
204
|
</div>
|
|
171
|
-
|
|
172
|
-
{/* Right Column: Donut Chart */}
|
|
173
205
|
<div>
|
|
174
|
-
<IssuesDonutChart data={
|
|
206
|
+
<IssuesDonutChart data={chartData} />
|
|
175
207
|
</div>
|
|
176
208
|
</div>
|
|
177
209
|
</div>
|
|
178
|
-
</
|
|
210
|
+
</PageContainer>
|
|
179
211
|
);
|
|
180
212
|
}
|