@salesforce/webapp-template-app-react-sample-b2e-experimental 1.64.0 → 1.66.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/CHANGELOG.md +16 -0
- package/dist/force-app/main/default/data/Agent__c.json +79 -0
- package/dist/force-app/main/default/data/Application__c.json +10 -10
- package/dist/force-app/main/default/data/Contact.json +44 -0
- package/dist/force-app/main/default/data/Maintenance_Request__c.json +30 -30
- package/dist/force-app/main/default/data/Property__c.json +25 -25
- package/dist/force-app/main/default/data/data-plan.json +13 -1
- package/dist/force-app/main/default/objects/Agent__c/Agent__c.object-meta.xml +66 -0
- package/dist/force-app/main/default/objects/Agent__c/fields/Agent_Type__c.field-meta.xml +37 -0
- package/dist/force-app/main/default/objects/Agent__c/fields/Availability__c.field-meta.xml +37 -0
- package/dist/force-app/main/default/objects/Agent__c/fields/Emergency_Alt__c.field-meta.xml +11 -0
- package/dist/force-app/main/default/objects/Agent__c/fields/Language__c.field-meta.xml +42 -0
- package/dist/force-app/main/default/objects/Agent__c/fields/License_Expiry__c.field-meta.xml +11 -0
- package/dist/force-app/main/default/objects/Agent__c/fields/License_Number__c.field-meta.xml +14 -0
- package/dist/force-app/main/default/objects/Agent__c/fields/Office_Location__c.field-meta.xml +42 -0
- package/dist/force-app/main/default/objects/Agent__c/fields/Territory__c.field-meta.xml +42 -0
- package/dist/force-app/main/default/objects/Maintenance_Request__c/fields/User__c.field-meta.xml +1 -1
- package/dist/force-app/main/default/objects/Property__c/fields/Agent__c.field-meta.xml +1 -1
- package/dist/force-app/main/default/permissionsets/Property_Management_Access.permissionset-meta.xml +53 -25
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/package-lock.json +18307 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/package.json +9 -7
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/api/applications.ts +142 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/api/dashboard.ts +1 -2
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/api/maintenance.ts +63 -6
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/api/properties.ts +1 -2
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/appLayout.tsx +4 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/AgentforceConversationClient.tsx +127 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/ApplicationDetailsModal.tsx +150 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/ApplicationsTable.tsx +105 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/MaintenanceDetailsModal.tsx +191 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/PropertyDetailsModal.tsx +274 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/StatusBadge.tsx +17 -17
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/TopBar.tsx +37 -7
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/VerticalNav.tsx +1 -5
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/index.ts +6 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/types.ts +6 -2
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/pages/Applications.tsx +129 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/pages/Home.tsx +28 -13
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/pages/Maintenance.tsx +95 -62
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/pages/Properties.tsx +22 -2
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/pages/TestAccPage.tsx +19 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/routes.tsx +12 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/types/conversation.ts +21 -0
- package/dist/package.json +1 -1
- package/package.json +2 -4
- package/dist/force-app/main/default/applications/Property_Management.app-meta.xml +0 -26
- package/dist/force-app/main/default/tabs/Application__c.tab-meta.xml +0 -6
- package/dist/force-app/main/default/tabs/Maintenance_Request__c.tab-meta.xml +0 -7
- package/dist/force-app/main/default/tabs/Maintenance_Worker__c.tab-meta.xml +0 -6
- package/dist/force-app/main/default/tabs/Notification__c.tab-meta.xml +0 -6
- package/dist/force-app/main/default/tabs/Property__c.tab-meta.xml +0 -7
- package/dist/force-app/main/default/tabs/Tenant__c.tab-meta.xml +0 -7
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/api/utils.ts +0 -4
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Card } from "@/components/ui/card";
|
|
3
|
+
import type { Application } from "../lib/types.js";
|
|
4
|
+
|
|
5
|
+
interface ApplicationsTableProps {
|
|
6
|
+
applications: Application[];
|
|
7
|
+
onRowClick: (application: Application) => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const getStatusColor = (status: string) => {
|
|
11
|
+
const statusLower = status.toLowerCase();
|
|
12
|
+
if (statusLower.includes("approved")) return "bg-green-100 text-green-700";
|
|
13
|
+
if (statusLower.includes("rejected")) return "bg-red-100 text-red-700";
|
|
14
|
+
if (statusLower.includes("background")) return "bg-blue-100 text-blue-700";
|
|
15
|
+
if (statusLower.includes("review")) return "bg-yellow-100 text-yellow-700";
|
|
16
|
+
return "bg-gray-100 text-gray-700";
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const ApplicationsTable: React.FC<ApplicationsTableProps> = ({
|
|
20
|
+
applications,
|
|
21
|
+
onRowClick,
|
|
22
|
+
}) => {
|
|
23
|
+
const formatDate = (dateString: string) => {
|
|
24
|
+
if (!dateString) return "N/A";
|
|
25
|
+
try {
|
|
26
|
+
const date = new Date(dateString);
|
|
27
|
+
return date.toLocaleDateString("en-US", {
|
|
28
|
+
year: "numeric",
|
|
29
|
+
month: "short",
|
|
30
|
+
day: "numeric",
|
|
31
|
+
});
|
|
32
|
+
} catch {
|
|
33
|
+
return dateString;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<Card className="border-gray-200 shadow-sm">
|
|
39
|
+
<div className="overflow-x-auto">
|
|
40
|
+
<table className="w-full">
|
|
41
|
+
<thead className="bg-gray-50 border-b border-gray-200">
|
|
42
|
+
<tr>
|
|
43
|
+
<th className="px-6 py-4 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
|
|
44
|
+
User
|
|
45
|
+
</th>
|
|
46
|
+
<th className="px-6 py-4 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
|
|
47
|
+
Start Date
|
|
48
|
+
</th>
|
|
49
|
+
<th className="px-6 py-4 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
|
|
50
|
+
Status
|
|
51
|
+
</th>
|
|
52
|
+
</tr>
|
|
53
|
+
</thead>
|
|
54
|
+
<tbody className="bg-white divide-y divide-gray-200">
|
|
55
|
+
{applications.length === 0 ? (
|
|
56
|
+
<tr>
|
|
57
|
+
<td colSpan={3} className="px-6 py-8 text-center text-gray-500">
|
|
58
|
+
No applications found
|
|
59
|
+
</td>
|
|
60
|
+
</tr>
|
|
61
|
+
) : (
|
|
62
|
+
applications.map((application) => (
|
|
63
|
+
<tr
|
|
64
|
+
key={application.id}
|
|
65
|
+
onClick={() => onRowClick(application)}
|
|
66
|
+
className="hover:bg-gray-50 cursor-pointer transition-colors"
|
|
67
|
+
>
|
|
68
|
+
<td className="px-6 py-4 whitespace-nowrap">
|
|
69
|
+
<div className="flex items-center">
|
|
70
|
+
<div className="w-10 h-10 bg-purple-100 rounded-full flex items-center justify-center flex-shrink-0">
|
|
71
|
+
<span className="text-sm font-medium text-purple-700">
|
|
72
|
+
{application.applicantName?.charAt(0) || "?"}
|
|
73
|
+
</span>
|
|
74
|
+
</div>
|
|
75
|
+
<div className="ml-4">
|
|
76
|
+
<div className="text-sm font-medium text-gray-900">
|
|
77
|
+
{application.applicantName || "Unknown"}
|
|
78
|
+
</div>
|
|
79
|
+
<div className="text-sm text-gray-500">
|
|
80
|
+
{application.propertyName || application.propertyAddress}
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
</td>
|
|
85
|
+
<td className="px-6 py-4 whitespace-nowrap">
|
|
86
|
+
<div className="text-sm text-gray-900">
|
|
87
|
+
{formatDate(application.startDate || application.submittedDate)}
|
|
88
|
+
</div>
|
|
89
|
+
</td>
|
|
90
|
+
<td className="px-6 py-4 whitespace-nowrap">
|
|
91
|
+
<span
|
|
92
|
+
className={`inline-flex px-3 py-1 rounded-full text-xs font-medium ${getStatusColor(application.status)}`}
|
|
93
|
+
>
|
|
94
|
+
{application.status}
|
|
95
|
+
</span>
|
|
96
|
+
</td>
|
|
97
|
+
</tr>
|
|
98
|
+
))
|
|
99
|
+
)}
|
|
100
|
+
</tbody>
|
|
101
|
+
</table>
|
|
102
|
+
</div>
|
|
103
|
+
</Card>
|
|
104
|
+
);
|
|
105
|
+
};
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import { X } from "lucide-react";
|
|
3
|
+
import { Button } from "@/components/ui/button";
|
|
4
|
+
import { PriorityBadge } from "./PriorityBadge.js";
|
|
5
|
+
import type { MaintenanceRequest } from "../lib/types.js";
|
|
6
|
+
|
|
7
|
+
interface MaintenanceDetailsModalProps {
|
|
8
|
+
request: MaintenanceRequest;
|
|
9
|
+
isOpen: boolean;
|
|
10
|
+
onClose: () => void;
|
|
11
|
+
onSave: (requestId: string, status: string) => Promise<void>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const MaintenanceDetailsModal: React.FC<MaintenanceDetailsModalProps> = ({
|
|
15
|
+
request,
|
|
16
|
+
isOpen,
|
|
17
|
+
onClose,
|
|
18
|
+
onSave,
|
|
19
|
+
}) => {
|
|
20
|
+
const [selectedStatus, setSelectedStatus] = useState(request.status);
|
|
21
|
+
const [isSaving, setIsSaving] = useState(false);
|
|
22
|
+
|
|
23
|
+
// Determine if status is editable
|
|
24
|
+
// Only New and In Progress can be updated to Resolved
|
|
25
|
+
const isStatusEditable = request.status === "New" || request.status === "In Progress";
|
|
26
|
+
|
|
27
|
+
// Status options
|
|
28
|
+
const statusOptions = ["New", "In Progress", "Resolved"];
|
|
29
|
+
|
|
30
|
+
const handleSave = async () => {
|
|
31
|
+
if (!isStatusEditable) return;
|
|
32
|
+
|
|
33
|
+
setIsSaving(true);
|
|
34
|
+
try {
|
|
35
|
+
await onSave(request.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">Maintenance Request 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
|
+
{/* Image */}
|
|
64
|
+
{request.imageUrl && (
|
|
65
|
+
<div>
|
|
66
|
+
<h3 className="text-sm font-semibold text-gray-500 uppercase tracking-wide mb-2">
|
|
67
|
+
Image
|
|
68
|
+
</h3>
|
|
69
|
+
<div className="w-full h-64 rounded-lg bg-gray-200 overflow-hidden">
|
|
70
|
+
<img
|
|
71
|
+
src={request.imageUrl}
|
|
72
|
+
alt={request.description}
|
|
73
|
+
className="w-full h-full object-cover"
|
|
74
|
+
/>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
)}
|
|
78
|
+
|
|
79
|
+
{/* Description */}
|
|
80
|
+
<div>
|
|
81
|
+
<h3 className="text-sm font-semibold text-gray-500 uppercase tracking-wide mb-2">
|
|
82
|
+
Description
|
|
83
|
+
</h3>
|
|
84
|
+
<p className="text-lg font-medium text-gray-900">{request.description}</p>
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
{/* Issue Type & Priority */}
|
|
88
|
+
<div className="grid grid-cols-2 gap-4">
|
|
89
|
+
<div>
|
|
90
|
+
<h3 className="text-sm font-semibold text-gray-500 uppercase tracking-wide mb-2">
|
|
91
|
+
Issue Type
|
|
92
|
+
</h3>
|
|
93
|
+
<p className="text-base text-gray-900">{request.issueType}</p>
|
|
94
|
+
</div>
|
|
95
|
+
<div>
|
|
96
|
+
<h3 className="text-sm font-semibold text-gray-500 uppercase tracking-wide mb-2">
|
|
97
|
+
Priority
|
|
98
|
+
</h3>
|
|
99
|
+
<PriorityBadge priority={request.priority} />
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
{/* Property */}
|
|
104
|
+
<div>
|
|
105
|
+
<h3 className="text-sm font-semibold text-gray-500 uppercase tracking-wide mb-2">
|
|
106
|
+
Property
|
|
107
|
+
</h3>
|
|
108
|
+
<p className="text-base text-gray-900">{request.propertyAddress}</p>
|
|
109
|
+
</div>
|
|
110
|
+
|
|
111
|
+
{/* Tenant */}
|
|
112
|
+
<div>
|
|
113
|
+
<h3 className="text-sm font-semibold text-gray-500 uppercase tracking-wide mb-2">
|
|
114
|
+
Tenant
|
|
115
|
+
</h3>
|
|
116
|
+
<p className="text-base text-gray-900">{request.tenantName || "Unknown"}</p>
|
|
117
|
+
{request.tenantUnit && (
|
|
118
|
+
<p className="text-sm text-gray-600 mt-1">Unit: {request.tenantUnit}</p>
|
|
119
|
+
)}
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
{/* Assigned Worker */}
|
|
123
|
+
{request.assignedWorkerName && (
|
|
124
|
+
<div>
|
|
125
|
+
<h3 className="text-sm font-semibold text-gray-500 uppercase tracking-wide mb-2">
|
|
126
|
+
Assigned Worker
|
|
127
|
+
</h3>
|
|
128
|
+
<p className="text-base text-gray-900">{request.assignedWorkerName}</p>
|
|
129
|
+
{request.assignedWorkerOrg && (
|
|
130
|
+
<p className="text-sm text-gray-600 mt-1">{request.assignedWorkerOrg}</p>
|
|
131
|
+
)}
|
|
132
|
+
</div>
|
|
133
|
+
)}
|
|
134
|
+
|
|
135
|
+
{/* Scheduled Date */}
|
|
136
|
+
{request.formattedDate && (
|
|
137
|
+
<div>
|
|
138
|
+
<h3 className="text-sm font-semibold text-gray-500 uppercase tracking-wide mb-2">
|
|
139
|
+
Scheduled Date
|
|
140
|
+
</h3>
|
|
141
|
+
<p className="text-base text-gray-900">{request.formattedDate}</p>
|
|
142
|
+
</div>
|
|
143
|
+
)}
|
|
144
|
+
|
|
145
|
+
{/* Status */}
|
|
146
|
+
<div>
|
|
147
|
+
<h3 className="text-sm font-semibold text-gray-500 uppercase tracking-wide mb-2">
|
|
148
|
+
Status
|
|
149
|
+
</h3>
|
|
150
|
+
{isStatusEditable ? (
|
|
151
|
+
<select
|
|
152
|
+
value={selectedStatus}
|
|
153
|
+
onChange={(e) => setSelectedStatus(e.target.value)}
|
|
154
|
+
className="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-purple-500 focus:border-transparent"
|
|
155
|
+
>
|
|
156
|
+
{statusOptions.map((status) => (
|
|
157
|
+
<option key={status} value={status}>
|
|
158
|
+
{status}
|
|
159
|
+
</option>
|
|
160
|
+
))}
|
|
161
|
+
</select>
|
|
162
|
+
) : (
|
|
163
|
+
<div className="flex items-center">
|
|
164
|
+
<span className="px-4 py-2 bg-gray-100 text-gray-700 rounded-md font-medium">
|
|
165
|
+
{request.status}
|
|
166
|
+
</span>
|
|
167
|
+
<span className="ml-3 text-sm text-gray-500">(Cannot be modified)</span>
|
|
168
|
+
</div>
|
|
169
|
+
)}
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
|
|
173
|
+
{/* Footer */}
|
|
174
|
+
<div className="flex items-center justify-end gap-3 p-6 border-t border-gray-200">
|
|
175
|
+
<Button variant="outline" onClick={onClose} disabled={isSaving}>
|
|
176
|
+
Close
|
|
177
|
+
</Button>
|
|
178
|
+
{isStatusEditable && (
|
|
179
|
+
<Button
|
|
180
|
+
onClick={handleSave}
|
|
181
|
+
disabled={isSaving || selectedStatus === request.status}
|
|
182
|
+
className="bg-purple-600 hover:bg-purple-700"
|
|
183
|
+
>
|
|
184
|
+
{isSaving ? "Saving..." : "Save Changes"}
|
|
185
|
+
</Button>
|
|
186
|
+
)}
|
|
187
|
+
</div>
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
);
|
|
191
|
+
};
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { X } from "lucide-react";
|
|
3
|
+
import { Button } from "@/components/ui/button";
|
|
4
|
+
import type { Property } from "../lib/types.js";
|
|
5
|
+
|
|
6
|
+
interface PropertyDetailsModalProps {
|
|
7
|
+
property: Property;
|
|
8
|
+
isOpen: boolean;
|
|
9
|
+
onClose: () => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const PropertyDetailsModal: React.FC<PropertyDetailsModalProps> = ({
|
|
13
|
+
property,
|
|
14
|
+
isOpen,
|
|
15
|
+
onClose,
|
|
16
|
+
}) => {
|
|
17
|
+
if (!isOpen) return null;
|
|
18
|
+
|
|
19
|
+
const formatCurrency = (amount: number) => {
|
|
20
|
+
return new Intl.NumberFormat("en-US", {
|
|
21
|
+
style: "currency",
|
|
22
|
+
currency: "USD",
|
|
23
|
+
minimumFractionDigits: 0,
|
|
24
|
+
}).format(amount);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const getStatusColor = (status: string) => {
|
|
28
|
+
switch (status.toLowerCase()) {
|
|
29
|
+
case "available":
|
|
30
|
+
return "bg-green-100 text-green-700";
|
|
31
|
+
case "rented":
|
|
32
|
+
return "bg-blue-100 text-blue-700";
|
|
33
|
+
case "maintenance":
|
|
34
|
+
return "bg-yellow-100 text-yellow-700";
|
|
35
|
+
default:
|
|
36
|
+
return "bg-gray-100 text-gray-700";
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
|
42
|
+
{/* Backdrop */}
|
|
43
|
+
<div className="fixed inset-0 bg-black bg-opacity-50" onClick={onClose} />
|
|
44
|
+
|
|
45
|
+
{/* Modal */}
|
|
46
|
+
<div className="relative bg-white rounded-lg shadow-xl w-full max-w-4xl mx-4 max-h-[90vh] overflow-y-auto">
|
|
47
|
+
{/* Header */}
|
|
48
|
+
<div className="flex items-center justify-between p-6 border-b border-gray-200">
|
|
49
|
+
<h2 className="text-xl font-semibold text-gray-900">Property Details</h2>
|
|
50
|
+
<button onClick={onClose} className="text-gray-400 hover:text-gray-600 transition-colors">
|
|
51
|
+
<X className="w-6 h-6" />
|
|
52
|
+
</button>
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
{/* Content */}
|
|
56
|
+
<div className="p-6 space-y-6">
|
|
57
|
+
{/* Hero Image */}
|
|
58
|
+
{property.heroImage && (
|
|
59
|
+
<div className="w-full h-80 rounded-lg bg-gray-200 overflow-hidden">
|
|
60
|
+
<img
|
|
61
|
+
src={property.heroImage}
|
|
62
|
+
alt={property.name}
|
|
63
|
+
className="w-full h-full object-cover"
|
|
64
|
+
/>
|
|
65
|
+
</div>
|
|
66
|
+
)}
|
|
67
|
+
|
|
68
|
+
{/* Property Name and Address */}
|
|
69
|
+
<div>
|
|
70
|
+
<h3 className="text-2xl font-bold text-gray-900">{property.name}</h3>
|
|
71
|
+
<p className="text-base text-gray-600 mt-1">{property.address}</p>
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
{/* Type, Status, and Rent */}
|
|
75
|
+
<div className="grid grid-cols-3 gap-4">
|
|
76
|
+
<div>
|
|
77
|
+
<h4 className="text-sm font-semibold text-gray-500 uppercase tracking-wide mb-2">
|
|
78
|
+
Type
|
|
79
|
+
</h4>
|
|
80
|
+
<p className="text-base text-gray-900 capitalize">{property.type}</p>
|
|
81
|
+
</div>
|
|
82
|
+
<div>
|
|
83
|
+
<h4 className="text-sm font-semibold text-gray-500 uppercase tracking-wide mb-2">
|
|
84
|
+
Status
|
|
85
|
+
</h4>
|
|
86
|
+
<span
|
|
87
|
+
className={`inline-flex px-3 py-1 rounded-full text-sm font-medium capitalize ${getStatusColor(property.status)}`}
|
|
88
|
+
>
|
|
89
|
+
{property.status}
|
|
90
|
+
</span>
|
|
91
|
+
</div>
|
|
92
|
+
<div>
|
|
93
|
+
<h4 className="text-sm font-semibold text-gray-500 uppercase tracking-wide mb-2">
|
|
94
|
+
Monthly Rent
|
|
95
|
+
</h4>
|
|
96
|
+
<p className="text-xl font-bold text-purple-700">
|
|
97
|
+
{formatCurrency(property.monthlyRent)}
|
|
98
|
+
</p>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
{/* Property Specifications */}
|
|
103
|
+
<div>
|
|
104
|
+
<h4 className="text-sm font-semibold text-gray-500 uppercase tracking-wide mb-3">
|
|
105
|
+
Property Specifications
|
|
106
|
+
</h4>
|
|
107
|
+
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
|
108
|
+
{property.bedrooms !== undefined && (
|
|
109
|
+
<div className="bg-gray-50 rounded-lg p-4">
|
|
110
|
+
<p className="text-sm text-gray-600">Bedrooms</p>
|
|
111
|
+
<p className="text-lg font-semibold text-gray-900">{property.bedrooms}</p>
|
|
112
|
+
</div>
|
|
113
|
+
)}
|
|
114
|
+
{property.bathrooms !== undefined && (
|
|
115
|
+
<div className="bg-gray-50 rounded-lg p-4">
|
|
116
|
+
<p className="text-sm text-gray-600">Bathrooms</p>
|
|
117
|
+
<p className="text-lg font-semibold text-gray-900">{property.bathrooms}</p>
|
|
118
|
+
</div>
|
|
119
|
+
)}
|
|
120
|
+
{property.sqFt !== undefined && (
|
|
121
|
+
<div className="bg-gray-50 rounded-lg p-4">
|
|
122
|
+
<p className="text-sm text-gray-600">Square Feet</p>
|
|
123
|
+
<p className="text-lg font-semibold text-gray-900">
|
|
124
|
+
{property.sqFt.toLocaleString()}
|
|
125
|
+
</p>
|
|
126
|
+
</div>
|
|
127
|
+
)}
|
|
128
|
+
{property.yearBuilt !== undefined && (
|
|
129
|
+
<div className="bg-gray-50 rounded-lg p-4">
|
|
130
|
+
<p className="text-sm text-gray-600">Year Built</p>
|
|
131
|
+
<p className="text-lg font-semibold text-gray-900">{property.yearBuilt}</p>
|
|
132
|
+
</div>
|
|
133
|
+
)}
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
|
|
137
|
+
{/* Lease Information */}
|
|
138
|
+
<div>
|
|
139
|
+
<h4 className="text-sm font-semibold text-gray-500 uppercase tracking-wide mb-3">
|
|
140
|
+
Lease Information
|
|
141
|
+
</h4>
|
|
142
|
+
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
|
|
143
|
+
{property.deposit !== undefined && (
|
|
144
|
+
<div>
|
|
145
|
+
<p className="text-sm text-gray-600">Security Deposit</p>
|
|
146
|
+
<p className="text-base font-semibold text-gray-900">
|
|
147
|
+
{formatCurrency(property.deposit)}
|
|
148
|
+
</p>
|
|
149
|
+
</div>
|
|
150
|
+
)}
|
|
151
|
+
{property.leaseTerm !== undefined && (
|
|
152
|
+
<div>
|
|
153
|
+
<p className="text-sm text-gray-600">Lease Term</p>
|
|
154
|
+
<p className="text-base font-semibold text-gray-900">
|
|
155
|
+
{property.leaseTerm} months
|
|
156
|
+
</p>
|
|
157
|
+
</div>
|
|
158
|
+
)}
|
|
159
|
+
{property.availableDate && (
|
|
160
|
+
<div>
|
|
161
|
+
<p className="text-sm text-gray-600">Available Date</p>
|
|
162
|
+
<p className="text-base font-semibold text-gray-900">{property.availableDate}</p>
|
|
163
|
+
</div>
|
|
164
|
+
)}
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
|
|
168
|
+
{/* Amenities */}
|
|
169
|
+
<div>
|
|
170
|
+
<h4 className="text-sm font-semibold text-gray-500 uppercase tracking-wide mb-3">
|
|
171
|
+
Amenities
|
|
172
|
+
</h4>
|
|
173
|
+
<div className="flex flex-wrap gap-4">
|
|
174
|
+
{property.parking !== undefined && (
|
|
175
|
+
<div className="flex items-center gap-2">
|
|
176
|
+
<span className="text-lg">🚗</span>
|
|
177
|
+
<span className="text-sm text-gray-700">{property.parking} Parking Space(s)</span>
|
|
178
|
+
</div>
|
|
179
|
+
)}
|
|
180
|
+
{property.petFriendly !== undefined && (
|
|
181
|
+
<div className="flex items-center gap-2">
|
|
182
|
+
<span className="text-lg">{property.petFriendly ? "🐾" : "🚫"}</span>
|
|
183
|
+
<span className="text-sm text-gray-700">
|
|
184
|
+
{property.petFriendly ? "Pet Friendly" : "No Pets"}
|
|
185
|
+
</span>
|
|
186
|
+
</div>
|
|
187
|
+
)}
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
|
|
191
|
+
{/* Features */}
|
|
192
|
+
{property.features && property.features.length > 0 && (
|
|
193
|
+
<div>
|
|
194
|
+
<h4 className="text-sm font-semibold text-gray-500 uppercase tracking-wide mb-3">
|
|
195
|
+
Features
|
|
196
|
+
</h4>
|
|
197
|
+
<div className="flex flex-wrap gap-2">
|
|
198
|
+
{property.features.map((feature, index) => (
|
|
199
|
+
<span
|
|
200
|
+
key={index}
|
|
201
|
+
className="px-3 py-1 bg-purple-100 text-purple-700 rounded-full text-sm font-medium"
|
|
202
|
+
>
|
|
203
|
+
{feature}
|
|
204
|
+
</span>
|
|
205
|
+
))}
|
|
206
|
+
</div>
|
|
207
|
+
</div>
|
|
208
|
+
)}
|
|
209
|
+
|
|
210
|
+
{/* Utilities */}
|
|
211
|
+
{property.utilities && property.utilities.length > 0 && (
|
|
212
|
+
<div>
|
|
213
|
+
<h4 className="text-sm font-semibold text-gray-500 uppercase tracking-wide mb-3">
|
|
214
|
+
Utilities Included
|
|
215
|
+
</h4>
|
|
216
|
+
<div className="flex flex-wrap gap-2">
|
|
217
|
+
{property.utilities.map((utility, index) => (
|
|
218
|
+
<span
|
|
219
|
+
key={index}
|
|
220
|
+
className="px-3 py-1 bg-blue-100 text-blue-700 rounded-full text-sm font-medium"
|
|
221
|
+
>
|
|
222
|
+
{utility}
|
|
223
|
+
</span>
|
|
224
|
+
))}
|
|
225
|
+
</div>
|
|
226
|
+
</div>
|
|
227
|
+
)}
|
|
228
|
+
|
|
229
|
+
{/* Description */}
|
|
230
|
+
{property.description && (
|
|
231
|
+
<div>
|
|
232
|
+
<h4 className="text-sm font-semibold text-gray-500 uppercase tracking-wide mb-2">
|
|
233
|
+
Description
|
|
234
|
+
</h4>
|
|
235
|
+
<p className="text-sm text-gray-700 whitespace-pre-wrap">{property.description}</p>
|
|
236
|
+
</div>
|
|
237
|
+
)}
|
|
238
|
+
|
|
239
|
+
{/* Virtual Tour */}
|
|
240
|
+
{property.tourUrl && (
|
|
241
|
+
<div>
|
|
242
|
+
<h4 className="text-sm font-semibold text-gray-500 uppercase tracking-wide mb-2">
|
|
243
|
+
Virtual Tour
|
|
244
|
+
</h4>
|
|
245
|
+
<a
|
|
246
|
+
href={property.tourUrl}
|
|
247
|
+
target="_blank"
|
|
248
|
+
rel="noopener noreferrer"
|
|
249
|
+
className="inline-flex items-center gap-2 text-purple-600 hover:text-purple-700 font-medium"
|
|
250
|
+
>
|
|
251
|
+
<span>View Virtual Tour</span>
|
|
252
|
+
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
253
|
+
<path
|
|
254
|
+
strokeLinecap="round"
|
|
255
|
+
strokeLinejoin="round"
|
|
256
|
+
strokeWidth={2}
|
|
257
|
+
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
|
|
258
|
+
/>
|
|
259
|
+
</svg>
|
|
260
|
+
</a>
|
|
261
|
+
</div>
|
|
262
|
+
)}
|
|
263
|
+
</div>
|
|
264
|
+
|
|
265
|
+
{/* Footer */}
|
|
266
|
+
<div className="flex items-center justify-end gap-3 p-6 border-t border-gray-200">
|
|
267
|
+
<Button variant="outline" onClick={onClose}>
|
|
268
|
+
Close
|
|
269
|
+
</Button>
|
|
270
|
+
</div>
|
|
271
|
+
</div>
|
|
272
|
+
</div>
|
|
273
|
+
);
|
|
274
|
+
};
|
package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/StatusBadge.tsx
CHANGED
|
@@ -2,36 +2,36 @@ import React from "react";
|
|
|
2
2
|
import { Check } from "lucide-react";
|
|
3
3
|
|
|
4
4
|
interface StatusBadgeProps {
|
|
5
|
-
status:
|
|
5
|
+
status: string;
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
export const StatusBadge: React.FC<StatusBadgeProps> = ({ status }) => {
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
9
|
+
const statusLower = status.toLowerCase();
|
|
10
|
+
|
|
11
|
+
const getStyle = () => {
|
|
12
|
+
if (statusLower === "new") return "bg-pink-100 text-pink-700";
|
|
13
|
+
if (statusLower === "in progress") return "bg-yellow-100 text-yellow-700";
|
|
14
|
+
if (statusLower === "resolved") return "bg-green-100 text-green-700";
|
|
15
|
+
return "bg-gray-100 text-gray-700";
|
|
15
16
|
};
|
|
16
17
|
|
|
17
|
-
const
|
|
18
|
-
new
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
completed: "Completed",
|
|
18
|
+
const getLabel = () => {
|
|
19
|
+
if (statusLower === "new") return "Needs Action";
|
|
20
|
+
if (statusLower === "in progress") return "In Progress";
|
|
21
|
+
if (statusLower === "resolved") return "Resolved";
|
|
22
|
+
return status;
|
|
23
23
|
};
|
|
24
24
|
|
|
25
|
-
const showCheckmark =
|
|
26
|
-
const showDot =
|
|
25
|
+
const showCheckmark = statusLower === "resolved";
|
|
26
|
+
const showDot = statusLower === "new" || statusLower === "in progress";
|
|
27
27
|
|
|
28
28
|
return (
|
|
29
29
|
<span
|
|
30
|
-
className={`inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-sm font-medium ${
|
|
30
|
+
className={`inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-sm font-medium ${getStyle()}`}
|
|
31
31
|
>
|
|
32
32
|
{showCheckmark && <Check className="w-4 h-4" />}
|
|
33
33
|
{showDot && <span className="w-2 h-2 rounded-full bg-current" />}
|
|
34
|
-
{
|
|
34
|
+
{getLabel()}
|
|
35
35
|
</span>
|
|
36
36
|
);
|
|
37
37
|
};
|
package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/TopBar.tsx
CHANGED
|
@@ -9,6 +9,7 @@ interface TopBarProps {
|
|
|
9
9
|
|
|
10
10
|
export const TopBar: React.FC<TopBarProps> = ({ onMenuClick }) => {
|
|
11
11
|
const [userName, setUserName] = useState<string>("User");
|
|
12
|
+
const [showNotifications, setShowNotifications] = useState(false);
|
|
12
13
|
|
|
13
14
|
useEffect(() => {
|
|
14
15
|
const loadUserInfo = async () => {
|
|
@@ -19,6 +20,14 @@ export const TopBar: React.FC<TopBarProps> = ({ onMenuClick }) => {
|
|
|
19
20
|
};
|
|
20
21
|
loadUserInfo();
|
|
21
22
|
}, []);
|
|
23
|
+
|
|
24
|
+
const handleNotificationClick = () => {
|
|
25
|
+
setShowNotifications(!showNotifications);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const handleCloseNotifications = () => {
|
|
29
|
+
setShowNotifications(false);
|
|
30
|
+
};
|
|
22
31
|
return (
|
|
23
32
|
<div className="bg-[#372949] text-white h-16 flex items-center justify-between px-6">
|
|
24
33
|
{/* Left section - Logo and Menu */}
|
|
@@ -50,13 +59,34 @@ export const TopBar: React.FC<TopBarProps> = ({ onMenuClick }) => {
|
|
|
50
59
|
</button>
|
|
51
60
|
|
|
52
61
|
{/* Notifications */}
|
|
53
|
-
<
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
62
|
+
<div className="relative">
|
|
63
|
+
<button
|
|
64
|
+
onClick={handleNotificationClick}
|
|
65
|
+
className="p-2 hover:bg-purple-700 rounded-md transition-colors relative"
|
|
66
|
+
aria-label="Notifications"
|
|
67
|
+
>
|
|
68
|
+
<Bell className="w-5 h-5" />
|
|
69
|
+
</button>
|
|
70
|
+
|
|
71
|
+
{/* Notifications Overlay */}
|
|
72
|
+
{showNotifications && (
|
|
73
|
+
<>
|
|
74
|
+
{/* Backdrop */}
|
|
75
|
+
<div className="fixed inset-0 z-40" onClick={handleCloseNotifications} />
|
|
76
|
+
|
|
77
|
+
{/* Notification Panel */}
|
|
78
|
+
<div className="absolute right-0 top-full mt-2 w-80 bg-white rounded-lg shadow-xl z-50 overflow-hidden">
|
|
79
|
+
<div className="p-4 border-b border-gray-200">
|
|
80
|
+
<h3 className="text-sm font-semibold text-gray-900">Notifications</h3>
|
|
81
|
+
</div>
|
|
82
|
+
<div className="p-8 text-center">
|
|
83
|
+
<Bell className="w-12 h-12 mx-auto text-gray-300 mb-3" />
|
|
84
|
+
<p className="text-sm text-gray-500">No new notifications</p>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
</>
|
|
88
|
+
)}
|
|
89
|
+
</div>
|
|
60
90
|
|
|
61
91
|
{/* User Profile */}
|
|
62
92
|
<button className="flex items-center gap-2 px-3 py-2 hover:bg-purple-700 rounded-md transition-colors">
|