@salesforce/webapp-template-app-react-sample-b2x-experimental 1.80.0 → 1.81.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.
Files changed (17) hide show
  1. package/dist/.a4drules/features/feature-react-agentforce-conversation-client-embedded-agent-rule.md +7 -21
  2. package/dist/.a4drules/skills/feature-react-agentforce-conversation-client-embedded-agent/SKILL.md +69 -29
  3. package/dist/.a4drules/skills/feature-react-agentforce-conversation-client-embedded-agent/docs/embed-examples.md +42 -32
  4. package/dist/.a4drules/skills/feature-react-agentforce-conversation-client-embedded-agent/docs/troubleshooting.md +51 -0
  5. package/dist/CHANGELOG.md +16 -0
  6. package/dist/force-app/main/default/classes/MaintenanceRequestTriggerHandler.cls +66 -0
  7. package/dist/force-app/main/default/classes/MaintenanceRequestTriggerHandler.cls-meta.xml +5 -0
  8. package/dist/force-app/main/default/classes/MaintenanceRequestTriggerHandler_Test.cls +308 -0
  9. package/dist/force-app/main/default/classes/MaintenanceRequestTriggerHandler_Test.cls-meta.xml +5 -0
  10. package/dist/force-app/main/default/objects/Application__c/fields/Status__c.field-meta.xml +5 -0
  11. package/dist/force-app/main/default/objects/Maintenance_Request__c/fields/Assigned_Worker__c.field-meta.xml +15 -0
  12. package/dist/force-app/main/default/permissionsets/Property_Management_Access.permissionset-meta.xml +5 -52
  13. package/dist/force-app/main/default/triggers/MaintenanceRequestTrigger.trigger +5 -0
  14. package/dist/force-app/main/default/triggers/MaintenanceRequestTrigger.trigger-meta.xml +5 -0
  15. package/dist/force-app/main/default/webapplications/appreactsampleb2x/package.json +3 -3
  16. package/dist/package.json +1 -1
  17. package/package.json +1 -1
@@ -6,27 +6,13 @@ paths:
6
6
 
7
7
  # Agentforce Conversation Client (standards)
8
8
 
9
- When adding or editing the embedded Agentforce chat client in this project, follow these conventions.
9
+ ## DO NOT build a chat UI from scratch
10
10
 
11
- ## Component and library
11
+ When the user asks for a chat UI, chat widget, chatbot, agent, or conversational interface — **always use the existing `AgentforceConversationClient` component** from `@salesforce/webapp-template-feature-react-agentforce-conversation-client-experimental`. Never generate a custom chat implementation, third-party chat library, WebSocket/REST chat, or direct calls to `embedAgentforceClient`.
12
12
 
13
- - Use the shared **AgentforceConversationClient** React component from `@salesforce/webapp-template-feature-react-agentforce-conversation-client-experimental`. Do not call `embedAgentforceClient` directly in application code.
14
- - Install with `npm install @salesforce/webapp-template-feature-react-agentforce-conversation-client-experimental`. The underlying SDK (`@salesforce/agentforce-conversation-client`) is included automatically as a dependency.
13
+ ## Hard constraints
15
14
 
16
- ## Authentication
17
-
18
- - The component resolves auth automatically: **localhost** fetches `frontdoorUrl` from `/__lo/frontdoor`; **production** uses `window.location.origin` as `salesforceOrigin`.
19
- - Do not hard-code `salesforceOrigin` or `frontdoorUrl` unless the consumer explicitly provides them as props.
20
-
21
- ## Rendering mode
22
-
23
- - Pass `agentforceClientConfig.renderingConfig.mode` to select **floating** (default) or **inline**. Do not apply custom positioning CSS to override the built-in layout.
24
- - For inline mode, set `width` and `height` in `renderingConfig`. Do not override iframe dimensions with external CSS.
25
-
26
- ## Agent selection
27
-
28
- - Use `agentforceClientConfig.agentId` to select a specific agent. Ask the user for the agent ID; if not provided, note that the org's default agent is used.
29
-
30
- ## Placement
31
-
32
- - Render `<AgentforceConversationClient />` inside the existing app layout (e.g. alongside `<Outlet />`). Do not replace the entire page shell with the chat client.
15
+ - **`agentId` is required.** The component will not work without it. Always ask the user for their agent ID before generating code. Do not proceed without one.
16
+ - **Use the React wrapper only.** Import `AgentforceConversationClient` from the package. Never call `embedAgentforceClient` directly.
17
+ - **One instance per window.** Render in the app layout alongside `<Outlet />`, not on individual pages. The component is a singleton.
18
+ - **No auth hard-coding.** The component resolves `salesforceOrigin` and `frontdoorUrl` automatically.
@@ -1,13 +1,45 @@
1
1
  ---
2
2
  name: feature-react-agentforce-conversation-client-embedded-agent
3
- description: Embed an Agentforce conversation client (chat UI) into a React web application. Use when the user wants to add an employee agent, a chat client, chatbot, chat widget, chat component, conversation client, or conversational interface to their React app. Also applies when the user asks to embed or integrate any Salesforce agent — including employee agent, travel agent, HR agent, service agent, or any custom-named agent — or mentions Agentforce, Agentforce widget, Agentforce chat, or agent chat.
3
+ description: Embed an Agentforce conversation client (chat UI) into a React web application. Use when the user wants to add an employee agent, a chat client, chatbot, chat widget, chat component, conversation client, or conversational interface to their React app. Also applies when the user asks to embed or integrate any Salesforce agent — including employee agent, travel agent, HR agent, or any custom-named agent — or mentions Agentforce, Agentforce widget, Agentforce chat, or agent chat. ALWAYS use this skill instead of building a chat UI from scratch. Do NOT generate custom chat components, use third-party chat libraries, or create WebSocket/REST chat implementations. Do NOT use for non-React contexts or Lightning Web Components without React.
4
4
  ---
5
5
 
6
6
  # Embedded Agentforce chat (workflow)
7
7
 
8
8
  When the user wants an embedded Agentforce chat client in a React app, follow this workflow.
9
9
 
10
- ## 1. Install the package
10
+ ## DO NOT build a chat UI from scratch
11
+
12
+ When the user asks for a chat UI, chat widget, chatbot, conversational interface, agent embed, or anything related to an embedded agent — **always use the `AgentforceConversationClient` component** from `@salesforce/webapp-template-feature-react-agentforce-conversation-client-experimental`.
13
+
14
+ **Never do any of the following:**
15
+
16
+ - Build a custom chat component from scratch (no custom message bubbles, input boxes, or chat layouts)
17
+ - Use third-party chat libraries (e.g. `react-chat-widget`, `stream-chat`, `chatscope`, or similar)
18
+ - Create WebSocket, polling, or REST-based chat implementations
19
+ - Generate custom HTML/CSS chat UIs
20
+ - Write a wrapper around `embedAgentforceClient` directly — always use the provided React component
21
+
22
+ If the user asks for chat functionality that goes beyond what `AgentforceConversationClient` supports (e.g. custom message rendering, message history, typing indicators), explain that the embedded Agentforce client handles all of this internally and cannot be customized beyond the supported `agentforceClientConfig` options (`renderingConfig`, `styleTokens`, `agentId`).
23
+
24
+ ## CRITICAL: Agent ID is required
25
+
26
+ The Agentforce Conversation Client **will not work** without an `agentId`. There is no default agent — the component renders nothing and silently fails if `agentId` is missing. **Always ask the user for their agent ID before writing any code.**
27
+
28
+ > **Before proceeding:** Ask the user for their Salesforce agent ID (18-character record ID starting with `0Xx`). If they do not have one, direct them to **Setup → Agents** in their Salesforce org to find or create one. Do not generate code without an `agentId`.
29
+
30
+ ## 1. Collect the agent ID
31
+
32
+ Ask the user:
33
+
34
+ - "What is your Salesforce agent ID? (You can find it in Setup → Agents → select an agent → copy the ID from the URL. It's an 18-character ID starting with `0Xx`.)"
35
+
36
+ If the user does not provide one:
37
+
38
+ - Explain that the conversation client **requires** an agent ID and will not function without it.
39
+ - Direct them to **Setup → Agents** in their org.
40
+ - Do **not** proceed to generate the embed code until an agent ID is provided.
41
+
42
+ ## 2. Install the package
11
43
 
12
44
  ```bash
13
45
  npm install @salesforce/webapp-template-feature-react-agentforce-conversation-client-experimental
@@ -15,16 +47,16 @@ npm install @salesforce/webapp-template-feature-react-agentforce-conversation-cl
15
47
 
16
48
  This single install also brings in `@salesforce/agentforce-conversation-client` (the underlying SDK) automatically.
17
49
 
18
- ## 2. Use the shared wrapper
50
+ ## 3. Use the shared wrapper
19
51
 
20
52
  Use the `AgentforceConversationClient` React component. It resolves auth automatically:
21
53
 
22
54
  - **Dev (localhost)**: fetches `frontdoorUrl` from `/__lo/frontdoor`
23
55
  - **Prod (hosted in org)**: uses `salesforceOrigin` from `window.location.origin`
24
56
 
25
- ## 3. Embed in the layout
57
+ ## 4. Embed in the layout
26
58
 
27
- Render `<AgentforceConversationClient />` in the app layout so the chat client loads globally. Keep it alongside the existing layout (do not replace the page shell).
59
+ Render `<AgentforceConversationClient />` in the app layout so the chat client loads globally. Keep it alongside the existing layout (do not replace the page shell). **Always pass `agentId`.**
28
60
 
29
61
  ```tsx
30
62
  import { Outlet } from "react-router";
@@ -34,37 +66,51 @@ export default function AppLayout() {
34
66
  return (
35
67
  <>
36
68
  <Outlet />
37
- <AgentforceConversationClient />
69
+ <AgentforceConversationClient
70
+ agentforceClientConfig={{
71
+ agentId: "0Xx000000000000AAA",
72
+ }}
73
+ />
38
74
  </>
39
75
  );
40
76
  }
41
77
  ```
42
78
 
43
- ## 4. Configure the agent
79
+ Replace `"0Xx000000000000AAA"` with the agent ID provided by the user.
44
80
 
45
- Pass options via the `agentforceClientConfig` prop:
81
+ ## 5. Configure rendering and theming (optional)
46
82
 
47
- | Option | Purpose |
48
- | ---------------------------------- | ------------------------------------------------- |
49
- | `renderingConfig.mode` | `"floating"` (default) or `"inline"` |
50
- | `renderingConfig.width` / `height` | Inline dimensions (number for px, string for CSS) |
51
- | `agentId` | Select a specific agent from the org |
52
- | `styleTokens` | Theme colors and style overrides |
83
+ Pass additional options via the `agentforceClientConfig` prop:
84
+
85
+ | Option | Purpose | Required |
86
+ | ---------------------------------- | ---------------------------------------------------------- | -------- |
87
+ | `agentId` | The agent to load **required, will not work without it** | **Yes** |
88
+ | `renderingConfig.mode` | `"floating"` (default) or `"inline"` | No |
89
+ | `renderingConfig.width` / `height` | Inline dimensions (number for px, string for CSS) | No |
90
+ | `styleTokens` | Theme colors and style overrides | No |
53
91
 
54
92
  See [embed-examples.md](docs/embed-examples.md) for complete examples of each mode.
55
93
 
56
- ## 5. Validate prerequisites
94
+ ## 6. Validate prerequisites
95
+
96
+ Before the conversation client will work, the user must verify all of the following in their Salesforce org:
57
97
 
58
- - The org must allow `localhost:<PORT>` in **Trusted Domains for Inline Frames** (session settings).
98
+ 1. **Agent is active:** The org must have the agent referenced by `agentId` in an **Active** state and deployed to the correct channel (**Setup → Agents**).
99
+ 2. **Trusted domains:** The org must allow `localhost:<PORT>` in **Trusted Domains for Inline Frames** (**Setup → Session Settings → Trusted Domains for Inline Frames**). Required for local development.
100
+ 3. **First-party cookies disabled:** **"Require first party use of Salesforce cookies"** must be **unchecked/disabled** in **Setup → My Domain**. If this setting is enabled, the embedded conversation client will fail to authenticate and will not load.
59
101
 
60
102
  ## Quick reference: rendering modes
61
103
 
62
- ### Floating (default)
104
+ ### Floating (default rendering mode)
63
105
 
64
- A persistent chat widget overlay pinned to the bottom-right corner. No extra config neededfloating is the default.
106
+ A persistent chat widget overlay pinned to the bottom-right corner. Floating is the default rendering mode but `agentId` is still required.
65
107
 
66
108
  ```tsx
67
- <AgentforceConversationClient />
109
+ <AgentforceConversationClient
110
+ agentforceClientConfig={{
111
+ agentId: "0Xx000000000000AAA",
112
+ }}
113
+ />
68
114
  ```
69
115
 
70
116
  ### Inline
@@ -74,6 +120,7 @@ The chat renders within the page layout at a specific size.
74
120
  ```tsx
75
121
  <AgentforceConversationClient
76
122
  agentforceClientConfig={{
123
+ agentId: "0Xx000000000000AAA",
77
124
  renderingConfig: { mode: "inline", width: 420, height: 600 },
78
125
  }}
79
126
  />
@@ -86,6 +133,7 @@ Use `styleTokens` to customize the chat appearance.
86
133
  ```tsx
87
134
  <AgentforceConversationClient
88
135
  agentforceClientConfig={{
136
+ agentId: "0Xx000000000000AAA",
89
137
  styleTokens: {
90
138
  headerBlockBackground: "#0176d3",
91
139
  headerBlockTextColor: "#ffffff",
@@ -95,14 +143,6 @@ Use `styleTokens` to customize the chat appearance.
95
143
  />
96
144
  ```
97
145
 
98
- ### Agent selection
99
-
100
- Pass `agentId` to load a specific agent (e.g. travel agent, HR agent).
146
+ ## Troubleshooting
101
147
 
102
- ```tsx
103
- <AgentforceConversationClient
104
- agentforceClientConfig={{
105
- agentId: "0Xx000000000000",
106
- }}
107
- />
108
- ```
148
+ If the chat widget does not appear, fails to authenticate, or behaves unexpectedly, see [troubleshooting.md](docs/troubleshooting.md).
@@ -2,16 +2,22 @@
2
2
 
3
3
  Detailed examples for configuring the Agentforce Conversation Client. All examples use the `AgentforceConversationClient` React component; the underlying `embedAgentforceClient` API accepts the same `agentforceClientConfig` shape.
4
4
 
5
+ > **Important:** Every example requires an `agentId`. The component will not render without one. There is no default agent. Replace `"0Xx000000000000AAA"` in every example with the user's actual agent ID.
6
+
5
7
  ---
6
8
 
7
- ## Floating mode (default)
9
+ ## Floating mode (default rendering mode)
8
10
 
9
- A floating chat widget appears in the bottom-right corner. It starts minimized and expands when the user clicks it. This is the default — no `renderingConfig` is required.
11
+ A floating chat widget appears in the bottom-right corner. It starts minimized and expands when the user clicks it. Floating is the default rendering mode — no `renderingConfig` is needed — but `agentId` is always required.
10
12
 
11
- ### Minimal (no config)
13
+ ### Minimal
12
14
 
13
15
  ```tsx
14
- <AgentforceConversationClient />
16
+ <AgentforceConversationClient
17
+ agentforceClientConfig={{
18
+ agentId: "0Xx000000000000AAA",
19
+ }}
20
+ />
15
21
  ```
16
22
 
17
23
  ### Explicit floating
@@ -19,18 +25,19 @@ A floating chat widget appears in the bottom-right corner. It starts minimized a
19
25
  ```tsx
20
26
  <AgentforceConversationClient
21
27
  agentforceClientConfig={{
28
+ agentId: "0Xx000000000000AAA",
22
29
  renderingConfig: { mode: "floating" },
23
30
  }}
24
31
  />
25
32
  ```
26
33
 
27
- ### Floating with a specific agent and theme
34
+ ### Floating with theming
28
35
 
29
36
  ```tsx
30
37
  <AgentforceConversationClient
31
38
  agentforceClientConfig={{
39
+ agentId: "0Xx000000000000AAA",
32
40
  renderingConfig: { mode: "floating" },
33
- agentId: "0Xx000000000000",
34
41
  styleTokens: {
35
42
  headerBlockBackground: "#032D60",
36
43
  headerBlockTextColor: "#ffffff",
@@ -50,6 +57,7 @@ The chat renders inside the parent container at a specific size. Use this when t
50
57
  ```tsx
51
58
  <AgentforceConversationClient
52
59
  agentforceClientConfig={{
60
+ agentId: "0Xx000000000000AAA",
53
61
  renderingConfig: { mode: "inline", width: 420, height: 600 },
54
62
  }}
55
63
  />
@@ -60,6 +68,7 @@ The chat renders inside the parent container at a specific size. Use this when t
60
68
  ```tsx
61
69
  <AgentforceConversationClient
62
70
  agentforceClientConfig={{
71
+ agentId: "0Xx000000000000AAA",
63
72
  renderingConfig: { mode: "inline", width: "100%", height: "80vh" },
64
73
  }}
65
74
  />
@@ -73,6 +82,7 @@ The chat renders inside the parent container at a specific size. Use this when t
73
82
  <aside style={{ width: 400 }}>
74
83
  <AgentforceConversationClient
75
84
  agentforceClientConfig={{
85
+ agentId: "0Xx000000000000AAA",
76
86
  renderingConfig: { mode: "inline", width: "100%", height: "100%" },
77
87
  }}
78
88
  />
@@ -91,6 +101,7 @@ Use `styleTokens` to customize colors. Tokens are passed directly to the Agentfo
91
101
  ```tsx
92
102
  <AgentforceConversationClient
93
103
  agentforceClientConfig={{
104
+ agentId: "0Xx000000000000AAA",
94
105
  styleTokens: {
95
106
  headerBlockBackground: "#0176d3",
96
107
  headerBlockTextColor: "#ffffff",
@@ -104,6 +115,7 @@ Use `styleTokens` to customize colors. Tokens are passed directly to the Agentfo
104
115
  ```tsx
105
116
  <AgentforceConversationClient
106
117
  agentforceClientConfig={{
118
+ agentId: "0Xx000000000000AAA",
107
119
  styleTokens: {
108
120
  headerBlockBackground: "#0176d3",
109
121
  headerBlockTextColor: "#ffffff",
@@ -118,6 +130,7 @@ Use `styleTokens` to customize colors. Tokens are passed directly to the Agentfo
118
130
  ```tsx
119
131
  <AgentforceConversationClient
120
132
  agentforceClientConfig={{
133
+ agentId: "0Xx000000000000AAA",
121
134
  styleTokens: {
122
135
  headerBlockBackground: "#1a1a2e",
123
136
  headerBlockTextColor: "#e0e0e0",
@@ -129,40 +142,37 @@ Use `styleTokens` to customize colors. Tokens are passed directly to the Agentfo
129
142
 
130
143
  ---
131
144
 
132
- ## Agent selection
133
-
134
- Use `agentId` to load a specific agent from the org. If omitted, the org's default employee agent is used.
135
-
136
- ### Specific agent by ID
137
-
138
- ```tsx
139
- <AgentforceConversationClient
140
- agentforceClientConfig={{
141
- agentId: "0Xx000000000000",
142
- }}
143
- />
144
- ```
145
+ ## Full layout example
145
146
 
146
- ### Agent with inline mode and theming
147
+ Shows the recommended pattern: agent ID passed directly, single render in the app layout.
147
148
 
148
149
  ```tsx
149
- <AgentforceConversationClient
150
- agentforceClientConfig={{
151
- agentId: "0Xx000000000000",
152
- renderingConfig: { mode: "inline", width: 400, height: 700 },
153
- styleTokens: {
154
- headerBlockBackground: "#0176d3",
155
- headerBlockTextColor: "#ffffff",
156
- },
157
- }}
158
- />
150
+ import { Outlet } from "react-router";
151
+ import { AgentforceConversationClient } from "@salesforce/webapp-template-feature-react-agentforce-conversation-client-experimental";
152
+
153
+ export default function AppLayout() {
154
+ return (
155
+ <>
156
+ <Outlet />
157
+ <AgentforceConversationClient
158
+ agentforceClientConfig={{
159
+ agentId: "0Xx000000000000AAA",
160
+ styleTokens: {
161
+ headerBlockBackground: "#0176d3",
162
+ headerBlockTextColor: "#ffffff",
163
+ },
164
+ }}
165
+ />
166
+ </>
167
+ );
168
+ }
159
169
  ```
160
170
 
161
171
  ---
162
172
 
163
173
  ## Using the low-level `embedAgentforceClient` API
164
174
 
165
- The React component wraps `embedAgentforceClient`. If you need the raw API (e.g. in a non-React context), the config shape is the same:
175
+ The React component wraps `embedAgentforceClient`. If you need the raw API (e.g. in a non-React context), the config shape is the same — `agentId` is still required:
166
176
 
167
177
  ```ts
168
178
  import { embedAgentforceClient } from "@salesforce/agentforce-conversation-client";
@@ -171,7 +181,7 @@ const { loApp, chatClientComponent } = embedAgentforceClient({
171
181
  container: "#agentforce-container",
172
182
  salesforceOrigin: "https://myorg.my.salesforce.com",
173
183
  agentforceClientConfig: {
174
- agentId: "0Xx000000000000",
184
+ agentId: "0Xx000000000000AAA",
175
185
  renderingConfig: { mode: "floating" },
176
186
  styleTokens: {
177
187
  headerBlockBackground: "#0176d3",
@@ -0,0 +1,51 @@
1
+ # Troubleshooting
2
+
3
+ Common issues when using the Agentforce Conversation Client.
4
+
5
+ ---
6
+
7
+ ### Chat widget does not appear
8
+
9
+ **Cause:** Missing or invalid `agentId`. The component will not render anything without a valid agent ID.
10
+
11
+ **Solution:**
12
+
13
+ 1. Verify `agentId` is passed in `agentforceClientConfig` — it is required
14
+ 2. Confirm the ID is correct (18-character Salesforce record ID, starts with `0Xx`)
15
+ 3. Check that the agent exists and is **Active** in **Setup → Agents**
16
+
17
+ ### Chat loads but shows "agent not available"
18
+
19
+ **Cause:** The agent exists but is not deployed or is inactive.
20
+
21
+ **Solution:**
22
+
23
+ 1. In **Setup → Agents**, ensure the agent status is **Active**
24
+ 2. Verify the agent is deployed to the correct channel
25
+
26
+ ### Authentication error on localhost
27
+
28
+ **Cause:** `localhost:<PORT>` is not in the org's trusted domains for inline frames.
29
+
30
+ **Solution:**
31
+
32
+ 1. Go to **Setup → Session Settings → Trusted Domains for Inline Frames**
33
+ 2. Add `localhost:<PORT>` (e.g. `localhost:3000`)
34
+ 3. Restart the dev server
35
+
36
+ ### Chat fails to authenticate / blank iframe
37
+
38
+ **Cause:** "Require first party use of Salesforce cookies" is enabled in the org's session settings. This blocks the embedded client from establishing a session.
39
+
40
+ **Solution:**
41
+
42
+ 1. Go to **Setup → Session Settings**
43
+ 2. Find **"Require first party use of Salesforce cookies"**
44
+ 3. **Uncheck / disable** this setting
45
+ 4. Save and reload the app
46
+
47
+ ### Multiple chat widgets appear
48
+
49
+ **Cause:** `AgentforceConversationClient` is rendered in multiple places.
50
+
51
+ **Solution:** Render it once in the app layout, not on individual pages. The component uses a singleton pattern — only one instance should exist per window.
package/dist/CHANGELOG.md CHANGED
@@ -3,6 +3,22 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # [1.81.0](https://github.com/salesforce-experience-platform-emu/webapps/compare/v1.80.1...v1.81.0) (2026-03-09)
7
+
8
+ **Note:** Version bump only for package @salesforce/webapp-template-base-sfdx-project-experimental
9
+
10
+
11
+
12
+
13
+
14
+ ## [1.80.1](https://github.com/salesforce-experience-platform-emu/webapps/compare/v1.80.0...v1.80.1) (2026-03-09)
15
+
16
+ **Note:** Version bump only for package @salesforce/webapp-template-base-sfdx-project-experimental
17
+
18
+
19
+
20
+
21
+
6
22
  # [1.80.0](https://github.com/salesforce-experience-platform-emu/webapps/compare/v1.79.2...v1.80.0) (2026-03-07)
7
23
 
8
24
  **Note:** Version bump only for package @salesforce/webapp-template-base-sfdx-project-experimental
@@ -0,0 +1,66 @@
1
+ public with sharing class MaintenanceRequestTriggerHandler {
2
+
3
+ /**
4
+ * Handles before insert logic for Maintenance Request records
5
+ * Automatically assigns workers based on request type and updates status to Assigned
6
+ */
7
+ public static void handleBeforeInsert(List<Maintenance_Request__c> newRequests) {
8
+ // Map to store request type to worker type mappings
9
+ Map<String, String> requestTypeToWorkerType = new Map<String, String>{
10
+ 'Plumbing' => 'Plumbing',
11
+ 'Electrical' => 'Electrical',
12
+ 'HVAC' => 'HVAC (Heating & Cooling)',
13
+ 'Appliance' => 'Appliance Repair',
14
+ 'Pest' => 'Pest Control'
15
+ };
16
+
17
+ // Collect unique worker types needed
18
+ Set<String> workerTypesNeeded = new Set<String>();
19
+ for (Maintenance_Request__c request : newRequests) {
20
+ if (request.Type__c != null && requestTypeToWorkerType.containsKey(request.Type__c)) {
21
+ workerTypesNeeded.add(requestTypeToWorkerType.get(request.Type__c));
22
+ }
23
+ }
24
+
25
+ // Query for available workers by type
26
+ Map<String, List<Maintenance_Worker__c>> workersByType = new Map<String, List<Maintenance_Worker__c>>();
27
+ if (!workerTypesNeeded.isEmpty()) {
28
+ for (Maintenance_Worker__c worker : [
29
+ SELECT Id, Name, Type__c, Rating__c
30
+ FROM Maintenance_Worker__c
31
+ WHERE Type__c IN :workerTypesNeeded
32
+ AND IsActive__c = true
33
+ ORDER BY Rating__c DESC NULLS LAST, Name ASC
34
+ ]) {
35
+ if (!workersByType.containsKey(worker.Type__c)) {
36
+ workersByType.put(worker.Type__c, new List<Maintenance_Worker__c>());
37
+ }
38
+ workersByType.get(worker.Type__c).add(worker);
39
+ }
40
+ }
41
+
42
+ // Assign workers to requests
43
+ for (Maintenance_Request__c request : newRequests) {
44
+ // Only process requests with 'New' status (or null, since 'New' is the default)
45
+ if ((request.Status__c == 'New' || request.Status__c == null) &&
46
+ request.Type__c != null && requestTypeToWorkerType.containsKey(request.Type__c)) {
47
+ String workerType = requestTypeToWorkerType.get(request.Type__c);
48
+
49
+ // Check if we have available workers for this type
50
+ if (workersByType.containsKey(workerType) && !workersByType.get(workerType).isEmpty()) {
51
+ // Assign the first available worker (highest rated)
52
+ Maintenance_Worker__c assignedWorker = workersByType.get(workerType).get(0);
53
+ request.Assigned_Worker__c = assignedWorker.Id;
54
+ request.Status__c = 'Assigned';
55
+
56
+ // Set scheduled date to 3 days from now
57
+ request.Scheduled__c = DateTime.now().addDays(3);
58
+
59
+ // Rotate worker to end of list for round-robin assignment
60
+ workersByType.get(workerType).remove(0);
61
+ workersByType.get(workerType).add(assignedWorker);
62
+ }
63
+ }
64
+ }
65
+ }
66
+ }
@@ -0,0 +1,5 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
3
+ <apiVersion>65.0</apiVersion>
4
+ <status>Active</status>
5
+ </ApexClass>
@@ -0,0 +1,308 @@
1
+ /**
2
+ * Test class for MaintenanceRequestTriggerHandler
3
+ * Validates automatic worker assignment, status updates to Assigned, and scheduled date setting
4
+ */
5
+ @isTest
6
+ private class MaintenanceRequestTriggerHandler_Test {
7
+
8
+ /**
9
+ * Setup test data for all test methods
10
+ */
11
+ @testSetup
12
+ static void setupTestData() {
13
+ // Create test maintenance workers with different specialties
14
+ List<Maintenance_Worker__c> workers = new List<Maintenance_Worker__c>{
15
+ new Maintenance_Worker__c(
16
+ Name = 'John Plumber',
17
+ Type__c = 'Plumbing',
18
+ IsActive__c = true,
19
+ Rating__c = 4.5,
20
+ Phone__c = '555-0001'
21
+ ),
22
+ new Maintenance_Worker__c(
23
+ Name = 'Jane Electrician',
24
+ Type__c = 'Electrical',
25
+ IsActive__c = true,
26
+ Rating__c = 4.8,
27
+ Phone__c = '555-0002'
28
+ ),
29
+ new Maintenance_Worker__c(
30
+ Name = 'Bob HVAC',
31
+ Type__c = 'HVAC (Heating & Cooling)',
32
+ IsActive__c = true,
33
+ Rating__c = 4.2,
34
+ Phone__c = '555-0003'
35
+ ),
36
+ new Maintenance_Worker__c(
37
+ Name = 'Alice Appliance',
38
+ Type__c = 'Appliance Repair',
39
+ IsActive__c = true,
40
+ Rating__c = 4.7,
41
+ Phone__c = '555-0004'
42
+ ),
43
+ new Maintenance_Worker__c(
44
+ Name = 'Charlie Pest',
45
+ Type__c = 'Pest Control',
46
+ IsActive__c = true,
47
+ Rating__c = 4.3,
48
+ Phone__c = '555-0005'
49
+ ),
50
+ new Maintenance_Worker__c(
51
+ Name = 'Inactive Worker',
52
+ Type__c = 'Plumbing',
53
+ IsActive__c = false,
54
+ Rating__c = 5.0,
55
+ Phone__c = '555-0006'
56
+ )
57
+ };
58
+ insert workers;
59
+ }
60
+
61
+ /**
62
+ * Test that plumbing requests are assigned to plumbing workers
63
+ */
64
+ @isTest
65
+ static void testPlumbingRequestAssignment() {
66
+ // Query for plumbing worker
67
+ Maintenance_Worker__c plumber = [SELECT Id FROM Maintenance_Worker__c WHERE Type__c = 'Plumbing' AND IsActive__c = true LIMIT 1];
68
+
69
+ Test.startTest();
70
+ Maintenance_Request__c request = new Maintenance_Request__c(
71
+ Type__c = 'Plumbing',
72
+ Description__c = 'Leaky faucet in bathroom',
73
+ Priority__c = 'Standard'
74
+ );
75
+ insert request;
76
+ Test.stopTest();
77
+
78
+ // Verify assignment
79
+ Maintenance_Request__c insertedRequest = [
80
+ SELECT Id, Assigned_Worker__c, Status__c, Scheduled__c, Type__c
81
+ FROM Maintenance_Request__c
82
+ WHERE Id = :request.Id
83
+ ];
84
+
85
+ System.assertEquals(plumber.Id, insertedRequest.Assigned_Worker__c, 'Worker should be assigned');
86
+ System.assertEquals('Assigned', insertedRequest.Status__c, 'Status should be Assigned');
87
+ System.assertNotEquals(null, insertedRequest.Scheduled__c, 'Scheduled date should be set');
88
+
89
+ // Verify scheduled date is approximately 3 days from now (within 1 minute tolerance)
90
+ DateTime expectedDate = DateTime.now().addDays(3);
91
+ Long timeDiff = Math.abs(insertedRequest.Scheduled__c.getTime() - expectedDate.getTime());
92
+ System.assert(timeDiff < 60000, 'Scheduled date should be 3 days from now');
93
+ }
94
+
95
+ /**
96
+ * Test that electrical requests are assigned to electrical workers
97
+ */
98
+ @isTest
99
+ static void testElectricalRequestAssignment() {
100
+ Maintenance_Worker__c electrician = [SELECT Id FROM Maintenance_Worker__c WHERE Type__c = 'Electrical' AND IsActive__c = true LIMIT 1];
101
+
102
+ Test.startTest();
103
+ Maintenance_Request__c request = new Maintenance_Request__c(
104
+ Type__c = 'Electrical',
105
+ Description__c = 'Outlet not working',
106
+ Priority__c = 'High (Same Day)'
107
+ );
108
+ insert request;
109
+ Test.stopTest();
110
+
111
+ Maintenance_Request__c insertedRequest = [
112
+ SELECT Id, Assigned_Worker__c, Status__c
113
+ FROM Maintenance_Request__c
114
+ WHERE Id = :request.Id
115
+ ];
116
+
117
+ System.assertEquals(electrician.Id, insertedRequest.Assigned_Worker__c, 'Electrician should be assigned');
118
+ System.assertEquals('Assigned', insertedRequest.Status__c, 'Status should be Assigned');
119
+ }
120
+
121
+ /**
122
+ * Test that HVAC requests are assigned to HVAC workers
123
+ */
124
+ @isTest
125
+ static void testHVACRequestAssignment() {
126
+ Maintenance_Worker__c hvacWorker = [SELECT Id FROM Maintenance_Worker__c WHERE Type__c = 'HVAC (Heating & Cooling)' AND IsActive__c = true LIMIT 1];
127
+
128
+ Test.startTest();
129
+ Maintenance_Request__c request = new Maintenance_Request__c(
130
+ Type__c = 'HVAC',
131
+ Description__c = 'AC not cooling',
132
+ Priority__c = 'High (Same Day)'
133
+ );
134
+ insert request;
135
+ Test.stopTest();
136
+
137
+ Maintenance_Request__c insertedRequest = [
138
+ SELECT Id, Assigned_Worker__c, Status__c
139
+ FROM Maintenance_Request__c
140
+ WHERE Id = :request.Id
141
+ ];
142
+
143
+ System.assertEquals(hvacWorker.Id, insertedRequest.Assigned_Worker__c, 'HVAC worker should be assigned');
144
+ System.assertEquals('Assigned', insertedRequest.Status__c, 'Status should be Assigned');
145
+ }
146
+
147
+ /**
148
+ * Test that appliance requests are assigned to appliance repair workers
149
+ */
150
+ @isTest
151
+ static void testApplianceRequestAssignment() {
152
+ Maintenance_Worker__c applianceWorker = [SELECT Id FROM Maintenance_Worker__c WHERE Type__c = 'Appliance Repair' AND IsActive__c = true LIMIT 1];
153
+
154
+ Test.startTest();
155
+ Maintenance_Request__c request = new Maintenance_Request__c(
156
+ Type__c = 'Appliance',
157
+ Description__c = 'Dishwasher not draining',
158
+ Priority__c = 'Standard'
159
+ );
160
+ insert request;
161
+ Test.stopTest();
162
+
163
+ Maintenance_Request__c insertedRequest = [
164
+ SELECT Id, Assigned_Worker__c, Status__c
165
+ FROM Maintenance_Request__c
166
+ WHERE Id = :request.Id
167
+ ];
168
+
169
+ System.assertEquals(applianceWorker.Id, insertedRequest.Assigned_Worker__c, 'Appliance worker should be assigned');
170
+ System.assertEquals('Assigned', insertedRequest.Status__c, 'Status should be Assigned');
171
+ }
172
+
173
+ /**
174
+ * Test that pest requests are assigned to pest control workers
175
+ */
176
+ @isTest
177
+ static void testPestRequestAssignment() {
178
+ Maintenance_Worker__c pestWorker = [SELECT Id FROM Maintenance_Worker__c WHERE Type__c = 'Pest Control' AND IsActive__c = true LIMIT 1];
179
+
180
+ Test.startTest();
181
+ Maintenance_Request__c request = new Maintenance_Request__c(
182
+ Type__c = 'Pest',
183
+ Description__c = 'Ants in kitchen',
184
+ Priority__c = 'Standard'
185
+ );
186
+ insert request;
187
+ Test.stopTest();
188
+
189
+ Maintenance_Request__c insertedRequest = [
190
+ SELECT Id, Assigned_Worker__c, Status__c
191
+ FROM Maintenance_Request__c
192
+ WHERE Id = :request.Id
193
+ ];
194
+
195
+ System.assertEquals(pestWorker.Id, insertedRequest.Assigned_Worker__c, 'Pest control worker should be assigned');
196
+ System.assertEquals('Assigned', insertedRequest.Status__c, 'Status should be Assigned');
197
+ }
198
+
199
+ /**
200
+ * Test bulk insert with multiple request types
201
+ */
202
+ @isTest
203
+ static void testBulkRequestAssignment() {
204
+ Test.startTest();
205
+ List<Maintenance_Request__c> requests = new List<Maintenance_Request__c>{
206
+ new Maintenance_Request__c(Type__c = 'Plumbing', Description__c = 'Leak 1', Priority__c = 'High (Same Day)'),
207
+ new Maintenance_Request__c(Type__c = 'Electrical', Description__c = 'Issue 1', Priority__c = 'Standard'),
208
+ new Maintenance_Request__c(Type__c = 'Plumbing', Description__c = 'Leak 2', Priority__c = 'Standard'),
209
+ new Maintenance_Request__c(Type__c = 'HVAC', Description__c = 'AC Issue', Priority__c = 'High (Same Day)'),
210
+ new Maintenance_Request__c(Type__c = 'Appliance', Description__c = 'Fridge Issue', Priority__c = 'Standard')
211
+ };
212
+ insert requests;
213
+ Test.stopTest();
214
+
215
+ List<Maintenance_Request__c> insertedRequests = [
216
+ SELECT Id, Assigned_Worker__c, Status__c, Type__c
217
+ FROM Maintenance_Request__c
218
+ WHERE Id IN :requests
219
+ ];
220
+
221
+ // Verify all requests were assigned
222
+ for (Maintenance_Request__c req : insertedRequests) {
223
+ System.assertNotEquals(null, req.Assigned_Worker__c, 'All requests should have assigned workers');
224
+ System.assertEquals('Assigned', req.Status__c, 'All requests should be Assigned');
225
+ }
226
+
227
+ System.assertEquals(5, insertedRequests.size(), 'All 5 requests should be inserted');
228
+ }
229
+
230
+ /**
231
+ * Test that requests without type are not assigned
232
+ */
233
+ @isTest
234
+ static void testRequestWithoutType() {
235
+ Test.startTest();
236
+ Maintenance_Request__c request = new Maintenance_Request__c(
237
+ Description__c = 'General maintenance',
238
+ Priority__c = 'Standard'
239
+ );
240
+ insert request;
241
+ Test.stopTest();
242
+
243
+ Maintenance_Request__c insertedRequest = [
244
+ SELECT Id, Assigned_Worker__c, Status__c
245
+ FROM Maintenance_Request__c
246
+ WHERE Id = :request.Id
247
+ ];
248
+
249
+ System.assertEquals(null, insertedRequest.Assigned_Worker__c, 'Worker should not be assigned without type');
250
+ }
251
+
252
+ /**
253
+ * Test that inactive workers are not assigned
254
+ */
255
+ @isTest
256
+ static void testInactiveWorkerNotAssigned() {
257
+ // Delete all active plumbing workers
258
+ delete [SELECT Id FROM Maintenance_Worker__c WHERE Type__c = 'Plumbing' AND IsActive__c = true];
259
+
260
+ Test.startTest();
261
+ Maintenance_Request__c request = new Maintenance_Request__c(
262
+ Type__c = 'Plumbing',
263
+ Description__c = 'Emergency leak',
264
+ Priority__c = 'High (Same Day)'
265
+ );
266
+ insert request;
267
+ Test.stopTest();
268
+
269
+ Maintenance_Request__c insertedRequest = [
270
+ SELECT Id, Assigned_Worker__c, Status__c
271
+ FROM Maintenance_Request__c
272
+ WHERE Id = :request.Id
273
+ ];
274
+
275
+ System.assertEquals(null, insertedRequest.Assigned_Worker__c, 'Inactive worker should not be assigned');
276
+ }
277
+
278
+ /**
279
+ * Test that requests with non-New status are not automatically processed
280
+ */
281
+ @isTest
282
+ static void testNonNewStatusNotProcessed() {
283
+ // Query for plumbing worker
284
+ Maintenance_Worker__c plumber = [SELECT Id FROM Maintenance_Worker__c WHERE Type__c = 'Plumbing' AND IsActive__c = true LIMIT 1];
285
+
286
+ Test.startTest();
287
+ // Create a request with status explicitly set to 'In Progress'
288
+ Maintenance_Request__c request = new Maintenance_Request__c(
289
+ Type__c = 'Plumbing',
290
+ Description__c = 'Manually assigned request',
291
+ Priority__c = 'Standard',
292
+ Status__c = 'In Progress'
293
+ );
294
+ insert request;
295
+ Test.stopTest();
296
+
297
+ // Verify the request was not processed by trigger (no worker assigned)
298
+ Maintenance_Request__c insertedRequest = [
299
+ SELECT Id, Assigned_Worker__c, Status__c, Scheduled__c
300
+ FROM Maintenance_Request__c
301
+ WHERE Id = :request.Id
302
+ ];
303
+
304
+ System.assertEquals(null, insertedRequest.Assigned_Worker__c, 'Worker should not be auto-assigned for non-New status');
305
+ System.assertEquals('In Progress', insertedRequest.Status__c, 'Status should remain as originally set');
306
+ System.assertEquals(null, insertedRequest.Scheduled__c, 'Scheduled date should not be set by trigger');
307
+ }
308
+ }
@@ -0,0 +1,5 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
3
+ <apiVersion>65.0</apiVersion>
4
+ <status>Active</status>
5
+ </ApexClass>
@@ -27,6 +27,11 @@
27
27
  <default>false</default>
28
28
  <label>Background Check</label>
29
29
  </value>
30
+ <value>
31
+ <fullName>Under Review</fullName>
32
+ <default>false</default>
33
+ <label>Under Review</label>
34
+ </value>
30
35
  <value>
31
36
  <fullName>Approved</fullName>
32
37
  <default>false</default>
@@ -0,0 +1,15 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
3
+ <fullName>Assigned_Worker__c</fullName>
4
+ <deleteConstraint>SetNull</deleteConstraint>
5
+ <description>The maintenance worker assigned to handle this request</description>
6
+ <externalId>false</externalId>
7
+ <label>Assigned Worker</label>
8
+ <referenceTo>Maintenance_Worker__c</referenceTo>
9
+ <relationshipLabel>Maintenance Requests</relationshipLabel>
10
+ <relationshipName>Maintenance_Requests</relationshipName>
11
+ <required>false</required>
12
+ <trackHistory>true</trackHistory>
13
+ <trackTrending>false</trackTrending>
14
+ <type>Lookup</type>
15
+ </CustomField>
@@ -272,6 +272,11 @@
272
272
  <field>Maintenance_Request__c.Tenant_Home__c</field>
273
273
  <readable>true</readable>
274
274
  </fieldPermissions>
275
+ <fieldPermissions>
276
+ <editable>true</editable>
277
+ <field>Maintenance_Request__c.Assigned_Worker__c</field>
278
+ <readable>true</readable>
279
+ </fieldPermissions>
275
280
 
276
281
  <!-- Field Permissions: Maintenance_Worker__c -->
277
282
  <fieldPermissions>
@@ -363,12 +368,6 @@
363
368
  <readable>true</readable>
364
369
  </fieldPermissions>
365
370
 
366
- <!-- Tab Visibility -->
367
- <tabSettings>
368
- <tab>Property__c</tab>
369
- <visibility>Visible</visibility>
370
- </tabSettings>
371
-
372
371
  <fieldPermissions>
373
372
  <editable>true</editable>
374
373
  <field>Agent__c.License_Expiry__c</field>
@@ -633,50 +632,4 @@
633
632
  <field>Tenant__c.Status__c</field>
634
633
  <readable>true</readable>
635
634
  </fieldPermissions>
636
-
637
- <!-- Tab Settings -->
638
- <tabSettings>
639
- <tab>Application__c</tab>
640
- <visibility>Visible</visibility>
641
- </tabSettings>
642
- <tabSettings>
643
- <tab>KPI_Snapshot__c</tab>
644
- <visibility>Visible</visibility>
645
- </tabSettings>
646
- <tabSettings>
647
- <tab>Lease__c</tab>
648
- <visibility>Visible</visibility>
649
- </tabSettings>
650
- <tabSettings>
651
- <tab>Maintenance_Request__c</tab>
652
- <visibility>Visible</visibility>
653
- </tabSettings>
654
- <tabSettings>
655
- <tab>Maintenance_Worker__c</tab>
656
- <visibility>Visible</visibility>
657
- </tabSettings>
658
- <tabSettings>
659
- <tab>Notification__c</tab>
660
- <visibility>Visible</visibility>
661
- </tabSettings>
662
- <tabSettings>
663
- <tab>Payment__c</tab>
664
- <visibility>Visible</visibility>
665
- </tabSettings>
666
- <tabSettings>
667
- <tab>Property__c</tab>
668
- <visibility>Visible</visibility>
669
- </tabSettings>
670
- <tabSettings>
671
- <tab>Property_Owner__c</tab>
672
- <visibility>Visible</visibility>
673
- </tabSettings>
674
- <tabSettings>
675
- <tab>Property_Sale__c</tab>
676
- <visibility>Visible</visibility>
677
- </tabSettings>
678
- <tabSettings>
679
- <tab>Tenant__c</tab>
680
- <visibility>Visible</visibility>
681
- </tabSettings>
682
635
  </PermissionSet>
@@ -0,0 +1,5 @@
1
+ trigger MaintenanceRequestTrigger on Maintenance_Request__c (before insert) {
2
+ if (Trigger.isBefore && Trigger.isInsert) {
3
+ MaintenanceRequestTriggerHandler.handleBeforeInsert(Trigger.new);
4
+ }
5
+ }
@@ -0,0 +1,5 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <ApexTrigger xmlns="http://soap.sforce.com/2006/04/metadata">
3
+ <apiVersion>65.0</apiVersion>
4
+ <status>Active</status>
5
+ </ApexTrigger>
@@ -15,8 +15,8 @@
15
15
  "graphql:schema": "node scripts/get-graphql-schema.mjs"
16
16
  },
17
17
  "dependencies": {
18
- "@salesforce/sdk-data": "^1.80.0",
19
- "@salesforce/webapp-experimental": "^1.80.0",
18
+ "@salesforce/sdk-data": "^1.81.0",
19
+ "@salesforce/webapp-experimental": "^1.81.0",
20
20
  "@tailwindcss/vite": "^4.1.17",
21
21
  "@tanstack/react-form": "^1.28.4",
22
22
  "@types/leaflet": "^1.9.21",
@@ -43,7 +43,7 @@
43
43
  "@graphql-eslint/eslint-plugin": "^4.1.0",
44
44
  "@graphql-tools/utils": "^11.0.0",
45
45
  "@playwright/test": "^1.49.0",
46
- "@salesforce/vite-plugin-webapp-experimental": "^1.80.0",
46
+ "@salesforce/vite-plugin-webapp-experimental": "^1.81.0",
47
47
  "@testing-library/jest-dom": "^6.6.3",
48
48
  "@testing-library/react": "^16.1.0",
49
49
  "@testing-library/user-event": "^14.5.2",
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforce/webapp-template-base-sfdx-project-experimental",
3
- "version": "1.80.0",
3
+ "version": "1.81.0",
4
4
  "description": "Base SFDX project template",
5
5
  "private": true,
6
6
  "files": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforce/webapp-template-app-react-sample-b2x-experimental",
3
- "version": "1.80.0",
3
+ "version": "1.81.0",
4
4
  "description": "B2C sample app template with app shell",
5
5
  "license": "SEE LICENSE IN LICENSE.txt",
6
6
  "author": "",