@salesforce/webapp-template-app-react-sample-b2e-experimental 1.64.0 → 1.66.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/Agent__c.json +79 -0
- package/dist/force-app/main/default/data/Application__c.json +10 -10
- package/dist/force-app/main/default/data/Contact.json +44 -0
- package/dist/force-app/main/default/data/Maintenance_Request__c.json +30 -30
- package/dist/force-app/main/default/data/Property__c.json +25 -25
- package/dist/force-app/main/default/data/data-plan.json +13 -1
- package/dist/force-app/main/default/objects/Agent__c/Agent__c.object-meta.xml +66 -0
- package/dist/force-app/main/default/objects/Agent__c/fields/Agent_Type__c.field-meta.xml +37 -0
- package/dist/force-app/main/default/objects/Agent__c/fields/Availability__c.field-meta.xml +37 -0
- package/dist/force-app/main/default/objects/Agent__c/fields/Emergency_Alt__c.field-meta.xml +11 -0
- package/dist/force-app/main/default/objects/Agent__c/fields/Language__c.field-meta.xml +42 -0
- package/dist/force-app/main/default/objects/Agent__c/fields/License_Expiry__c.field-meta.xml +11 -0
- package/dist/force-app/main/default/objects/Agent__c/fields/License_Number__c.field-meta.xml +14 -0
- package/dist/force-app/main/default/objects/Agent__c/fields/Office_Location__c.field-meta.xml +42 -0
- package/dist/force-app/main/default/objects/Agent__c/fields/Territory__c.field-meta.xml +42 -0
- package/dist/force-app/main/default/objects/Maintenance_Request__c/fields/User__c.field-meta.xml +1 -1
- package/dist/force-app/main/default/objects/Property__c/fields/Agent__c.field-meta.xml +1 -1
- package/dist/force-app/main/default/permissionsets/Property_Management_Access.permissionset-meta.xml +53 -25
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/package-lock.json +18307 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/package.json +9 -7
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/api/applications.ts +142 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/api/dashboard.ts +1 -2
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/api/maintenance.ts +63 -6
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/api/properties.ts +1 -2
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/appLayout.tsx +4 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/AgentforceConversationClient.tsx +127 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/ApplicationDetailsModal.tsx +150 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/ApplicationsTable.tsx +105 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/MaintenanceDetailsModal.tsx +191 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/PropertyDetailsModal.tsx +274 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/StatusBadge.tsx +17 -17
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/TopBar.tsx +37 -7
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/VerticalNav.tsx +1 -5
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/index.ts +6 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/types.ts +6 -2
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/pages/Applications.tsx +129 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/pages/Home.tsx +28 -13
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/pages/Maintenance.tsx +95 -62
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/pages/Properties.tsx +22 -2
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/pages/TestAccPage.tsx +19 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/routes.tsx +12 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/types/conversation.ts +21 -0
- package/dist/package.json +1 -1
- package/package.json +2 -4
- package/dist/force-app/main/default/applications/Property_Management.app-meta.xml +0 -26
- package/dist/force-app/main/default/tabs/Application__c.tab-meta.xml +0 -6
- package/dist/force-app/main/default/tabs/Maintenance_Request__c.tab-meta.xml +0 -7
- package/dist/force-app/main/default/tabs/Maintenance_Worker__c.tab-meta.xml +0 -6
- package/dist/force-app/main/default/tabs/Notification__c.tab-meta.xml +0 -6
- package/dist/force-app/main/default/tabs/Property__c.tab-meta.xml +0 -7
- package/dist/force-app/main/default/tabs/Tenant__c.tab-meta.xml +0 -7
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/api/utils.ts +0 -4
|
@@ -15,19 +15,21 @@
|
|
|
15
15
|
"graphql:schema": "node scripts/get-graphql-schema.mjs"
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@salesforce/
|
|
19
|
-
"@salesforce/
|
|
18
|
+
"@salesforce/agentforce-conversation-client": "^1.66.0",
|
|
19
|
+
"@salesforce/sdk-data": "^1.66.0",
|
|
20
|
+
"@salesforce/webapp-experimental": "^1.66.0",
|
|
20
21
|
"@tailwindcss/vite": "^4.1.17",
|
|
21
|
-
"react": "^19.2.0",
|
|
22
|
-
"react-dom": "^19.2.0",
|
|
23
|
-
"react-router": "^7.10.1",
|
|
24
|
-
"tailwindcss": "^4.1.17",
|
|
25
22
|
"class-variance-authority": "^0.7.1",
|
|
26
23
|
"clsx": "^2.1.1",
|
|
24
|
+
"date-fns": "^3.6.0",
|
|
27
25
|
"lucide-react": "^0.562.0",
|
|
28
26
|
"radix-ui": "^1.4.3",
|
|
27
|
+
"react": "^19.2.0",
|
|
28
|
+
"react-dom": "^19.2.0",
|
|
29
|
+
"react-router": "^7.10.1",
|
|
29
30
|
"shadcn": "^3.8.5",
|
|
30
31
|
"tailwind-merge": "^3.4.0",
|
|
32
|
+
"tailwindcss": "^4.1.17",
|
|
31
33
|
"tw-animate-css": "^1.4.0"
|
|
32
34
|
},
|
|
33
35
|
"devDependencies": {
|
|
@@ -38,7 +40,7 @@
|
|
|
38
40
|
"@graphql-eslint/eslint-plugin": "^4.1.0",
|
|
39
41
|
"@graphql-tools/utils": "^11.0.0",
|
|
40
42
|
"@playwright/test": "^1.49.0",
|
|
41
|
-
"@salesforce/vite-plugin-webapp-experimental": "^1.
|
|
43
|
+
"@salesforce/vite-plugin-webapp-experimental": "^1.66.0",
|
|
42
44
|
"@testing-library/jest-dom": "^6.6.3",
|
|
43
45
|
"@testing-library/react": "^16.1.0",
|
|
44
46
|
"@testing-library/user-event": "^14.5.2",
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { getDataSDK, gql } from "@salesforce/sdk-data";
|
|
2
|
+
import type { Application } from "../lib/types.js";
|
|
3
|
+
import type {
|
|
4
|
+
GetApplicationsQuery,
|
|
5
|
+
UpdateApplicationStatusMutationVariables,
|
|
6
|
+
UpdateApplicationStatusMutation,
|
|
7
|
+
} from "./graphql-operations-types.js";
|
|
8
|
+
|
|
9
|
+
// Query to get all applications
|
|
10
|
+
const GET_APPLICATIONS = gql`
|
|
11
|
+
query GetApplications {
|
|
12
|
+
uiapi {
|
|
13
|
+
query {
|
|
14
|
+
Application__c(orderBy: { Start_Date__c: { order: DESC } }) {
|
|
15
|
+
edges {
|
|
16
|
+
node {
|
|
17
|
+
Id
|
|
18
|
+
Name {
|
|
19
|
+
value
|
|
20
|
+
}
|
|
21
|
+
User__r {
|
|
22
|
+
FirstName {
|
|
23
|
+
value
|
|
24
|
+
}
|
|
25
|
+
LastName {
|
|
26
|
+
value
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
Property__r {
|
|
30
|
+
Name {
|
|
31
|
+
value
|
|
32
|
+
}
|
|
33
|
+
Address__c {
|
|
34
|
+
value
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
Start_Date__c {
|
|
38
|
+
value
|
|
39
|
+
}
|
|
40
|
+
Status__c {
|
|
41
|
+
value
|
|
42
|
+
}
|
|
43
|
+
Employment__c {
|
|
44
|
+
value
|
|
45
|
+
}
|
|
46
|
+
References__c {
|
|
47
|
+
value
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
`;
|
|
56
|
+
|
|
57
|
+
// Mutation to update application status
|
|
58
|
+
const UPDATE_APPLICATION_STATUS = gql`
|
|
59
|
+
mutation UpdateApplicationStatus($input: Application__cUpdateInput!) {
|
|
60
|
+
uiapi {
|
|
61
|
+
Application__cUpdate(input: $input) {
|
|
62
|
+
Record {
|
|
63
|
+
Id
|
|
64
|
+
Status__c {
|
|
65
|
+
value
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
success
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
`;
|
|
73
|
+
|
|
74
|
+
export async function getApplications(): Promise<Application[]> {
|
|
75
|
+
try {
|
|
76
|
+
const data = await getDataSDK();
|
|
77
|
+
const result = await data.graphql?.<GetApplicationsQuery>(GET_APPLICATIONS);
|
|
78
|
+
|
|
79
|
+
if (result?.errors?.length) {
|
|
80
|
+
const errorMessages = result.errors.map((e) => e.message).join("; ");
|
|
81
|
+
throw new Error(`GraphQL Error: ${errorMessages}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const edges = result?.data?.uiapi?.query?.Application__c?.edges || [];
|
|
85
|
+
|
|
86
|
+
return edges
|
|
87
|
+
.map((edge) => {
|
|
88
|
+
if (!edge || !edge.node) return null;
|
|
89
|
+
const node = edge.node;
|
|
90
|
+
const firstName = node.User__r?.FirstName?.value || "";
|
|
91
|
+
const lastName = node.User__r?.LastName?.value || "";
|
|
92
|
+
const applicantName = `${firstName} ${lastName}`.trim() || "Unknown";
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
id: node.Id,
|
|
96
|
+
applicantName,
|
|
97
|
+
propertyAddress: node.Property__r?.Address__c?.value || "Unknown Property",
|
|
98
|
+
propertyName: node.Property__r?.Name?.value || "",
|
|
99
|
+
submittedDate: node.Start_Date__c?.value || "",
|
|
100
|
+
startDate: node.Start_Date__c?.value || "",
|
|
101
|
+
status: node.Status__c?.value || "Unknown",
|
|
102
|
+
employment: node.Employment__c?.value || "",
|
|
103
|
+
references: node.References__c?.value || "",
|
|
104
|
+
};
|
|
105
|
+
})
|
|
106
|
+
.filter((app) => app !== null) as Application[];
|
|
107
|
+
} catch (error) {
|
|
108
|
+
console.error("Error fetching applications:", error);
|
|
109
|
+
return [];
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export async function updateApplicationStatus(
|
|
114
|
+
applicationId: string,
|
|
115
|
+
status: string,
|
|
116
|
+
): Promise<boolean> {
|
|
117
|
+
const variables: UpdateApplicationStatusMutationVariables = {
|
|
118
|
+
input: {
|
|
119
|
+
Id: applicationId,
|
|
120
|
+
Application__c: {
|
|
121
|
+
Status__c: status,
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
try {
|
|
126
|
+
const data = await getDataSDK();
|
|
127
|
+
const result = await data.graphql?.<
|
|
128
|
+
UpdateApplicationStatusMutation,
|
|
129
|
+
UpdateApplicationStatusMutationVariables
|
|
130
|
+
>(UPDATE_APPLICATION_STATUS, variables);
|
|
131
|
+
|
|
132
|
+
if (result?.errors?.length) {
|
|
133
|
+
const errorMessages = result.errors.map((e) => e.message).join("; ");
|
|
134
|
+
throw new Error(`GraphQL Error: ${errorMessages}`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return !!result?.data?.uiapi?.Application__cUpdate?.success;
|
|
138
|
+
} catch (error) {
|
|
139
|
+
console.error("Error updating application status:", error);
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { getDataSDK } from "@salesforce/sdk-data";
|
|
1
|
+
import { getDataSDK, gql } from "@salesforce/sdk-data";
|
|
2
2
|
import type { DashboardMetrics, Application } from "../lib/types.js";
|
|
3
|
-
import { gql } from "./utils.js";
|
|
4
3
|
import type {
|
|
5
4
|
GetDashboardMetricsQuery,
|
|
6
5
|
GetOpenApplicationsQuery,
|
package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/api/maintenance.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import { getDataSDK } from "@salesforce/sdk-data";
|
|
1
|
+
import { getDataSDK, gql } from "@salesforce/sdk-data";
|
|
2
2
|
import type { MaintenanceRequest } from "../lib/types.js";
|
|
3
|
-
import { gql } from "./utils.js";
|
|
4
3
|
import type {
|
|
5
4
|
GetMaintenanceRequestsQuery,
|
|
6
5
|
GetMaintenanceRequestsQueryVariables,
|
|
7
6
|
GetAllMaintenanceRequestsQuery,
|
|
8
7
|
GetAllMaintenanceRequestsQueryVariables,
|
|
8
|
+
UpdateMaintenanceStatusMutation,
|
|
9
|
+
UpdateMaintenanceStatusMutationVariables,
|
|
9
10
|
} from "./graphql-operations-types.js";
|
|
10
11
|
|
|
11
12
|
// Query to get recent maintenance requests
|
|
@@ -127,6 +128,23 @@ const GET_ALL_MAINTENANCE_REQUESTS = gql`
|
|
|
127
128
|
}
|
|
128
129
|
`;
|
|
129
130
|
|
|
131
|
+
// Mutation to update maintenance request status
|
|
132
|
+
const UPDATE_MAINTENANCE_STATUS = gql`
|
|
133
|
+
mutation UpdateMaintenanceStatus($input: Maintenance_Request__cUpdateInput!) {
|
|
134
|
+
uiapi {
|
|
135
|
+
Maintenance_Request__cUpdate(input: $input) {
|
|
136
|
+
Record {
|
|
137
|
+
Id
|
|
138
|
+
Status__c {
|
|
139
|
+
value
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
success
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
`;
|
|
147
|
+
|
|
130
148
|
// Fetch maintenance requests for dashboard
|
|
131
149
|
export async function getMaintenanceRequests(first: number = 5): Promise<MaintenanceRequest[]> {
|
|
132
150
|
const variables: GetMaintenanceRequestsQueryVariables = { first };
|
|
@@ -171,6 +189,16 @@ export async function getAllMaintenanceRequests(
|
|
|
171
189
|
return requests;
|
|
172
190
|
}
|
|
173
191
|
|
|
192
|
+
// Helper function to map priority values to badge format
|
|
193
|
+
function mapPriority(priority: string | undefined): "emergency" | "high" | "medium" | "low" {
|
|
194
|
+
if (!priority) return "medium";
|
|
195
|
+
const priorityLower = priority.toLowerCase();
|
|
196
|
+
if (priorityLower.includes("emergency")) return "emergency";
|
|
197
|
+
if (priorityLower.includes("high")) return "high";
|
|
198
|
+
if (priorityLower.includes("low")) return "low";
|
|
199
|
+
return "medium";
|
|
200
|
+
}
|
|
201
|
+
|
|
174
202
|
// Helper function to transform maintenance request data
|
|
175
203
|
function transformMaintenanceRequest(node: any): MaintenanceRequest {
|
|
176
204
|
const scheduledDate = node.Scheduled__c?.value
|
|
@@ -181,8 +209,8 @@ function transformMaintenanceRequest(node: any): MaintenanceRequest {
|
|
|
181
209
|
id: node.Id,
|
|
182
210
|
propertyAddress: node.Property__r?.Address__c?.value || "Unknown Address",
|
|
183
211
|
issueType: node.Type__c?.value || "General",
|
|
184
|
-
priority: node.Priority__c?.value
|
|
185
|
-
status: node.Status__c?.value
|
|
212
|
+
priority: mapPriority(node.Priority__c?.value),
|
|
213
|
+
status: node.Status__c?.value || "New",
|
|
186
214
|
assignedWorker: undefined,
|
|
187
215
|
scheduledDateTime: scheduledDate,
|
|
188
216
|
description: node.Description__c?.value || "",
|
|
@@ -220,8 +248,8 @@ function transformMaintenanceTaskFull(node: any): MaintenanceRequest {
|
|
|
220
248
|
id: node.Id,
|
|
221
249
|
propertyAddress: node.Property__r?.Address__c?.value || "Unknown Address",
|
|
222
250
|
issueType: node.Type__c?.value || "General",
|
|
223
|
-
priority: node.Priority__c?.value
|
|
224
|
-
status: node.Status__c?.value
|
|
251
|
+
priority: mapPriority(node.Priority__c?.value),
|
|
252
|
+
status: node.Status__c?.value || "New",
|
|
225
253
|
assignedWorker: assignedWorkerName,
|
|
226
254
|
scheduledDateTime: scheduledDate?.toLocaleString(),
|
|
227
255
|
description: node.Description__c?.value || "",
|
|
@@ -233,3 +261,32 @@ function transformMaintenanceTaskFull(node: any): MaintenanceRequest {
|
|
|
233
261
|
formattedDate,
|
|
234
262
|
};
|
|
235
263
|
}
|
|
264
|
+
|
|
265
|
+
// Update maintenance request status
|
|
266
|
+
export async function updateMaintenanceStatus(requestId: string, status: string): Promise<boolean> {
|
|
267
|
+
const variables: UpdateMaintenanceStatusMutationVariables = {
|
|
268
|
+
input: {
|
|
269
|
+
Id: requestId,
|
|
270
|
+
Maintenance_Request__c: {
|
|
271
|
+
Status__c: status,
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
};
|
|
275
|
+
try {
|
|
276
|
+
const data = await getDataSDK();
|
|
277
|
+
const result = await data.graphql?.<
|
|
278
|
+
UpdateMaintenanceStatusMutation,
|
|
279
|
+
UpdateMaintenanceStatusMutationVariables
|
|
280
|
+
>(UPDATE_MAINTENANCE_STATUS, variables);
|
|
281
|
+
|
|
282
|
+
if (result?.errors?.length) {
|
|
283
|
+
const errorMessages = result.errors.map((e) => e.message).join("; ");
|
|
284
|
+
throw new Error(`GraphQL Error: ${errorMessages}`);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return !!result?.data?.uiapi?.Maintenance_Request__cUpdate?.success;
|
|
288
|
+
} catch (error) {
|
|
289
|
+
console.error("Error updating maintenance status:", error);
|
|
290
|
+
return false;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Outlet } from "react-router";
|
|
2
2
|
import { TopBar } from "./components/TopBar.js";
|
|
3
3
|
import { VerticalNav } from "./components/VerticalNav.js";
|
|
4
|
+
import { AgentforceConversationClient } from "./components/AgentforceConversationClient";
|
|
4
5
|
|
|
5
6
|
export default function AppLayout() {
|
|
6
7
|
return (
|
|
@@ -18,6 +19,9 @@ export default function AppLayout() {
|
|
|
18
19
|
<Outlet />
|
|
19
20
|
</main>
|
|
20
21
|
</div>
|
|
22
|
+
|
|
23
|
+
{/* Agentforce Conversation Client */}
|
|
24
|
+
<AgentforceConversationClient />
|
|
21
25
|
</div>
|
|
22
26
|
);
|
|
23
27
|
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2026, Salesforce, Inc.
|
|
3
|
+
* All rights reserved.
|
|
4
|
+
* For full license text, see the LICENSE.txt file
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { embedAgentforceClient } from "@salesforce/agentforce-conversation-client";
|
|
8
|
+
import { useEffect } from "react";
|
|
9
|
+
import type {
|
|
10
|
+
ResolvedEmbedOptions,
|
|
11
|
+
AgentforceConversationClientProps,
|
|
12
|
+
} from "../types/conversation";
|
|
13
|
+
|
|
14
|
+
const GLOBAL_HOST_ID = "agentforce-conversation-client-global-host";
|
|
15
|
+
const SINGLETON_KEY = "__agentforceConversationClientSingleton";
|
|
16
|
+
|
|
17
|
+
interface AgentforceConversationClientSingleton {
|
|
18
|
+
initPromise?: Promise<void>;
|
|
19
|
+
initialized: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface WindowWithAgentforceSingleton extends Window {
|
|
23
|
+
[SINGLETON_KEY]?: AgentforceConversationClientSingleton;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function getSingleton(): AgentforceConversationClientSingleton {
|
|
27
|
+
const win = window as WindowWithAgentforceSingleton;
|
|
28
|
+
if (!win[SINGLETON_KEY]) {
|
|
29
|
+
win[SINGLETON_KEY] = {
|
|
30
|
+
initialized: false,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
return win[SINGLETON_KEY]!;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function getOrCreateGlobalHost(): HTMLDivElement {
|
|
37
|
+
let host = document.getElementById(GLOBAL_HOST_ID) as HTMLDivElement | null;
|
|
38
|
+
if (!host) {
|
|
39
|
+
host = document.createElement("div");
|
|
40
|
+
host.id = GLOBAL_HOST_ID;
|
|
41
|
+
document.body.appendChild(host);
|
|
42
|
+
}
|
|
43
|
+
return host;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function getDefaultEmbedOptions(): ResolvedEmbedOptions {
|
|
47
|
+
return { salesforceOrigin: window.location.origin };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* React wrapper that embeds the Agentforce Conversation Client (copilot/agent UI)
|
|
52
|
+
* using Lightning Out. Requires a valid Salesforce session for the given org.
|
|
53
|
+
* Config is passed through from the consumer to the embed client as-is.
|
|
54
|
+
*/
|
|
55
|
+
export function AgentforceConversationClient({
|
|
56
|
+
agentforceClientConfig,
|
|
57
|
+
salesforceOrigin,
|
|
58
|
+
frontdoorUrl,
|
|
59
|
+
}: AgentforceConversationClientProps) {
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
const singleton = getSingleton();
|
|
62
|
+
if (singleton.initialized || singleton.initPromise) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const initialize = (options: ResolvedEmbedOptions) => {
|
|
67
|
+
// If already initialized while this flow was in progress, no-op.
|
|
68
|
+
if (singleton.initialized) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const existingEmbed = document.querySelector('lightning-out-application[data-lo="acc"]');
|
|
72
|
+
if (existingEmbed) {
|
|
73
|
+
singleton.initialized = true;
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const host = getOrCreateGlobalHost();
|
|
77
|
+
|
|
78
|
+
embedAgentforceClient({
|
|
79
|
+
container: host,
|
|
80
|
+
salesforceOrigin: salesforceOrigin ?? options.salesforceOrigin,
|
|
81
|
+
frontdoorUrl: frontdoorUrl ?? options.frontdoorUrl,
|
|
82
|
+
agentforceClientConfig: agentforceClientConfig,
|
|
83
|
+
});
|
|
84
|
+
singleton.initialized = true;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const shouldFetchFrontdoor = window.location.hostname === "localhost";
|
|
88
|
+
|
|
89
|
+
if (shouldFetchFrontdoor) {
|
|
90
|
+
singleton.initPromise = fetch("/__lo/frontdoor")
|
|
91
|
+
.then(async (res) => {
|
|
92
|
+
if (!res.ok) {
|
|
93
|
+
console.error("frontdoor fetch failed");
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const { frontdoorUrl: resolvedFrontdoorUrl } = await res.json();
|
|
97
|
+
initialize({ frontdoorUrl: resolvedFrontdoorUrl });
|
|
98
|
+
})
|
|
99
|
+
.catch((err) => {
|
|
100
|
+
console.error("AgentforceConversationClient: failed to fetch frontdoor URL", err);
|
|
101
|
+
})
|
|
102
|
+
.finally(() => {
|
|
103
|
+
singleton.initPromise = undefined;
|
|
104
|
+
});
|
|
105
|
+
} else {
|
|
106
|
+
singleton.initPromise = Promise.resolve()
|
|
107
|
+
.then(() => {
|
|
108
|
+
initialize(getDefaultEmbedOptions());
|
|
109
|
+
})
|
|
110
|
+
.catch((err) => {
|
|
111
|
+
console.error("AgentforceConversationClient: failed to embed Agentforce client", err);
|
|
112
|
+
})
|
|
113
|
+
.finally(() => {
|
|
114
|
+
singleton.initPromise = undefined;
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return () => {
|
|
119
|
+
// Intentionally no cleanup:
|
|
120
|
+
// This component guarantees a single LO initialization per window.
|
|
121
|
+
};
|
|
122
|
+
}, [salesforceOrigin, frontdoorUrl, agentforceClientConfig]);
|
|
123
|
+
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export default AgentforceConversationClient;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import { X } from "lucide-react";
|
|
3
|
+
import { Button } from "@/components/ui/button";
|
|
4
|
+
import type { Application } from "../lib/types.js";
|
|
5
|
+
|
|
6
|
+
interface ApplicationDetailsModalProps {
|
|
7
|
+
application: Application;
|
|
8
|
+
isOpen: boolean;
|
|
9
|
+
onClose: () => void;
|
|
10
|
+
onSave: (applicationId: string, status: string) => Promise<void>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const ApplicationDetailsModal: React.FC<ApplicationDetailsModalProps> = ({
|
|
14
|
+
application,
|
|
15
|
+
isOpen,
|
|
16
|
+
onClose,
|
|
17
|
+
onSave,
|
|
18
|
+
}) => {
|
|
19
|
+
const [selectedStatus, setSelectedStatus] = useState(application.status);
|
|
20
|
+
const [isSaving, setIsSaving] = useState(false);
|
|
21
|
+
|
|
22
|
+
// Determine if status is editable
|
|
23
|
+
const isStatusEditable =
|
|
24
|
+
application.status.toLowerCase() !== "approved" &&
|
|
25
|
+
application.status.toLowerCase() !== "rejected";
|
|
26
|
+
|
|
27
|
+
// Common status options
|
|
28
|
+
const statusOptions = ["Submitted", "Background Check", "Under Review", "Approved", "Rejected"];
|
|
29
|
+
|
|
30
|
+
const handleSave = async () => {
|
|
31
|
+
if (!isStatusEditable) return;
|
|
32
|
+
|
|
33
|
+
setIsSaving(true);
|
|
34
|
+
try {
|
|
35
|
+
await onSave(application.id, selectedStatus);
|
|
36
|
+
onClose();
|
|
37
|
+
} catch (error) {
|
|
38
|
+
console.error("Error saving status:", error);
|
|
39
|
+
} finally {
|
|
40
|
+
setIsSaving(false);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
if (!isOpen) return null;
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
|
48
|
+
{/* Backdrop */}
|
|
49
|
+
<div className="fixed inset-0 bg-black bg-opacity-50" onClick={onClose} />
|
|
50
|
+
|
|
51
|
+
{/* Modal */}
|
|
52
|
+
<div className="relative bg-white rounded-lg shadow-xl w-full max-w-2xl mx-4 max-h-[90vh] overflow-y-auto">
|
|
53
|
+
{/* Header */}
|
|
54
|
+
<div className="flex items-center justify-between p-6 border-b border-gray-200">
|
|
55
|
+
<h2 className="text-xl font-semibold text-gray-900">Application Details</h2>
|
|
56
|
+
<button onClick={onClose} className="text-gray-400 hover:text-gray-600 transition-colors">
|
|
57
|
+
<X className="w-6 h-6" />
|
|
58
|
+
</button>
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
{/* Content */}
|
|
62
|
+
<div className="p-6 space-y-6">
|
|
63
|
+
{/* Applicant Info */}
|
|
64
|
+
<div>
|
|
65
|
+
<h3 className="text-sm font-semibold text-gray-500 uppercase tracking-wide mb-2">
|
|
66
|
+
Applicant
|
|
67
|
+
</h3>
|
|
68
|
+
<p className="text-lg font-medium text-gray-900">{application.applicantName}</p>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
{/* Property Info */}
|
|
72
|
+
<div>
|
|
73
|
+
<h3 className="text-sm font-semibold text-gray-500 uppercase tracking-wide mb-2">
|
|
74
|
+
Property
|
|
75
|
+
</h3>
|
|
76
|
+
<p className="text-base text-gray-900">
|
|
77
|
+
{application.propertyName && (
|
|
78
|
+
<span className="font-medium">{application.propertyName}</span>
|
|
79
|
+
)}
|
|
80
|
+
</p>
|
|
81
|
+
<p className="text-sm text-gray-600">{application.propertyAddress}</p>
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
{/* Employment Info */}
|
|
85
|
+
{application.employment && (
|
|
86
|
+
<div>
|
|
87
|
+
<h3 className="text-sm font-semibold text-gray-500 uppercase tracking-wide mb-2">
|
|
88
|
+
Employment
|
|
89
|
+
</h3>
|
|
90
|
+
<p className="text-sm text-gray-700 whitespace-pre-wrap">{application.employment}</p>
|
|
91
|
+
</div>
|
|
92
|
+
)}
|
|
93
|
+
|
|
94
|
+
{/* References */}
|
|
95
|
+
{application.references && (
|
|
96
|
+
<div>
|
|
97
|
+
<h3 className="text-sm font-semibold text-gray-500 uppercase tracking-wide mb-2">
|
|
98
|
+
References
|
|
99
|
+
</h3>
|
|
100
|
+
<p className="text-sm text-gray-700 whitespace-pre-wrap">{application.references}</p>
|
|
101
|
+
</div>
|
|
102
|
+
)}
|
|
103
|
+
|
|
104
|
+
{/* Status */}
|
|
105
|
+
<div>
|
|
106
|
+
<h3 className="text-sm font-semibold text-gray-500 uppercase tracking-wide mb-2">
|
|
107
|
+
Status
|
|
108
|
+
</h3>
|
|
109
|
+
{isStatusEditable ? (
|
|
110
|
+
<select
|
|
111
|
+
value={selectedStatus}
|
|
112
|
+
onChange={(e) => setSelectedStatus(e.target.value)}
|
|
113
|
+
className="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-purple-500 focus:border-transparent"
|
|
114
|
+
>
|
|
115
|
+
{statusOptions.map((status) => (
|
|
116
|
+
<option key={status} value={status}>
|
|
117
|
+
{status}
|
|
118
|
+
</option>
|
|
119
|
+
))}
|
|
120
|
+
</select>
|
|
121
|
+
) : (
|
|
122
|
+
<div className="flex items-center">
|
|
123
|
+
<span className="px-4 py-2 bg-gray-100 text-gray-700 rounded-md font-medium">
|
|
124
|
+
{application.status}
|
|
125
|
+
</span>
|
|
126
|
+
<span className="ml-3 text-sm text-gray-500">(Cannot be modified)</span>
|
|
127
|
+
</div>
|
|
128
|
+
)}
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
{/* Footer */}
|
|
133
|
+
<div className="flex items-center justify-end gap-3 p-6 border-t border-gray-200">
|
|
134
|
+
<Button variant="outline" onClick={onClose} disabled={isSaving}>
|
|
135
|
+
Close
|
|
136
|
+
</Button>
|
|
137
|
+
{isStatusEditable && (
|
|
138
|
+
<Button
|
|
139
|
+
onClick={handleSave}
|
|
140
|
+
disabled={isSaving || selectedStatus === application.status}
|
|
141
|
+
className="bg-purple-600 hover:bg-purple-700"
|
|
142
|
+
>
|
|
143
|
+
{isSaving ? "Saving..." : "Save Changes"}
|
|
144
|
+
</Button>
|
|
145
|
+
)}
|
|
146
|
+
</div>
|
|
147
|
+
</div>
|
|
148
|
+
</div>
|
|
149
|
+
);
|
|
150
|
+
};
|