@salesforce/webapp-template-app-react-sample-b2e-experimental 1.80.1 → 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.
- package/dist/CHANGELOG.md +8 -0
- package/dist/force-app/main/default/classes/MaintenanceRequestTriggerHandler.cls +66 -0
- package/dist/force-app/main/default/classes/MaintenanceRequestTriggerHandler.cls-meta.xml +5 -0
- package/dist/force-app/main/default/classes/MaintenanceRequestTriggerHandler_Test.cls +308 -0
- package/dist/force-app/main/default/classes/MaintenanceRequestTriggerHandler_Test.cls-meta.xml +5 -0
- package/dist/force-app/main/default/objects/Application__c/fields/Status__c.field-meta.xml +5 -0
- package/dist/force-app/main/default/objects/Maintenance_Request__c/fields/Assigned_Worker__c.field-meta.xml +15 -0
- package/dist/force-app/main/default/permissionsets/Property_Management_Access.permissionset-meta.xml +5 -52
- package/dist/force-app/main/default/triggers/MaintenanceRequestTrigger.trigger +5 -0
- package/dist/force-app/main/default/triggers/MaintenanceRequestTrigger.trigger-meta.xml +5 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/package.json +4 -4
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/api/applications.ts +2 -2
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/api/graphql-operations-types.ts +161 -13
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/api/maintenance.ts +10 -8
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/assets/icons/maintenance-worker.svg +8 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/IssuesDonutChart.tsx +23 -3
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/MaintenanceDetailsModal.tsx +4 -4
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/VerticalNav.tsx +3 -2
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/maintenanceAdapter.ts +7 -3
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/maintenanceColumns.ts +12 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/pages/Home.tsx +6 -7
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/pages/MaintenanceWorkers.tsx +46 -5
- package/dist/package.json +1 -1
- package/package.json +3 -3
package/dist/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,14 @@
|
|
|
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
|
+
|
|
6
14
|
## [1.80.1](https://github.com/salesforce-experience-platform-emu/webapps/compare/v1.80.0...v1.80.1) (2026-03-09)
|
|
7
15
|
|
|
8
16
|
**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,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
|
+
}
|
|
@@ -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>
|
package/dist/force-app/main/default/permissionsets/Property_Management_Access.permissionset-meta.xml
CHANGED
|
@@ -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>
|
|
@@ -15,9 +15,9 @@
|
|
|
15
15
|
"graphql:schema": "node scripts/get-graphql-schema.mjs"
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@salesforce/agentforce-conversation-client": "^1.
|
|
19
|
-
"@salesforce/sdk-data": "^1.
|
|
20
|
-
"@salesforce/webapp-experimental": "^1.
|
|
18
|
+
"@salesforce/agentforce-conversation-client": "^1.81.0",
|
|
19
|
+
"@salesforce/sdk-data": "^1.81.0",
|
|
20
|
+
"@salesforce/webapp-experimental": "^1.81.0",
|
|
21
21
|
"@tailwindcss/vite": "^4.1.17",
|
|
22
22
|
"@tanstack/react-form": "^1.28.4",
|
|
23
23
|
"class-variance-authority": "^0.7.1",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"@graphql-eslint/eslint-plugin": "^4.1.0",
|
|
43
43
|
"@graphql-tools/utils": "^11.0.0",
|
|
44
44
|
"@playwright/test": "^1.49.0",
|
|
45
|
-
"@salesforce/vite-plugin-webapp-experimental": "^1.
|
|
45
|
+
"@salesforce/vite-plugin-webapp-experimental": "^1.81.0",
|
|
46
46
|
"@testing-library/jest-dom": "^6.6.3",
|
|
47
47
|
"@testing-library/react": "^16.1.0",
|
|
48
48
|
"@testing-library/user-event": "^14.5.2",
|
package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/api/applications.ts
CHANGED
|
@@ -87,8 +87,8 @@ export async function getApplications(): Promise<Application[]> {
|
|
|
87
87
|
.map((edge) => {
|
|
88
88
|
if (!edge || !edge.node) return null;
|
|
89
89
|
const node = edge.node;
|
|
90
|
-
const firstName = node.
|
|
91
|
-
const lastName = node.
|
|
90
|
+
const firstName = node.User__r?.FirstName?.value || "";
|
|
91
|
+
const lastName = node.User__r?.LastName?.value || "";
|
|
92
92
|
const applicantName = `${firstName} ${lastName}`.trim() || "Unknown";
|
|
93
93
|
|
|
94
94
|
return {
|
|
@@ -22,6 +22,7 @@ export type Scalars = {
|
|
|
22
22
|
DateTime: { input: string; output: string };
|
|
23
23
|
Double: { input: number | string; output: number };
|
|
24
24
|
Email: { input: string; output: string };
|
|
25
|
+
EncryptedString: { input: string; output: string };
|
|
25
26
|
/** Can be set to an ID or a Reference to the result of another mutation operation. */
|
|
26
27
|
IdOrRef: { input: string; output: string };
|
|
27
28
|
Latitude: { input: number | string; output: number };
|
|
@@ -45,8 +46,13 @@ export type Application__CUpdateInput = {
|
|
|
45
46
|
};
|
|
46
47
|
|
|
47
48
|
export type Application__CUpdateRepresentation = {
|
|
49
|
+
Employment__c?: InputMaybe<Scalars["LongTextArea"]["input"]>;
|
|
48
50
|
OwnerId?: InputMaybe<Scalars["IdOrRef"]["input"]>;
|
|
51
|
+
Property__c?: InputMaybe<Scalars["IdOrRef"]["input"]>;
|
|
52
|
+
References__c?: InputMaybe<Scalars["LongTextArea"]["input"]>;
|
|
53
|
+
Start_Date__c?: InputMaybe<Scalars["Date"]["input"]>;
|
|
49
54
|
Status__c?: InputMaybe<Scalars["Picklist"]["input"]>;
|
|
55
|
+
User__c?: InputMaybe<Scalars["IdOrRef"]["input"]>;
|
|
50
56
|
};
|
|
51
57
|
|
|
52
58
|
export enum DataType {
|
|
@@ -88,14 +94,52 @@ export enum FieldExtraTypeInfo {
|
|
|
88
94
|
SwitchablePersonname = "SWITCHABLE_PERSONNAME",
|
|
89
95
|
}
|
|
90
96
|
|
|
97
|
+
export enum LayoutComponentType {
|
|
98
|
+
Canvas = "CANVAS",
|
|
99
|
+
CustomLink = "CUSTOM_LINK",
|
|
100
|
+
EmptySpace = "EMPTY_SPACE",
|
|
101
|
+
Field = "FIELD",
|
|
102
|
+
ReportChart = "REPORT_CHART",
|
|
103
|
+
VisualforcePage = "VISUALFORCE_PAGE",
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export enum LayoutMode {
|
|
107
|
+
Create = "CREATE",
|
|
108
|
+
Edit = "EDIT",
|
|
109
|
+
View = "VIEW",
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export enum LayoutType {
|
|
113
|
+
Compact = "COMPACT",
|
|
114
|
+
Full = "FULL",
|
|
115
|
+
}
|
|
116
|
+
|
|
91
117
|
export type Maintenance_Request__CUpdateInput = {
|
|
92
118
|
Id: Scalars["IdOrRef"]["input"];
|
|
93
119
|
Maintenance_Request__c: Maintenance_Request__CUpdateRepresentation;
|
|
94
120
|
};
|
|
95
121
|
|
|
96
122
|
export type Maintenance_Request__CUpdateRepresentation = {
|
|
123
|
+
Actual_Cost__c?: InputMaybe<Scalars["Currency"]["input"]>;
|
|
124
|
+
Assigned_Worker__c?: InputMaybe<Scalars["IdOrRef"]["input"]>;
|
|
125
|
+
Completed__c?: InputMaybe<Scalars["DateTime"]["input"]>;
|
|
126
|
+
Description__c?: InputMaybe<Scalars["LongTextArea"]["input"]>;
|
|
127
|
+
Est_Cost__c?: InputMaybe<Scalars["Currency"]["input"]>;
|
|
97
128
|
OwnerId?: InputMaybe<Scalars["IdOrRef"]["input"]>;
|
|
129
|
+
Priority__c?: InputMaybe<Scalars["Picklist"]["input"]>;
|
|
130
|
+
Property__c?: InputMaybe<Scalars["IdOrRef"]["input"]>;
|
|
131
|
+
Scheduled__c?: InputMaybe<Scalars["DateTime"]["input"]>;
|
|
98
132
|
Status__c?: InputMaybe<Scalars["Picklist"]["input"]>;
|
|
133
|
+
Tenant_Home__c?: InputMaybe<Scalars["Boolean"]["input"]>;
|
|
134
|
+
Type__c?: InputMaybe<Scalars["Picklist"]["input"]>;
|
|
135
|
+
User__c?: InputMaybe<Scalars["IdOrRef"]["input"]>;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
/** Input for ObjectInfo and PickValues */
|
|
139
|
+
export type ObjectInfoInput = {
|
|
140
|
+
apiName: Scalars["String"]["input"];
|
|
141
|
+
fieldNames?: InputMaybe<Array<Scalars["String"]["input"]>>;
|
|
142
|
+
recordTypeIDs?: InputMaybe<Array<Scalars["ID"]["input"]>>;
|
|
99
143
|
};
|
|
100
144
|
|
|
101
145
|
export enum ResultOrder {
|
|
@@ -103,6 +147,17 @@ export enum ResultOrder {
|
|
|
103
147
|
Desc = "DESC",
|
|
104
148
|
}
|
|
105
149
|
|
|
150
|
+
export enum TabOrder {
|
|
151
|
+
LeftRight = "LEFT_RIGHT",
|
|
152
|
+
TopDown = "TOP_DOWN",
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export enum UiBehavior {
|
|
156
|
+
Edit = "EDIT",
|
|
157
|
+
Readonly = "READONLY",
|
|
158
|
+
Required = "REQUIRED",
|
|
159
|
+
}
|
|
160
|
+
|
|
106
161
|
export type GetApplicationsQueryVariables = Exact<{ [key: string]: never }>;
|
|
107
162
|
|
|
108
163
|
export type GetApplicationsQuery = {
|
|
@@ -113,10 +168,9 @@ export type GetApplicationsQuery = {
|
|
|
113
168
|
node?: {
|
|
114
169
|
Id: string;
|
|
115
170
|
Name?: { value?: string | null } | null;
|
|
116
|
-
|
|
171
|
+
User__r?: {
|
|
117
172
|
FirstName?: { value?: string | null } | null;
|
|
118
173
|
LastName?: { value?: string | null } | null;
|
|
119
|
-
Name?: { value?: string | null } | null;
|
|
120
174
|
} | null;
|
|
121
175
|
Property__r?: {
|
|
122
176
|
Name?: { value?: string | null } | null;
|
|
@@ -177,7 +231,7 @@ export type GetOpenApplicationsQuery = {
|
|
|
177
231
|
node?: {
|
|
178
232
|
Id: string;
|
|
179
233
|
Name?: { value?: string | null } | null;
|
|
180
|
-
|
|
234
|
+
User__r?: { Name?: { value?: string | null } | null } | null;
|
|
181
235
|
Property__r?: { Address__c?: { value?: string | null } | null } | null;
|
|
182
236
|
Status__c?: { value?: string | null } | null;
|
|
183
237
|
CreatedDate?: { value?: string | null } | null;
|
|
@@ -215,10 +269,7 @@ export type GetMaintenanceRequestsQuery = {
|
|
|
215
269
|
Id: string;
|
|
216
270
|
Name?: { value?: string | null } | null;
|
|
217
271
|
Property__r?: { Address__c?: { value?: string | null } | null } | null;
|
|
218
|
-
|
|
219
|
-
Name?: { value?: string | null } | null;
|
|
220
|
-
User__r?: { Name?: { value?: string | null } | null } | null;
|
|
221
|
-
} | null;
|
|
272
|
+
User__r?: { Name?: { value?: string | null } | null } | null;
|
|
222
273
|
Type__c?: { value?: string | null } | null;
|
|
223
274
|
Priority__c?: { value?: string | null } | null;
|
|
224
275
|
Status__c?: { value?: string | null } | null;
|
|
@@ -253,11 +304,11 @@ export type GetAllMaintenanceRequestsQuery = {
|
|
|
253
304
|
Address__c?: { value?: string | null } | null;
|
|
254
305
|
Name?: { value?: string | null } | null;
|
|
255
306
|
} | null;
|
|
256
|
-
|
|
307
|
+
User__r?: { Name?: { value?: string | null } | null } | null;
|
|
308
|
+
Assigned_Worker__r?: {
|
|
257
309
|
Name?: { value?: string | null } | null;
|
|
258
|
-
|
|
310
|
+
Employment_Type__c?: { value?: string | null } | null;
|
|
259
311
|
} | null;
|
|
260
|
-
Owner?: { Name?: { value?: string | null } | null } | Record<PropertyKey, never> | null;
|
|
261
312
|
ContentDocumentLinks?: {
|
|
262
313
|
edges?: Array<{
|
|
263
314
|
node?: {
|
|
@@ -312,10 +363,10 @@ export type GetPropertiesQuery = {
|
|
|
312
363
|
Year_Built__c?: { value?: number | null } | null;
|
|
313
364
|
Hero_Image__c?: { value?: string | null } | null;
|
|
314
365
|
Deposit__c?: { value?: number | null } | null;
|
|
315
|
-
Parking__c?: { value?:
|
|
316
|
-
Pet_Friendly__c?: { value?:
|
|
366
|
+
Parking__c?: { value?: number | null } | null;
|
|
367
|
+
Pet_Friendly__c?: { value?: boolean | null } | null;
|
|
317
368
|
Available_Date__c?: { value?: string | null } | null;
|
|
318
|
-
Lease_Term__c?: { value?:
|
|
369
|
+
Lease_Term__c?: { value?: number | null } | null;
|
|
319
370
|
Features__c?: { value?: string | null } | null;
|
|
320
371
|
Utilities__c?: { value?: string | null } | null;
|
|
321
372
|
Tour_URL__c?: { value?: string | null } | null;
|
|
@@ -327,3 +378,100 @@ export type GetPropertiesQuery = {
|
|
|
327
378
|
};
|
|
328
379
|
};
|
|
329
380
|
};
|
|
381
|
+
|
|
382
|
+
export type GetObjectInfosQueryVariables = Exact<{
|
|
383
|
+
apiNames: Array<Scalars["String"]["input"]> | Scalars["String"]["input"];
|
|
384
|
+
}>;
|
|
385
|
+
|
|
386
|
+
export type GetObjectInfosQuery = {
|
|
387
|
+
uiapi: {
|
|
388
|
+
objectInfos?: Array<{
|
|
389
|
+
ApiName: string;
|
|
390
|
+
label?: string | null;
|
|
391
|
+
labelPlural?: string | null;
|
|
392
|
+
nameFields: Array<string | null>;
|
|
393
|
+
defaultRecordTypeId?: string | null;
|
|
394
|
+
keyPrefix?: string | null;
|
|
395
|
+
layoutable: boolean;
|
|
396
|
+
queryable: boolean;
|
|
397
|
+
searchable: boolean;
|
|
398
|
+
updateable: boolean;
|
|
399
|
+
deletable: boolean;
|
|
400
|
+
createable: boolean;
|
|
401
|
+
custom: boolean;
|
|
402
|
+
mruEnabled: boolean;
|
|
403
|
+
feedEnabled: boolean;
|
|
404
|
+
fields: Array<
|
|
405
|
+
| {
|
|
406
|
+
ApiName: string;
|
|
407
|
+
label?: string | null;
|
|
408
|
+
dataType?: DataType | null;
|
|
409
|
+
relationshipName?: string | null;
|
|
410
|
+
reference: boolean;
|
|
411
|
+
compound: boolean;
|
|
412
|
+
compoundFieldName?: string | null;
|
|
413
|
+
compoundComponentName?: string | null;
|
|
414
|
+
controllingFields: Array<string | null>;
|
|
415
|
+
controllerName?: string | null;
|
|
416
|
+
referenceToInfos: Array<{ ApiName: string; nameFields: Array<string | null> } | null>;
|
|
417
|
+
}
|
|
418
|
+
| {
|
|
419
|
+
ApiName: string;
|
|
420
|
+
label?: string | null;
|
|
421
|
+
dataType?: DataType | null;
|
|
422
|
+
relationshipName?: string | null;
|
|
423
|
+
reference: boolean;
|
|
424
|
+
compound: boolean;
|
|
425
|
+
compoundFieldName?: string | null;
|
|
426
|
+
compoundComponentName?: string | null;
|
|
427
|
+
controllingFields: Array<string | null>;
|
|
428
|
+
controllerName?: string | null;
|
|
429
|
+
referenceToInfos: Array<{ ApiName: string; nameFields: Array<string | null> } | null>;
|
|
430
|
+
}
|
|
431
|
+
| null
|
|
432
|
+
>;
|
|
433
|
+
recordTypeInfos: Array<{
|
|
434
|
+
recordTypeId?: string | null;
|
|
435
|
+
name?: string | null;
|
|
436
|
+
master: boolean;
|
|
437
|
+
available: boolean;
|
|
438
|
+
defaultRecordTypeMapping: boolean;
|
|
439
|
+
} | null>;
|
|
440
|
+
themeInfo?: { color?: string | null; iconUrl?: string | null } | null;
|
|
441
|
+
childRelationships: Array<{
|
|
442
|
+
relationshipName?: string | null;
|
|
443
|
+
fieldName?: string | null;
|
|
444
|
+
childObjectApiName: string;
|
|
445
|
+
} | null>;
|
|
446
|
+
dependentFields: Array<{ controllingField: string } | null>;
|
|
447
|
+
} | null> | null;
|
|
448
|
+
};
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
export type GetPicklistValuesQueryVariables = Exact<{
|
|
452
|
+
objectInfoInputs: Array<ObjectInfoInput> | ObjectInfoInput;
|
|
453
|
+
}>;
|
|
454
|
+
|
|
455
|
+
export type GetPicklistValuesQuery = {
|
|
456
|
+
uiapi: {
|
|
457
|
+
objectInfos?: Array<{
|
|
458
|
+
ApiName: string;
|
|
459
|
+
fields: Array<
|
|
460
|
+
| {
|
|
461
|
+
ApiName: string;
|
|
462
|
+
picklistValuesByRecordTypeIDs?: Array<{
|
|
463
|
+
recordTypeID: string;
|
|
464
|
+
defaultValue?: { value?: string | null } | null;
|
|
465
|
+
picklistValues?: Array<{
|
|
466
|
+
label?: string | null;
|
|
467
|
+
value?: string | null;
|
|
468
|
+
validFor?: Array<number | null> | null;
|
|
469
|
+
}> | null;
|
|
470
|
+
} | null> | null;
|
|
471
|
+
}
|
|
472
|
+
| { ApiName: string }
|
|
473
|
+
| null
|
|
474
|
+
>;
|
|
475
|
+
} | null> | null;
|
|
476
|
+
};
|
|
477
|
+
};
|
package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/api/maintenance.ts
CHANGED
|
@@ -98,11 +98,12 @@ const GET_ALL_MAINTENANCE_REQUESTS = gql`
|
|
|
98
98
|
value
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
101
|
+
Assigned_Worker__r {
|
|
102
|
+
Name {
|
|
103
|
+
value
|
|
104
|
+
}
|
|
105
|
+
Employment_Type__c {
|
|
106
|
+
value
|
|
106
107
|
}
|
|
107
108
|
}
|
|
108
109
|
ContentDocumentLinks(first: 1) {
|
|
@@ -241,8 +242,9 @@ function transformMaintenanceTaskFull(node: any): MaintenanceRequest {
|
|
|
241
242
|
// Get tenant unit from Property
|
|
242
243
|
const tenantUnit = node.Property__r?.Name?.value || node.Property__r?.Address__c?.value;
|
|
243
244
|
|
|
244
|
-
// Get assigned worker name from
|
|
245
|
-
const assignedWorkerName = node.
|
|
245
|
+
// Get assigned worker name and employment type from Assigned_Worker__r
|
|
246
|
+
const assignedWorkerName = node.Assigned_Worker__r?.Name?.value;
|
|
247
|
+
const assignedWorkerOrg = node.Assigned_Worker__r?.Employment_Type__c?.value;
|
|
246
248
|
|
|
247
249
|
return {
|
|
248
250
|
id: node.Id,
|
|
@@ -257,7 +259,7 @@ function transformMaintenanceTaskFull(node: any): MaintenanceRequest {
|
|
|
257
259
|
imageUrl,
|
|
258
260
|
tenantUnit,
|
|
259
261
|
assignedWorkerName,
|
|
260
|
-
assignedWorkerOrg
|
|
262
|
+
assignedWorkerOrg,
|
|
261
263
|
formattedDate,
|
|
262
264
|
};
|
|
263
265
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<svg width="16" height="19" viewBox="0 0 16 19" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<path d="M8 1.5L8.00006 0.5H7.99995L8 1.5ZM13.5967 6.56836L12.6015 6.66614L12.6826 7.49179L13.509 7.56451L13.5967 6.56836ZM13.4844 9.09375V10.0938V9.09375ZM2.40137 6.56836L2.49054 7.56438L3.31559 7.49051L3.39658 6.66612L2.40137 6.56836ZM8 1.5L7.99994 2.5C10.3989 2.50015 12.3717 4.32767 12.6015 6.66614L13.5967 6.56836L14.5919 6.47058C14.2626 3.11875 11.4383 0.500219 8.00006 0.5L8 1.5ZM13.5967 6.56836L13.509 7.56451C13.6446 7.57644 13.75 7.69093 13.75 7.82812H14.75H15.75C15.75 6.6431 14.8414 5.67402 13.6843 5.57221L13.5967 6.56836ZM14.75 7.82812H13.75C13.75 7.97483 13.6311 8.09375 13.4844 8.09375V9.09375V10.0938C14.7356 10.0938 15.75 9.0794 15.75 7.82812H14.75ZM13.4844 9.09375V8.09375H2.51562V9.09375V10.0938H13.4844V9.09375ZM2.51562 9.09375V8.09375C2.36892 8.09375 2.25 7.97483 2.25 7.82812H1.25H0.25C0.25 9.0794 1.26435 10.0938 2.51562 10.0938V9.09375ZM1.25 7.82812H2.25C2.25 7.69089 2.35531 7.57648 2.49054 7.56438L2.40137 6.56836L2.31219 5.57234C1.1566 5.67581 0.25 6.64446 0.25 7.82812H1.25ZM2.40137 6.56836L3.39658 6.66612C3.62626 4.32802 5.60062 2.50011 8.00005 2.5L8 1.5L7.99995 0.5C4.56208 0.500164 1.73548 3.11823 1.40616 6.47059L2.40137 6.56836Z" fill="#65185C"/>
|
|
3
|
+
<line x1="8.25" y1="1" x2="8.25" y2="3.5" stroke="#65185C" stroke-width="2" stroke-linecap="round"/>
|
|
4
|
+
<line x1="13.25" y1="3.66421" x2="11.6642" y2="5.25" stroke="#65185C" stroke-width="2" stroke-linecap="round"/>
|
|
5
|
+
<line x1="4.33579" y1="5.25" x2="2.75" y2="3.66421" stroke="#65185C" stroke-width="2" stroke-linecap="round"/>
|
|
6
|
+
<path d="M4 11C5.90288 13.8543 10.0971 13.8543 12 11" stroke="#65185C" stroke-width="2" stroke-linecap="round"/>
|
|
7
|
+
<path d="M1 18C4.70577 13.7648 11.2942 13.7648 15 18" stroke="#65185C" stroke-width="2" stroke-linecap="round"/>
|
|
8
|
+
</svg>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import { PieChart, Pie, Cell, ResponsiveContainer } from "recharts";
|
|
2
|
+
import { PieChart, Pie, Cell, ResponsiveContainer, Tooltip } from "recharts";
|
|
3
3
|
import { Card } from "./ui/card";
|
|
4
4
|
import { MoreVertical } from "lucide-react";
|
|
5
5
|
|
|
@@ -15,7 +15,26 @@ interface IssuesDonutChartProps {
|
|
|
15
15
|
|
|
16
16
|
export const IssuesDonutChart: React.FC<IssuesDonutChartProps> = ({ data }) => {
|
|
17
17
|
const total = data.reduce((sum, item) => sum + item.value, 0);
|
|
18
|
-
const
|
|
18
|
+
const maxValue = data.length > 0 ? Math.max(...data.map((item) => item.value)) : 0;
|
|
19
|
+
const mainPercentage = total > 0 ? Math.round((maxValue / total) * 100) : 0;
|
|
20
|
+
|
|
21
|
+
const CustomTooltip = ({ active, payload }: any) => {
|
|
22
|
+
if (active && payload && payload.length) {
|
|
23
|
+
const percentage = total > 0 ? Math.round((payload[0].value / total) * 100) : 0;
|
|
24
|
+
return (
|
|
25
|
+
<div className="bg-white p-3 border border-gray-200 rounded shadow-lg z-50">
|
|
26
|
+
<p className="text-sm font-semibold text-gray-800">{payload[0].name}</p>
|
|
27
|
+
<p className="text-sm text-gray-600">
|
|
28
|
+
Count: <span className="font-medium">{payload[0].value}</span>
|
|
29
|
+
</p>
|
|
30
|
+
<p className="text-sm text-gray-600">
|
|
31
|
+
Percentage: <span className="font-medium">{percentage}%</span>
|
|
32
|
+
</p>
|
|
33
|
+
</div>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
};
|
|
19
38
|
|
|
20
39
|
return (
|
|
21
40
|
<Card className="p-4 border-gray-200 shadow-sm flex flex-col relative">
|
|
@@ -43,10 +62,11 @@ export const IssuesDonutChart: React.FC<IssuesDonutChartProps> = ({ data }) => {
|
|
|
43
62
|
<Cell key={`cell-${index}`} fill={entry.color} />
|
|
44
63
|
))}
|
|
45
64
|
</Pie>
|
|
65
|
+
<Tooltip content={<CustomTooltip />} wrapperStyle={{ zIndex: 1000 }} />
|
|
46
66
|
</PieChart>
|
|
47
67
|
</ResponsiveContainer>
|
|
48
68
|
{/* Center text */}
|
|
49
|
-
<div className="absolute inset-0 flex items-center justify-center">
|
|
69
|
+
<div className="absolute inset-0 flex items-center justify-center pointer-events-none">
|
|
50
70
|
<div className="text-center">
|
|
51
71
|
<div className="text-5xl font-bold text-primary-purple">{mainPercentage}%</div>
|
|
52
72
|
</div>
|
|
@@ -21,11 +21,11 @@ export const MaintenanceDetailsModal: React.FC<MaintenanceDetailsModalProps> = (
|
|
|
21
21
|
const [isSaving, setIsSaving] = useState(false);
|
|
22
22
|
|
|
23
23
|
// Determine if status is editable
|
|
24
|
-
//
|
|
25
|
-
const isStatusEditable = request.status
|
|
24
|
+
// All statuses except Resolved can be edited
|
|
25
|
+
const isStatusEditable = request.status !== "Resolved";
|
|
26
26
|
|
|
27
|
-
// Status options
|
|
28
|
-
const statusOptions = ["New", "In Progress", "Resolved"];
|
|
27
|
+
// Status options - all possible statuses
|
|
28
|
+
const statusOptions = ["New", "Assigned", "In Progress", "On Hold", "Resolved"];
|
|
29
29
|
|
|
30
30
|
const handleSave = async () => {
|
|
31
31
|
if (!isStatusEditable) return;
|
package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/VerticalNav.tsx
CHANGED
|
@@ -4,6 +4,7 @@ import dashboardIcon from "../assets/icons/dashboard.svg";
|
|
|
4
4
|
import filesIcon from "../assets/icons/files.svg";
|
|
5
5
|
import propertiesIcon from "../assets/icons/properties.svg";
|
|
6
6
|
import maintenanceIcon from "../assets/icons/maintenance.svg";
|
|
7
|
+
import maintenanceWorkerIcon from "../assets/icons/maintenance-worker.svg";
|
|
7
8
|
import { PATHS } from "../lib/routeConfig.js";
|
|
8
9
|
|
|
9
10
|
interface NavItem {
|
|
@@ -17,7 +18,7 @@ const navItems: NavItem[] = [
|
|
|
17
18
|
{ path: PATHS.APPLICATIONS, icon: filesIcon, label: "Applications" },
|
|
18
19
|
{ path: PATHS.PROPERTIES, icon: propertiesIcon, label: "Properties" },
|
|
19
20
|
{ path: PATHS.MAINTENANCE_REQUESTS, icon: maintenanceIcon, label: "Maintenance Requests" },
|
|
20
|
-
{ path: PATHS.MAINTENANCE_WORKERS, icon:
|
|
21
|
+
{ path: PATHS.MAINTENANCE_WORKERS, icon: maintenanceWorkerIcon, label: "Maintenance Workers" },
|
|
21
22
|
];
|
|
22
23
|
|
|
23
24
|
export const VerticalNav: React.FC = () => {
|
|
@@ -44,7 +45,7 @@ export const VerticalNav: React.FC = () => {
|
|
|
44
45
|
title={item.label}
|
|
45
46
|
>
|
|
46
47
|
<img src={item.icon} alt={item.label} className="w-6 h-6" />
|
|
47
|
-
<span className="text-xs font-medium">{item.label}</span>
|
|
48
|
+
<span className="text-xs font-medium text-center">{item.label}</span>
|
|
48
49
|
</Link>
|
|
49
50
|
))}
|
|
50
51
|
</div>
|
package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/maintenanceAdapter.ts
CHANGED
|
@@ -16,7 +16,10 @@ interface MaintenanceNode {
|
|
|
16
16
|
Name?: { value?: string };
|
|
17
17
|
};
|
|
18
18
|
User__r?: { Name?: { value?: string }; DisplayValue?: { value?: string } };
|
|
19
|
-
|
|
19
|
+
Assigned_Worker__r?: {
|
|
20
|
+
Name?: { value?: string };
|
|
21
|
+
Employment_Type__c?: { value?: string };
|
|
22
|
+
};
|
|
20
23
|
ContentDocumentLinks?: {
|
|
21
24
|
edges?: Array<{
|
|
22
25
|
node?: {
|
|
@@ -68,7 +71,8 @@ export function nodeToMaintenanceRequest(
|
|
|
68
71
|
: undefined;
|
|
69
72
|
|
|
70
73
|
const tenantUnit = n.Property__r?.Name?.value ?? n.Property__r?.Address__c?.value;
|
|
71
|
-
const assignedWorkerName = n.
|
|
74
|
+
const assignedWorkerName = n.Assigned_Worker__r?.Name?.value;
|
|
75
|
+
const assignedWorkerOrg = n.Assigned_Worker__r?.Employment_Type__c?.value;
|
|
72
76
|
|
|
73
77
|
return {
|
|
74
78
|
id: n.Id ?? "",
|
|
@@ -83,7 +87,7 @@ export function nodeToMaintenanceRequest(
|
|
|
83
87
|
imageUrl,
|
|
84
88
|
tenantUnit,
|
|
85
89
|
assignedWorkerName: assignedWorkerName ?? undefined,
|
|
86
|
-
assignedWorkerOrg: undefined,
|
|
90
|
+
assignedWorkerOrg: assignedWorkerOrg ?? undefined,
|
|
87
91
|
formattedDate,
|
|
88
92
|
};
|
|
89
93
|
}
|
package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/maintenanceColumns.ts
CHANGED
|
@@ -13,6 +13,18 @@ export const MAINTENANCE_EXTRA_COLUMNS: Column[] = [
|
|
|
13
13
|
},
|
|
14
14
|
{ fieldApiName: "Property__r.Name", label: "Property Name", searchable: false, sortable: false },
|
|
15
15
|
{ fieldApiName: "User__r.Name", label: "User Name", searchable: false, sortable: false },
|
|
16
|
+
{
|
|
17
|
+
fieldApiName: "Assigned_Worker__r.Name",
|
|
18
|
+
label: "Assigned Worker Name",
|
|
19
|
+
searchable: false,
|
|
20
|
+
sortable: false,
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
fieldApiName: "Assigned_Worker__r.Employment_Type__c",
|
|
24
|
+
label: "Worker Employment Type",
|
|
25
|
+
searchable: false,
|
|
26
|
+
sortable: false,
|
|
27
|
+
},
|
|
16
28
|
{ fieldApiName: "OwnerId", label: "Owner Id", searchable: false, sortable: false },
|
|
17
29
|
{ fieldApiName: "Owner", label: "Owner", searchable: false, sortable: false },
|
|
18
30
|
];
|
|
@@ -22,9 +22,8 @@ import { nodeToMaintenanceRequest } from "../lib/maintenanceAdapter.js";
|
|
|
22
22
|
import { DASHBOARD_MAINTENANCE_LIMIT } from "../lib/constants.js";
|
|
23
23
|
import { PATHS } from "../lib/routeConfig.js";
|
|
24
24
|
|
|
25
|
-
const CHART_ISSUE_TYPES = ["Plumbing", "HVAC", "Electrical"] as const;
|
|
26
|
-
const
|
|
27
|
-
const CHART_COLORS = ["#7C3AED", "#EC4899", "#14B8A6", "#06B6D4"] as const;
|
|
25
|
+
const CHART_ISSUE_TYPES = ["Plumbing", "HVAC", "Electrical", "Appliance", "Pest"] as const;
|
|
26
|
+
const CHART_COLORS = ["#7C3AED", "#EC4899", "#14B8A6", "#06B6D4", "#F59E0B"] as const;
|
|
28
27
|
|
|
29
28
|
export default function Home() {
|
|
30
29
|
const navigate = useNavigate();
|
|
@@ -118,21 +117,21 @@ export default function Home() {
|
|
|
118
117
|
Plumbing: 0,
|
|
119
118
|
HVAC: 0,
|
|
120
119
|
Electrical: 0,
|
|
121
|
-
|
|
120
|
+
Appliance: 0,
|
|
121
|
+
Pest: 0,
|
|
122
122
|
};
|
|
123
123
|
maintenanceRequests.forEach((request) => {
|
|
124
124
|
const type = request.issueType;
|
|
125
125
|
if (CHART_ISSUE_TYPES.includes(type as (typeof CHART_ISSUE_TYPES)[number])) {
|
|
126
126
|
counts[type]++;
|
|
127
|
-
} else {
|
|
128
|
-
counts[CHART_OTHER_LABEL]++;
|
|
129
127
|
}
|
|
130
128
|
});
|
|
131
129
|
return [
|
|
132
130
|
{ name: "Plumbing", value: counts.Plumbing, color: CHART_COLORS[0] },
|
|
133
131
|
{ name: "HVAC", value: counts.HVAC, color: CHART_COLORS[1] },
|
|
134
132
|
{ name: "Electrical", value: counts.Electrical, color: CHART_COLORS[2] },
|
|
135
|
-
{ name:
|
|
133
|
+
{ name: "Appliance", value: counts.Appliance, color: CHART_COLORS[3] },
|
|
134
|
+
{ name: "Pest Control", value: counts.Pest, color: CHART_COLORS[4] },
|
|
136
135
|
];
|
|
137
136
|
}, [maintenanceRequests]);
|
|
138
137
|
|
|
@@ -1,13 +1,54 @@
|
|
|
1
|
-
import { useState } from "react";
|
|
1
|
+
import { useState, useEffect, useMemo } from "react";
|
|
2
2
|
import { useListPage } from "../hooks/useListPage.js";
|
|
3
3
|
import { maintenanceWorkersListConfig } from "../lib/listPageConfig.js";
|
|
4
4
|
import { ListPageWithFilters } from "../components/list/ListPageWithFilters.js";
|
|
5
5
|
import { PageHeader } from "../components/layout/PageHeader.js";
|
|
6
|
+
import { getAllMaintenanceRequests } from "../api/maintenance.js";
|
|
6
7
|
import type { MaintenanceWorker } from "../lib/types.js";
|
|
7
8
|
|
|
8
9
|
export default function MaintenanceWorkers() {
|
|
9
10
|
const list = useListPage(maintenanceWorkersListConfig);
|
|
10
11
|
const [selectedWorker, setSelectedWorker] = useState<MaintenanceWorker | null>(null);
|
|
12
|
+
const [requestCountsLoading, setRequestCountsLoading] = useState(true);
|
|
13
|
+
const [requestCounts, setRequestCounts] = useState<Record<string, number>>({});
|
|
14
|
+
|
|
15
|
+
// Fetch maintenance requests to calculate active request counts per worker
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
let mounted = true;
|
|
18
|
+
async function fetchRequestCounts() {
|
|
19
|
+
try {
|
|
20
|
+
setRequestCountsLoading(true);
|
|
21
|
+
const requests = await getAllMaintenanceRequests(500);
|
|
22
|
+
if (!mounted) return;
|
|
23
|
+
|
|
24
|
+
// Count active requests (not "Resolved") per worker
|
|
25
|
+
const counts: Record<string, number> = {};
|
|
26
|
+
for (const req of requests) {
|
|
27
|
+
if (req.status !== "Resolved" && req.assignedWorkerName) {
|
|
28
|
+
const workerName = req.assignedWorkerName;
|
|
29
|
+
counts[workerName] = (counts[workerName] || 0) + 1;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
setRequestCounts(counts);
|
|
33
|
+
} catch (error) {
|
|
34
|
+
console.error("Error fetching request counts:", error);
|
|
35
|
+
} finally {
|
|
36
|
+
if (mounted) setRequestCountsLoading(false);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
fetchRequestCounts();
|
|
40
|
+
return () => {
|
|
41
|
+
mounted = false;
|
|
42
|
+
};
|
|
43
|
+
}, []);
|
|
44
|
+
|
|
45
|
+
// Enrich workers with active request counts
|
|
46
|
+
const enrichedWorkers = useMemo(() => {
|
|
47
|
+
return list.items.map((worker) => ({
|
|
48
|
+
...worker,
|
|
49
|
+
activeRequestsCount: requestCounts[worker.name] || 0,
|
|
50
|
+
}));
|
|
51
|
+
}, [list.items, requestCounts]);
|
|
11
52
|
|
|
12
53
|
return (
|
|
13
54
|
<>
|
|
@@ -24,10 +65,10 @@ export default function MaintenanceWorkers() {
|
|
|
24
65
|
ariaLabel: maintenanceWorkersListConfig.filtersAriaLabel,
|
|
25
66
|
}}
|
|
26
67
|
filterError={list.filterError}
|
|
27
|
-
loading={list.loading}
|
|
68
|
+
loading={list.loading || requestCountsLoading}
|
|
28
69
|
error={list.error}
|
|
29
70
|
loadingMessage={maintenanceWorkersListConfig.loadingMessage}
|
|
30
|
-
isEmpty={
|
|
71
|
+
isEmpty={enrichedWorkers.length === 0}
|
|
31
72
|
searchPlaceholder="Search by name, organization, status..."
|
|
32
73
|
searchAriaLabel="Search workers"
|
|
33
74
|
>
|
|
@@ -55,10 +96,10 @@ export default function MaintenanceWorkers() {
|
|
|
55
96
|
</div>
|
|
56
97
|
</div>
|
|
57
98
|
<div className="divide-y divide-gray-200">
|
|
58
|
-
{
|
|
99
|
+
{enrichedWorkers.length === 0 ? (
|
|
59
100
|
<div className="text-center py-12 text-gray-500">No maintenance workers found</div>
|
|
60
101
|
) : (
|
|
61
|
-
|
|
102
|
+
enrichedWorkers.map((worker) => (
|
|
62
103
|
<div
|
|
63
104
|
key={worker.id}
|
|
64
105
|
onClick={() => setSelectedWorker(worker)}
|
package/dist/package.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@salesforce/webapp-template-app-react-sample-b2e-experimental",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.81.0",
|
|
4
4
|
"description": "B2E starter app template",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.txt",
|
|
6
6
|
"author": "",
|
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
"clean": "rm -rf dist"
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@salesforce/webapp-experimental": "^1.
|
|
20
|
-
"@salesforce/webapp-template-feature-react-global-search-experimental": "^1.
|
|
19
|
+
"@salesforce/webapp-experimental": "^1.81.0",
|
|
20
|
+
"@salesforce/webapp-template-feature-react-global-search-experimental": "^1.81.0"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
23
|
"@testing-library/jest-dom": "^6.6.3",
|