@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.
Files changed (58) hide show
  1. package/dist/CHANGELOG.md +16 -0
  2. package/dist/force-app/main/default/data/Property__c.json +2 -2
  3. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/api/dashboard.ts +170 -0
  4. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/api/maintenance.ts +221 -0
  5. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/api/properties.ts +157 -0
  6. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/api/utils.ts +4 -0
  7. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/appLayout.tsx +20 -8
  8. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/assets/icons/appliances.svg +13 -0
  9. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/assets/icons/dashboard.svg +4 -0
  10. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/assets/icons/downgraph.svg +3 -0
  11. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/assets/icons/electrical.svg +41 -0
  12. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/assets/icons/files.svg +7 -0
  13. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/assets/icons/hvac.svg +79 -0
  14. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/assets/icons/maintenance.svg +4 -0
  15. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/assets/icons/pest.svg +5 -0
  16. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/assets/icons/plumbing.svg +7 -0
  17. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/assets/icons/properties.svg +14 -0
  18. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/assets/icons/support.svg +6 -0
  19. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/assets/icons/upgraph.svg +3 -0
  20. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/assets/icons/users.svg +8 -0
  21. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/assets/icons/zen-logo.svg +5 -0
  22. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/AnalyticsTile.tsx +29 -0
  23. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/ApplicationCard.tsx +43 -0
  24. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/IssuesDonutChart.tsx +66 -0
  25. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/MaintenanceRequestCard.tsx +71 -0
  26. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/MaintenanceTable.tsx +110 -0
  27. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/PriorityBadge.tsx +29 -0
  28. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/PropertyCard.tsx +61 -0
  29. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/StatCard.tsx +52 -0
  30. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/StatusBadge.tsx +37 -0
  31. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/TopBar.tsx +72 -0
  32. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/UserAvatar.tsx +35 -0
  33. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/VerticalNav.tsx +54 -0
  34. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/ui/alert.tsx +69 -0
  35. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/ui/button.tsx +67 -0
  36. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/ui/card.tsx +92 -0
  37. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/ui/dialog.tsx +143 -0
  38. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/ui/field.tsx +222 -0
  39. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/ui/index.ts +72 -0
  40. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/ui/input.tsx +19 -0
  41. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/ui/label.tsx +19 -0
  42. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/ui/pagination.tsx +112 -0
  43. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/ui/select.tsx +183 -0
  44. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/ui/separator.tsx +26 -0
  45. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/ui/skeleton.tsx +14 -0
  46. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/ui/spinner.tsx +15 -0
  47. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/ui/table.tsx +87 -0
  48. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/ui/tabs.tsx +78 -0
  49. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components.json +18 -0
  50. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/types.ts +57 -0
  51. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/utils.ts +6 -0
  52. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/pages/Home.tsx +163 -10
  53. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/pages/Maintenance.tsx +176 -0
  54. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/pages/Properties.tsx +94 -0
  55. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/routes.tsx +19 -7
  56. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/styles/global.css +160 -0
  57. package/dist/package.json +1 -1
  58. package/package.json +2 -2
@@ -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 };
@@ -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
+ }
@@ -0,0 +1,6 @@
1
+ import { clsx, type ClassValue } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs));
6
+ }
@@ -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
- return (
3
- <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
4
- <div className="text-center">
5
- <h1 className="text-4xl font-bold text-gray-900 mb-4">Home</h1>
6
- <p className="text-lg text-gray-600 mb-8">
7
- Welcome to your React application.
8
- </p>
9
- </div>
10
- </div>
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
  }
@@ -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
+ }
@@ -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
+ }