@salesforce/webapp-template-app-react-sample-b2e-experimental 1.48.3 → 1.50.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/Property__c.json +2 -2
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/api/dashboard.ts +170 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/api/maintenance.ts +221 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/api/properties.ts +157 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/api/utils.ts +4 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/appLayout.tsx +20 -8
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/assets/icons/appliances.svg +13 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/assets/icons/dashboard.svg +4 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/assets/icons/downgraph.svg +3 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/assets/icons/electrical.svg +41 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/assets/icons/files.svg +7 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/assets/icons/hvac.svg +79 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/assets/icons/maintenance.svg +4 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/assets/icons/pest.svg +5 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/assets/icons/plumbing.svg +7 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/assets/icons/properties.svg +14 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/assets/icons/support.svg +6 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/assets/icons/upgraph.svg +3 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/assets/icons/users.svg +8 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/assets/icons/zen-logo.svg +5 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/AnalyticsTile.tsx +29 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/ApplicationCard.tsx +43 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/IssuesDonutChart.tsx +66 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/MaintenanceRequestCard.tsx +71 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/MaintenanceTable.tsx +110 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/PriorityBadge.tsx +29 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/PropertyCard.tsx +61 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/StatCard.tsx +52 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/StatusBadge.tsx +37 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/TopBar.tsx +72 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/UserAvatar.tsx +35 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/VerticalNav.tsx +54 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/ui/alert.tsx +69 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/ui/button.tsx +67 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/ui/card.tsx +92 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/ui/dialog.tsx +143 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/ui/field.tsx +222 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/ui/index.ts +72 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/ui/input.tsx +19 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/ui/label.tsx +19 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/ui/pagination.tsx +112 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/ui/select.tsx +183 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/ui/separator.tsx +26 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/ui/skeleton.tsx +14 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/ui/spinner.tsx +15 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/ui/table.tsx +87 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/ui/tabs.tsx +78 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components.json +18 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/types.ts +57 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/utils.ts +6 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/pages/Home.tsx +163 -10
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/pages/Maintenance.tsx +176 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/pages/Properties.tsx +94 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/routes.tsx +19 -7
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/styles/global.css +160 -0
- package/dist/package.json +1 -1
- package/package.json +2 -2
package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/ui/table.tsx
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
import { cn } from "../../lib/utils";
|
|
4
|
+
|
|
5
|
+
function Table({ className, ...props }: React.ComponentProps<"table">) {
|
|
6
|
+
return (
|
|
7
|
+
<div data-slot="table-container" className="relative w-full overflow-x-auto">
|
|
8
|
+
<table
|
|
9
|
+
data-slot="table"
|
|
10
|
+
className={cn("w-full caption-bottom text-sm", className)}
|
|
11
|
+
{...props}
|
|
12
|
+
/>
|
|
13
|
+
</div>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
|
|
18
|
+
return <thead data-slot="table-header" className={cn("[&_tr]:border-b", className)} {...props} />;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
|
|
22
|
+
return (
|
|
23
|
+
<tbody
|
|
24
|
+
data-slot="table-body"
|
|
25
|
+
className={cn("[&_tr:last-child]:border-0", className)}
|
|
26
|
+
{...props}
|
|
27
|
+
/>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
|
|
32
|
+
return (
|
|
33
|
+
<tfoot
|
|
34
|
+
data-slot="table-footer"
|
|
35
|
+
className={cn("bg-muted/50 border-t font-medium [&>tr]:last:border-b-0", className)}
|
|
36
|
+
{...props}
|
|
37
|
+
/>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
|
|
42
|
+
return (
|
|
43
|
+
<tr
|
|
44
|
+
data-slot="table-row"
|
|
45
|
+
className={cn(
|
|
46
|
+
"hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
|
|
47
|
+
className,
|
|
48
|
+
)}
|
|
49
|
+
{...props}
|
|
50
|
+
/>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
|
|
55
|
+
return (
|
|
56
|
+
<th
|
|
57
|
+
data-slot="table-head"
|
|
58
|
+
className={cn(
|
|
59
|
+
"text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0",
|
|
60
|
+
className,
|
|
61
|
+
)}
|
|
62
|
+
{...props}
|
|
63
|
+
/>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
|
|
68
|
+
return (
|
|
69
|
+
<td
|
|
70
|
+
data-slot="table-cell"
|
|
71
|
+
className={cn("p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0", className)}
|
|
72
|
+
{...props}
|
|
73
|
+
/>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function TableCaption({ className, ...props }: React.ComponentProps<"caption">) {
|
|
78
|
+
return (
|
|
79
|
+
<caption
|
|
80
|
+
data-slot="table-caption"
|
|
81
|
+
className={cn("text-muted-foreground mt-4 text-sm", className)}
|
|
82
|
+
{...props}
|
|
83
|
+
/>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption };
|
package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/ui/tabs.tsx
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
3
|
+
import { Tabs as TabsPrimitive } from "radix-ui";
|
|
4
|
+
|
|
5
|
+
import { cn } from "../../lib/utils";
|
|
6
|
+
|
|
7
|
+
function Tabs({
|
|
8
|
+
className,
|
|
9
|
+
orientation = "horizontal",
|
|
10
|
+
...props
|
|
11
|
+
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
|
|
12
|
+
return (
|
|
13
|
+
<TabsPrimitive.Root
|
|
14
|
+
data-slot="tabs"
|
|
15
|
+
data-orientation={orientation}
|
|
16
|
+
className={cn("gap-2 group/tabs flex data-horizontal:flex-col", className)}
|
|
17
|
+
{...props}
|
|
18
|
+
/>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const tabsListVariants = cva(
|
|
23
|
+
"rounded-lg p-[3px] group-data-horizontal/tabs:h-8 data-[variant=line]:rounded-none group/tabs-list text-muted-foreground inline-flex w-fit items-center justify-center group-data-vertical/tabs:h-fit group-data-vertical/tabs:flex-col",
|
|
24
|
+
{
|
|
25
|
+
variants: {
|
|
26
|
+
variant: {
|
|
27
|
+
default: "bg-muted",
|
|
28
|
+
line: "gap-1 bg-transparent",
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
defaultVariants: {
|
|
32
|
+
variant: "default",
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
function TabsList({
|
|
38
|
+
className,
|
|
39
|
+
variant = "default",
|
|
40
|
+
...props
|
|
41
|
+
}: React.ComponentProps<typeof TabsPrimitive.List> & VariantProps<typeof tabsListVariants>) {
|
|
42
|
+
return (
|
|
43
|
+
<TabsPrimitive.List
|
|
44
|
+
data-slot="tabs-list"
|
|
45
|
+
data-variant={variant}
|
|
46
|
+
className={cn(tabsListVariants({ variant }), className)}
|
|
47
|
+
{...props}
|
|
48
|
+
/>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function TabsTrigger({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
|
|
53
|
+
return (
|
|
54
|
+
<TabsPrimitive.Trigger
|
|
55
|
+
data-slot="tabs-trigger"
|
|
56
|
+
className={cn(
|
|
57
|
+
"gap-1.5 rounded-md border border-transparent px-1.5 py-0.5 text-sm font-medium group-data-[variant=default]/tabs-list:data-active:shadow-sm group-data-[variant=line]/tabs-list:data-active:shadow-none [&_svg:not([class*='size-'])]:size-4 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring text-foreground/60 hover:text-foreground dark:text-muted-foreground dark:hover:text-foreground relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center whitespace-nowrap transition-all group-data-vertical/tabs:w-full group-data-vertical/tabs:justify-start focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
|
58
|
+
"group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-active:bg-transparent dark:group-data-[variant=line]/tabs-list:data-active:border-transparent dark:group-data-[variant=line]/tabs-list:data-active:bg-transparent",
|
|
59
|
+
"data-active:bg-background dark:data-active:text-foreground dark:data-active:border-input dark:data-active:bg-input/30 data-active:text-foreground",
|
|
60
|
+
"after:bg-foreground after:absolute after:opacity-0 after:transition-opacity group-data-horizontal/tabs:after:inset-x-0 group-data-horizontal/tabs:after:bottom-[-5px] group-data-horizontal/tabs:after:h-0.5 group-data-vertical/tabs:after:inset-y-0 group-data-vertical/tabs:after:-right-1 group-data-vertical/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-active:after:opacity-100",
|
|
61
|
+
className,
|
|
62
|
+
)}
|
|
63
|
+
{...props}
|
|
64
|
+
/>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function TabsContent({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Content>) {
|
|
69
|
+
return (
|
|
70
|
+
<TabsPrimitive.Content
|
|
71
|
+
data-slot="tabs-content"
|
|
72
|
+
className={cn("text-sm flex-1 outline-none", className)}
|
|
73
|
+
{...props}
|
|
74
|
+
/>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"style": "new-york",
|
|
3
|
+
"rsc": true,
|
|
4
|
+
"tailwind": {
|
|
5
|
+
"config": "",
|
|
6
|
+
"css": "styles/globals.css",
|
|
7
|
+
"baseColor": "neutral",
|
|
8
|
+
"cssVariables": true
|
|
9
|
+
},
|
|
10
|
+
"iconLibrary": "lucide",
|
|
11
|
+
"aliases": {
|
|
12
|
+
"components": "@/components",
|
|
13
|
+
"utils": "@/lib/utils",
|
|
14
|
+
"ui": "@/components/ui",
|
|
15
|
+
"lib": "@/lib",
|
|
16
|
+
"hooks": "@/hooks"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// Dashboard Data Types
|
|
2
|
+
export interface DashboardMetrics {
|
|
3
|
+
totalProperties: number;
|
|
4
|
+
unitsAvailable: number;
|
|
5
|
+
occupiedUnits: number;
|
|
6
|
+
topMaintenanceIssue: string;
|
|
7
|
+
topMaintenanceIssueCount: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface Application {
|
|
11
|
+
id: string;
|
|
12
|
+
applicantName: string;
|
|
13
|
+
propertyAddress: string;
|
|
14
|
+
submittedDate: string;
|
|
15
|
+
status: "pending" | "approved" | "rejected";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface MaintenanceRequest {
|
|
19
|
+
id: string;
|
|
20
|
+
propertyAddress: string;
|
|
21
|
+
issueType: string;
|
|
22
|
+
priority: "emergency" | "high" | "medium" | "low";
|
|
23
|
+
status: "new" | "assigned" | "scheduled" | "in_progress" | "completed";
|
|
24
|
+
assignedWorker?: string;
|
|
25
|
+
scheduledDateTime?: string;
|
|
26
|
+
description: string;
|
|
27
|
+
tenantName?: string;
|
|
28
|
+
imageUrl?: string;
|
|
29
|
+
tenantUnit?: string;
|
|
30
|
+
assignedWorkerName?: string;
|
|
31
|
+
assignedWorkerOrg?: string;
|
|
32
|
+
formattedDate?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface Property {
|
|
36
|
+
id: string;
|
|
37
|
+
name: string;
|
|
38
|
+
address: string;
|
|
39
|
+
type: "apartment" | "house" | "commercial";
|
|
40
|
+
status: "available" | "rented" | "maintenance";
|
|
41
|
+
monthlyRent: number;
|
|
42
|
+
bedrooms?: number;
|
|
43
|
+
bathrooms?: number;
|
|
44
|
+
heroImage?: string;
|
|
45
|
+
description?: string;
|
|
46
|
+
sqFt?: number;
|
|
47
|
+
yearBuilt?: number;
|
|
48
|
+
deposit?: number;
|
|
49
|
+
parking?: number;
|
|
50
|
+
petFriendly?: boolean;
|
|
51
|
+
availableDate?: string;
|
|
52
|
+
leaseTerm?: number;
|
|
53
|
+
features?: string[];
|
|
54
|
+
utilities?: string[];
|
|
55
|
+
tourUrl?: string;
|
|
56
|
+
createdDate?: string;
|
|
57
|
+
}
|
|
@@ -1,12 +1,165 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { StatCard } from "../components/StatCard.js";
|
|
3
|
+
import { IssuesDonutChart } from "../components/IssuesDonutChart.js";
|
|
4
|
+
import { MaintenanceTable } from "../components/MaintenanceTable.js";
|
|
5
|
+
import { getDashboardMetrics, calculateMetrics } from "../api/dashboard.js";
|
|
6
|
+
import { getMaintenanceRequests } from "../api/maintenance.js";
|
|
7
|
+
import type { DashboardMetrics, MaintenanceRequest } from "../lib/types.js";
|
|
8
|
+
|
|
1
9
|
export default function Home() {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
const [metrics, setMetrics] = useState<DashboardMetrics>({
|
|
11
|
+
totalProperties: 0,
|
|
12
|
+
unitsAvailable: 0,
|
|
13
|
+
occupiedUnits: 0,
|
|
14
|
+
topMaintenanceIssue: "",
|
|
15
|
+
topMaintenanceIssueCount: 0,
|
|
16
|
+
});
|
|
17
|
+
const [maintenanceRequests, setMaintenanceRequests] = useState<MaintenanceRequest[]>([]);
|
|
18
|
+
const [loading, setLoading] = useState(true);
|
|
19
|
+
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
loadDashboardData();
|
|
22
|
+
}, []);
|
|
23
|
+
|
|
24
|
+
const loadDashboardData = async () => {
|
|
25
|
+
try {
|
|
26
|
+
setLoading(true);
|
|
27
|
+
|
|
28
|
+
// Load metrics
|
|
29
|
+
const { properties } = await getDashboardMetrics();
|
|
30
|
+
setMetrics(calculateMetrics(properties));
|
|
31
|
+
|
|
32
|
+
// Load maintenance requests
|
|
33
|
+
const maintenanceData = await getMaintenanceRequests(5);
|
|
34
|
+
setMaintenanceRequests(maintenanceData);
|
|
35
|
+
} catch (error) {
|
|
36
|
+
console.error("Error loading dashboard data:", error);
|
|
37
|
+
} finally {
|
|
38
|
+
setLoading(false);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const handleViewMaintenance = (id: string) => {
|
|
43
|
+
console.log("View maintenance request:", id);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// Calculate chart data from maintenance requests
|
|
47
|
+
const calculateChartData = () => {
|
|
48
|
+
const issueCounts: Record<string, number> = {
|
|
49
|
+
Plumbing: 0,
|
|
50
|
+
HVAC: 0,
|
|
51
|
+
Electrical: 0,
|
|
52
|
+
Other: 0,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
maintenanceRequests.forEach((request) => {
|
|
56
|
+
const type = request.issueType;
|
|
57
|
+
if (type === "Plumbing" || type === "HVAC" || type === "Electrical") {
|
|
58
|
+
issueCounts[type]++;
|
|
59
|
+
} else {
|
|
60
|
+
issueCounts.Other++;
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
return [
|
|
65
|
+
{ name: "Plumbing", value: issueCounts.Plumbing, color: "#7C3AED" },
|
|
66
|
+
{ name: "HVAC", value: issueCounts.HVAC, color: "#EC4899" },
|
|
67
|
+
{ name: "Electrical", value: issueCounts.Electrical, color: "#14B8A6" },
|
|
68
|
+
{ name: "Other", value: issueCounts.Other, color: "#06B6D4" },
|
|
69
|
+
];
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// Calculate previous month's data (mock for now)
|
|
73
|
+
const getPreviousMetrics = () => ({
|
|
74
|
+
totalProperties: 12,
|
|
75
|
+
unitsAvailable: 78,
|
|
76
|
+
occupiedUnits: 422,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const previousMetrics = getPreviousMetrics();
|
|
80
|
+
|
|
81
|
+
if (loading) {
|
|
82
|
+
return (
|
|
83
|
+
<div className="flex items-center justify-center h-screen bg-gray-50">
|
|
84
|
+
<div className="text-lg text-gray-600">Loading dashboard...</div>
|
|
85
|
+
</div>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<div className="min-h-screen bg-gray-50">
|
|
91
|
+
{/* Main Content Area */}
|
|
92
|
+
<div className="max-w-7xl mx-auto p-8 space-y-6">
|
|
93
|
+
{/* Search Bar (Desktop) */}
|
|
94
|
+
<div className="hidden md:flex justify-end mb-4">
|
|
95
|
+
<div className="w-96 bg-white rounded-full px-6 py-3 shadow-sm border border-gray-200 flex items-center">
|
|
96
|
+
<input
|
|
97
|
+
type="text"
|
|
98
|
+
placeholder="Search"
|
|
99
|
+
className="flex-1 outline-none text-gray-600"
|
|
100
|
+
disabled
|
|
101
|
+
/>
|
|
102
|
+
<svg
|
|
103
|
+
className="w-5 h-5 text-gray-400"
|
|
104
|
+
fill="none"
|
|
105
|
+
stroke="currentColor"
|
|
106
|
+
viewBox="0 0 24 24"
|
|
107
|
+
>
|
|
108
|
+
<path
|
|
109
|
+
strokeLinecap="round"
|
|
110
|
+
strokeLinejoin="round"
|
|
111
|
+
strokeWidth={2}
|
|
112
|
+
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
|
113
|
+
/>
|
|
114
|
+
</svg>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
{/* Main Layout: 70/30 split */}
|
|
119
|
+
<div className="grid grid-cols-1 lg:grid-cols-[70%_30%] gap-6">
|
|
120
|
+
{/* Left Column: Stat Cards + Maintenance Table */}
|
|
121
|
+
<div className="space-y-6">
|
|
122
|
+
{/* Stat Cards Row */}
|
|
123
|
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
124
|
+
<StatCard
|
|
125
|
+
title="Total Properties"
|
|
126
|
+
value={metrics.totalProperties}
|
|
127
|
+
trend={{
|
|
128
|
+
value: 20,
|
|
129
|
+
isPositive: true,
|
|
130
|
+
}}
|
|
131
|
+
subtitle={`Last month total ${previousMetrics.totalProperties}`}
|
|
132
|
+
/>
|
|
133
|
+
<StatCard
|
|
134
|
+
title="Units Available"
|
|
135
|
+
value={metrics.unitsAvailable}
|
|
136
|
+
trend={{
|
|
137
|
+
value: 10,
|
|
138
|
+
isPositive: false,
|
|
139
|
+
}}
|
|
140
|
+
subtitle={`Last month total ${previousMetrics.unitsAvailable}/${metrics.totalProperties}`}
|
|
141
|
+
/>
|
|
142
|
+
<StatCard
|
|
143
|
+
title="Occupied Units"
|
|
144
|
+
value={metrics.occupiedUnits}
|
|
145
|
+
trend={{
|
|
146
|
+
value: 5,
|
|
147
|
+
isPositive: true,
|
|
148
|
+
}}
|
|
149
|
+
subtitle={`Last month total ${previousMetrics.occupiedUnits}`}
|
|
150
|
+
/>
|
|
151
|
+
</div>
|
|
152
|
+
|
|
153
|
+
{/* Maintenance Requests Table */}
|
|
154
|
+
<MaintenanceTable requests={maintenanceRequests} onView={handleViewMaintenance} />
|
|
155
|
+
</div>
|
|
156
|
+
|
|
157
|
+
{/* Right Column: Donut Chart */}
|
|
158
|
+
<div>
|
|
159
|
+
<IssuesDonutChart data={calculateChartData()} />
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
);
|
|
12
165
|
}
|
package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/pages/Maintenance.tsx
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { ChevronDown } from "lucide-react";
|
|
3
|
+
import { getAllMaintenanceRequests } from "../api/maintenance.js";
|
|
4
|
+
import type { MaintenanceRequest } from "../lib/types.js";
|
|
5
|
+
import { UserAvatar } from "../components/UserAvatar.js";
|
|
6
|
+
import { PriorityBadge } from "../components/PriorityBadge.js";
|
|
7
|
+
import { StatusBadge } from "../components/StatusBadge.js";
|
|
8
|
+
|
|
9
|
+
export default function Maintenance() {
|
|
10
|
+
const [requests, setRequests] = useState<MaintenanceRequest[]>([]);
|
|
11
|
+
const [loading, setLoading] = useState(true);
|
|
12
|
+
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
loadMaintenanceRequests();
|
|
15
|
+
}, []);
|
|
16
|
+
|
|
17
|
+
const loadMaintenanceRequests = async () => {
|
|
18
|
+
try {
|
|
19
|
+
setLoading(true);
|
|
20
|
+
const data = await getAllMaintenanceRequests();
|
|
21
|
+
setRequests(data);
|
|
22
|
+
} catch (error) {
|
|
23
|
+
console.error("Error loading maintenance requests:", error);
|
|
24
|
+
} finally {
|
|
25
|
+
setLoading(false);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
if (loading) {
|
|
30
|
+
return (
|
|
31
|
+
<div className="flex items-center justify-center h-screen bg-gray-50">
|
|
32
|
+
<div className="text-lg text-gray-600">Loading maintenance requests...</div>
|
|
33
|
+
</div>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<div className="min-h-screen bg-gray-50 p-8">
|
|
39
|
+
<div className="max-w-7xl mx-auto">
|
|
40
|
+
{/* Table Header */}
|
|
41
|
+
<div className="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
|
|
42
|
+
<div className="grid grid-cols-12 gap-4 px-6 py-4 bg-gray-50 border-b border-gray-200">
|
|
43
|
+
<div className="col-span-4 flex items-center gap-2">
|
|
44
|
+
<span className="text-sm font-semibold text-purple-700 uppercase tracking-wide">
|
|
45
|
+
Maintenance Task
|
|
46
|
+
</span>
|
|
47
|
+
<ChevronDown className="w-4 h-4 text-purple-700" />
|
|
48
|
+
</div>
|
|
49
|
+
<div className="col-span-2">
|
|
50
|
+
<span className="text-sm font-semibold text-purple-700 uppercase tracking-wide">
|
|
51
|
+
Tenant Unit
|
|
52
|
+
</span>
|
|
53
|
+
</div>
|
|
54
|
+
<div className="col-span-2">
|
|
55
|
+
<span className="text-sm font-semibold text-purple-700 uppercase tracking-wide">
|
|
56
|
+
Assigned to
|
|
57
|
+
</span>
|
|
58
|
+
</div>
|
|
59
|
+
<div className="col-span-2">
|
|
60
|
+
<span className="text-sm font-semibold text-purple-700 uppercase tracking-wide">
|
|
61
|
+
Date
|
|
62
|
+
</span>
|
|
63
|
+
</div>
|
|
64
|
+
<div className="col-span-2">
|
|
65
|
+
<span className="text-sm font-semibold text-purple-700 uppercase tracking-wide">
|
|
66
|
+
Status
|
|
67
|
+
</span>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
{/* Table Rows */}
|
|
72
|
+
<div className="divide-y divide-gray-200">
|
|
73
|
+
{requests.length === 0 ? (
|
|
74
|
+
<div className="text-center py-12 text-gray-500">No maintenance requests found</div>
|
|
75
|
+
) : (
|
|
76
|
+
requests.map((request) => (
|
|
77
|
+
<div
|
|
78
|
+
key={request.id}
|
|
79
|
+
className="grid grid-cols-12 gap-4 px-6 py-5 hover:bg-gray-50 transition-colors"
|
|
80
|
+
>
|
|
81
|
+
{/* Maintenance Task */}
|
|
82
|
+
<div className="col-span-4 flex items-center gap-4">
|
|
83
|
+
{/* Task Image */}
|
|
84
|
+
<div className="w-16 h-16 rounded-lg bg-gray-200 flex-shrink-0 overflow-hidden">
|
|
85
|
+
{request.imageUrl ? (
|
|
86
|
+
<img
|
|
87
|
+
src={request.imageUrl}
|
|
88
|
+
alt={request.description}
|
|
89
|
+
className="w-full h-full object-cover"
|
|
90
|
+
/>
|
|
91
|
+
) : (
|
|
92
|
+
<div className="w-full h-full flex items-center justify-center text-gray-400">
|
|
93
|
+
<span className="text-2xl">🔧</span>
|
|
94
|
+
</div>
|
|
95
|
+
)}
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
{/* Task Details */}
|
|
99
|
+
<div className="flex-1 min-w-0">
|
|
100
|
+
<div className="flex items-center gap-2 mb-1">
|
|
101
|
+
<h3 className="font-semibold text-gray-900 truncate">
|
|
102
|
+
{request.description}
|
|
103
|
+
</h3>
|
|
104
|
+
{request.priority && <PriorityBadge priority={request.priority} />}
|
|
105
|
+
</div>
|
|
106
|
+
<p className="text-sm text-gray-500">By Tenant</p>
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
{/* Tenant Unit */}
|
|
111
|
+
<div className="col-span-2 flex items-center">
|
|
112
|
+
<div className="flex items-center gap-3">
|
|
113
|
+
<UserAvatar name={request.tenantName || "Unknown"} size="md" />
|
|
114
|
+
<div className="min-w-0">
|
|
115
|
+
<p className="text-sm font-medium text-gray-900 truncate">
|
|
116
|
+
{request.tenantName || "Unknown"}
|
|
117
|
+
</p>
|
|
118
|
+
<p className="text-sm text-gray-500 truncate">
|
|
119
|
+
{request.tenantUnit || request.propertyAddress}
|
|
120
|
+
</p>
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
|
|
125
|
+
{/* Assigned to */}
|
|
126
|
+
<div className="col-span-2 flex items-center">
|
|
127
|
+
{request.assignedWorkerName ? (
|
|
128
|
+
<div className="flex items-center gap-3">
|
|
129
|
+
<UserAvatar name={request.assignedWorkerName} size="md" />
|
|
130
|
+
<div className="min-w-0">
|
|
131
|
+
<p className="text-sm font-medium text-gray-900 truncate">
|
|
132
|
+
{request.assignedWorkerName}
|
|
133
|
+
</p>
|
|
134
|
+
<p className="text-sm text-gray-500 truncate">
|
|
135
|
+
{request.assignedWorkerOrg}
|
|
136
|
+
</p>
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
) : (
|
|
140
|
+
<span className="text-sm text-gray-400">Unassigned</span>
|
|
141
|
+
)}
|
|
142
|
+
</div>
|
|
143
|
+
|
|
144
|
+
{/* Date */}
|
|
145
|
+
<div className="col-span-2 flex items-center">
|
|
146
|
+
<div className="flex items-center gap-2 text-sm text-gray-700">
|
|
147
|
+
<svg
|
|
148
|
+
className="w-4 h-4"
|
|
149
|
+
fill="none"
|
|
150
|
+
stroke="currentColor"
|
|
151
|
+
viewBox="0 0 24 24"
|
|
152
|
+
>
|
|
153
|
+
<path
|
|
154
|
+
strokeLinecap="round"
|
|
155
|
+
strokeLinejoin="round"
|
|
156
|
+
strokeWidth={2}
|
|
157
|
+
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
|
|
158
|
+
/>
|
|
159
|
+
</svg>
|
|
160
|
+
<span>{request.formattedDate || "Not scheduled"}</span>
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
|
|
164
|
+
{/* Status */}
|
|
165
|
+
<div className="col-span-2 flex items-center">
|
|
166
|
+
<StatusBadge status={request.status} />
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
))
|
|
170
|
+
)}
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
);
|
|
176
|
+
}
|
package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/pages/Properties.tsx
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { getProperties } from "../api/properties.js";
|
|
3
|
+
import type { Property } from "../lib/types.js";
|
|
4
|
+
import { PropertyCard } from "../components/PropertyCard.js";
|
|
5
|
+
import { Button } from "@/components/ui/button";
|
|
6
|
+
|
|
7
|
+
export default function Properties() {
|
|
8
|
+
const [properties, setProperties] = useState<Property[]>([]);
|
|
9
|
+
const [loading, setLoading] = useState(true);
|
|
10
|
+
const [loadingMore, setLoadingMore] = useState(false);
|
|
11
|
+
const [hasNextPage, setHasNextPage] = useState(false);
|
|
12
|
+
const [endCursor, setEndCursor] = useState<string | null>(null);
|
|
13
|
+
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
loadProperties();
|
|
16
|
+
}, []);
|
|
17
|
+
|
|
18
|
+
const loadProperties = async () => {
|
|
19
|
+
try {
|
|
20
|
+
setLoading(true);
|
|
21
|
+
const result = await getProperties(12);
|
|
22
|
+
setProperties(result.properties);
|
|
23
|
+
setHasNextPage(result.pageInfo.hasNextPage);
|
|
24
|
+
setEndCursor(result.pageInfo.endCursor!);
|
|
25
|
+
} catch (error) {
|
|
26
|
+
console.error("Error loading properties:", error);
|
|
27
|
+
} finally {
|
|
28
|
+
setLoading(false);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const loadMoreProperties = async () => {
|
|
33
|
+
if (!endCursor) return;
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
setLoadingMore(true);
|
|
37
|
+
const result = await getProperties(12, endCursor);
|
|
38
|
+
setProperties((prev) => [...prev, ...result.properties]);
|
|
39
|
+
setHasNextPage(result.pageInfo.hasNextPage);
|
|
40
|
+
setEndCursor(result.pageInfo.endCursor!);
|
|
41
|
+
} catch (error) {
|
|
42
|
+
console.error("Error loading more properties:", error);
|
|
43
|
+
} finally {
|
|
44
|
+
setLoadingMore(false);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const handlePropertyClick = (property: Property) => {
|
|
49
|
+
console.log("Property clicked:", property);
|
|
50
|
+
// TODO: Navigate to property details page
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
if (loading) {
|
|
54
|
+
return (
|
|
55
|
+
<div className="flex items-center justify-center h-screen bg-gray-50">
|
|
56
|
+
<div className="text-lg text-gray-600">Loading properties...</div>
|
|
57
|
+
</div>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<div className="min-h-screen bg-gray-50 p-8">
|
|
63
|
+
<div className="max-w-7xl mx-auto">
|
|
64
|
+
{/* Properties Grid */}
|
|
65
|
+
{properties.length === 0 ? (
|
|
66
|
+
<div className="text-center py-12">
|
|
67
|
+
<p className="text-gray-500 text-lg">No properties found</p>
|
|
68
|
+
</div>
|
|
69
|
+
) : (
|
|
70
|
+
<>
|
|
71
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
72
|
+
{properties.map((property) => (
|
|
73
|
+
<PropertyCard key={property.id} property={property} onClick={handlePropertyClick} />
|
|
74
|
+
))}
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
{/* Load More Button */}
|
|
78
|
+
{hasNextPage && (
|
|
79
|
+
<div className="flex justify-center mt-8">
|
|
80
|
+
<Button
|
|
81
|
+
onClick={loadMoreProperties}
|
|
82
|
+
disabled={loadingMore}
|
|
83
|
+
className="px-8 py-3 bg-purple-700 hover:bg-purple-800 text-white rounded-lg font-medium transition-colors"
|
|
84
|
+
>
|
|
85
|
+
{loadingMore ? "Loading..." : "Load More"}
|
|
86
|
+
</Button>
|
|
87
|
+
</div>
|
|
88
|
+
)}
|
|
89
|
+
</>
|
|
90
|
+
)}
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
);
|
|
94
|
+
}
|