@salesforce/webapp-template-app-react-sample-b2e-experimental 1.64.0 → 1.66.0

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