@schandlergarcia/sf-web-components 2.3.17 → 2.4.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 (80) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/CLAUDE.md +12 -13
  3. package/README.md +0 -15
  4. package/dist/styles/global.css +44 -57
  5. package/package.json +1 -2
  6. package/scripts/apply-brand.mjs +47 -30
  7. package/scripts/postinstall.mjs +1 -11
  8. package/src/styles/global.css +44 -57
  9. package/brands/engine/PARTNER_HUB_PRD.md +0 -584
  10. package/brands/engine/agentApiConfig.ts +0 -36
  11. package/brands/engine/app/api/graphql-operations-types.ts +0 -11260
  12. package/brands/engine/app/api/graphqlClient.ts +0 -25
  13. package/brands/engine/app/api/partnerQueries.ts +0 -212
  14. package/brands/engine/app/appLayout.tsx +0 -5
  15. package/brands/engine/app/components/AgentPanel.tsx +0 -541
  16. package/brands/engine/app/components/AgentforceConversationClient.tsx +0 -201
  17. package/brands/engine/app/components/Data360Widget.tsx +0 -301
  18. package/brands/engine/app/components/__inherit_AgentforceConversationClient.tsx +0 -3
  19. package/brands/engine/app/components/alerts/status-alert.tsx +0 -49
  20. package/brands/engine/app/components/layouts/card-layout.tsx +0 -29
  21. package/brands/engine/app/components/workspace/CommandCenter.tsx +0 -16
  22. package/brands/engine/app/config/agentApi.ts +0 -36
  23. package/brands/engine/app/data/partner-hub-sample-data.js +0 -297
  24. package/brands/engine/app/features/object-search/__examples__/api/accountSearchService.ts +0 -46
  25. package/brands/engine/app/features/object-search/__examples__/api/query/distinctAccountIndustries.graphql +0 -19
  26. package/brands/engine/app/features/object-search/__examples__/api/query/distinctAccountTypes.graphql +0 -19
  27. package/brands/engine/app/features/object-search/__examples__/api/query/getAccountDetail.graphql +0 -121
  28. package/brands/engine/app/features/object-search/__examples__/api/query/searchAccounts.graphql +0 -51
  29. package/brands/engine/app/features/object-search/__examples__/pages/AccountObjectDetailPage.tsx +0 -357
  30. package/brands/engine/app/features/object-search/__examples__/pages/AccountSearch.tsx +0 -312
  31. package/brands/engine/app/features/object-search/__examples__/pages/Home.tsx +0 -34
  32. package/brands/engine/app/features/object-search/api/objectSearchService.ts +0 -84
  33. package/brands/engine/app/features/object-search/components/ActiveFilters.tsx +0 -89
  34. package/brands/engine/app/features/object-search/components/FilterContext.tsx +0 -83
  35. package/brands/engine/app/features/object-search/components/ObjectBreadcrumb.tsx +0 -66
  36. package/brands/engine/app/features/object-search/components/PaginationControls.tsx +0 -109
  37. package/brands/engine/app/features/object-search/components/SearchBar.tsx +0 -41
  38. package/brands/engine/app/features/object-search/components/SortControl.tsx +0 -143
  39. package/brands/engine/app/features/object-search/components/filters/BooleanFilter.tsx +0 -78
  40. package/brands/engine/app/features/object-search/components/filters/DateFilter.tsx +0 -128
  41. package/brands/engine/app/features/object-search/components/filters/DateRangeFilter.tsx +0 -70
  42. package/brands/engine/app/features/object-search/components/filters/FilterFieldWrapper.tsx +0 -33
  43. package/brands/engine/app/features/object-search/components/filters/MultiSelectFilter.tsx +0 -97
  44. package/brands/engine/app/features/object-search/components/filters/NumericRangeFilter.tsx +0 -163
  45. package/brands/engine/app/features/object-search/components/filters/SearchFilter.tsx +0 -50
  46. package/brands/engine/app/features/object-search/components/filters/SelectFilter.tsx +0 -97
  47. package/brands/engine/app/features/object-search/components/filters/TextFilter.tsx +0 -91
  48. package/brands/engine/app/features/object-search/hooks/useAsyncData.ts +0 -54
  49. package/brands/engine/app/features/object-search/hooks/useCachedAsyncData.ts +0 -184
  50. package/brands/engine/app/features/object-search/hooks/useDebouncedCallback.ts +0 -34
  51. package/brands/engine/app/features/object-search/hooks/useObjectSearchParams.ts +0 -252
  52. package/brands/engine/app/features/object-search/utils/debounce.ts +0 -25
  53. package/brands/engine/app/features/object-search/utils/fieldUtils.ts +0 -29
  54. package/brands/engine/app/features/object-search/utils/filterUtils.ts +0 -404
  55. package/brands/engine/app/features/object-search/utils/sortUtils.ts +0 -38
  56. package/brands/engine/app/hooks/useEngineLiveData.ts +0 -49
  57. package/brands/engine/app/hooks/useEvaAgent.ts +0 -288
  58. package/brands/engine/app/hooks/usePartnerDashboardData.ts +0 -141
  59. package/brands/engine/app/navigationMenu.tsx +0 -80
  60. package/brands/engine/app/pages/AccountObjectDetailPage.tsx +0 -361
  61. package/brands/engine/app/pages/AccountSearch.tsx +0 -305
  62. package/brands/engine/app/pages/BlankDashboard.tsx +0 -15
  63. package/brands/engine/app/pages/DataTest.tsx +0 -78
  64. package/brands/engine/app/pages/Home.tsx +0 -5
  65. package/brands/engine/app/pages/NotFound.tsx +0 -19
  66. package/brands/engine/app/pages/PartnerHubDashboard.tsx +0 -2760
  67. package/brands/engine/app/pages/Search.tsx +0 -13
  68. package/brands/engine/app/router-utils.tsx +0 -35
  69. package/brands/engine/app/routes.tsx +0 -39
  70. package/brands/engine/app/styles/global.css +0 -269
  71. package/brands/engine/brand.css +0 -40
  72. package/brands/engine/engine-command-center-prd.md +0 -575
  73. package/brands/engine/engine-live-data.js +0 -135
  74. package/brands/engine/engine-sample-data.js +0 -378
  75. package/brands/engine/engine_logo.png +0 -0
  76. package/brands/engine/global.css +0 -269
  77. package/brands/engine/partner-hub-sample-data.js +0 -281
  78. package/brands/engine/schema.graphql +0 -292
  79. package/brands/engine/useEngineLiveData.ts +0 -49
  80. package/brands/engine/useEvaAgent.ts +0 -288
@@ -1,2760 +0,0 @@
1
- import { ListCard, ActivityCard, D3Chart, Dropdown, Button, Modal, CardSkeleton, Tabs } from "@/components/library";
2
- import useDataSource from "@/components/library/data/useDataSource";
3
- import { useThemeMode } from "@/components/library/theme/AppThemeProvider";
4
- import { toast } from "sonner";
5
- import React from "react";
6
- import { usePartnerDashboardData } from "@/hooks/usePartnerDashboardData";
7
- import AgentPanel from "@/components/AgentPanel";
8
- import { ENABLE_SAMPLE_DATA_CACHE } from "@/lib/dataStrategy";
9
- import {
10
- PENALTY_TABLE_ITEMS,
11
- DISPUTE_CARDS,
12
- INVOICE_CARDS,
13
- RECENT_ACTIVITY,
14
- REVENUE_TREND_BY_PROPERTY,
15
- PROPERTY_LEADERBOARD,
16
- } from "@/data/partner-hub-sample-data";
17
- import {
18
- BuildingOfficeIcon,
19
- BanknotesIcon,
20
- ExclamationTriangleIcon,
21
- ClockIcon,
22
- ShieldCheckIcon,
23
- MoonIcon,
24
- SunIcon,
25
- UserCircleIcon,
26
- Cog6ToothIcon,
27
- ArrowRightOnRectangleIcon,
28
- TrophyIcon,
29
- StarIcon,
30
- RocketLaunchIcon,
31
- LightBulbIcon,
32
- DocumentArrowDownIcon,
33
- SparklesIcon,
34
- } from "@heroicons/react/24/outline";
35
- import * as d3 from "d3";
36
- import engineLogo from "@/assets/images/engine_logo.png";
37
-
38
- /**
39
- * Partner Hub Dashboard
40
- *
41
- * Partner-facing portal where hotel partners (Marriott, Hilton, etc.) log in
42
- * to view their business relationship with Engine:
43
- * - Their properties and performance
44
- * - Their invoices and payments
45
- * - Their attrition penalties and disputes
46
- * - Their contract details
47
- * - Communication with Engine
48
- */
49
- type TabId = "overview" | "cases" | "properties" | "analytics";
50
-
51
- export default function PartnerHubDashboard() {
52
- const { mode, toggle } = useThemeMode();
53
- const [activeTab, setActiveTab] = React.useState<TabId>("overview");
54
- const [tabLoading, setTabLoading] = React.useState(false);
55
- const tabLoadRef = React.useRef<ReturnType<typeof setTimeout>>();
56
-
57
- const switchTab = React.useCallback((id: TabId) => {
58
- if (id === activeTab) return;
59
- setTabLoading(true);
60
- setActiveTab(id);
61
- clearTimeout(tabLoadRef.current);
62
- tabLoadRef.current = setTimeout(() => setTabLoading(false), 700);
63
- }, [activeTab]);
64
-
65
- React.useEffect(() => () => clearTimeout(tabLoadRef.current), []);
66
-
67
- const [selectedPenalty, setSelectedPenalty] = React.useState(null);
68
- const [isPenaltyModalOpen, setIsPenaltyModalOpen] = React.useState(false);
69
- const [isPropertiesModalOpen, setIsPropertiesModalOpen] = React.useState(false);
70
- const [isRevenueModalOpen, setIsRevenueModalOpen] = React.useState(false);
71
- const [isReservationsModalOpen, setIsReservationsModalOpen] = React.useState(false);
72
- const [isDisputesModalOpen, setIsDisputesModalOpen] = React.useState(false);
73
- const [isInvoicesModalOpen, setIsInvoicesModalOpen] = React.useState(false);
74
-
75
- // Simulated logged-in partner (in real app, this comes from auth context)
76
- const currentPartner = {
77
- name: "Summit Hotels & Resorts",
78
- tier: "Gold",
79
- logo: null, // Could add partner logo here
80
- };
81
-
82
- // Fetch live data from Salesforce
83
- const { data: liveData, loading: liveLoading, error: liveError } = usePartnerDashboardData(
84
- ENABLE_SAMPLE_DATA_CACHE ? null : null // Fetch for current partner
85
- );
86
-
87
- // Determine if we're loading (only in live mode)
88
- const isLoading = !ENABLE_SAMPLE_DATA_CACHE && liveLoading;
89
- const showSkeleton = isLoading || tabLoading;
90
-
91
- // Show error toast if live data fails
92
- React.useEffect(() => {
93
- if (!ENABLE_SAMPLE_DATA_CACHE && liveError) {
94
- toast.error(`Failed to load data: ${liveError.message}`);
95
- console.error("Live Data Error:", liveError);
96
- }
97
- }, [liveError]);
98
-
99
- // Log live data status
100
- React.useEffect(() => {
101
- if (!ENABLE_SAMPLE_DATA_CACHE) {
102
- console.log("Live Data Mode - Loading:", liveLoading);
103
- console.log("Live Data Mode - Error:", liveError);
104
- console.log("Live Data Mode - Data:", liveData);
105
- }
106
- }, [liveLoading, liveError, liveData]);
107
-
108
- // Transform live data to match sample data format
109
- const transformLiveData = React.useCallback((liveData: any) => {
110
- if (!liveData) return null;
111
-
112
- // Transform penalties to match expected format
113
- const penalties = (liveData.penalties || []).map((p: any) => ({
114
- id: p.Id,
115
- name: p.Name?.value || p.Name,
116
- partner: currentPartner.name,
117
- property: p.Property__r?.Property_Name__c?.value || p.Property__r?.Name?.value || "Unknown Property",
118
- customer: p.Customer_Company__r?.Name?.value || "Unknown Customer",
119
- status: p.Penalty_Status__c?.value || "Unknown",
120
- penalty: p.Final_Penalty_Amount__c?.value || 0,
121
- credit: p.Resale_Credit_Applied__c?.value || 0,
122
- method: "Per Night", // From contract
123
- isHero: p.Resale_Credit_Applied__c?.value === 0 && (p.Rooms_Resold__c?.value || 0) > 0,
124
- originalRoomBlock: p.Original_Room_Block__c?.value || 0,
125
- actualRoomsUsed: p.Actual_Rooms_Used__c?.value || 0,
126
- unusedRooms: p.Unused_Rooms__c?.value || 0,
127
- roomRate: p.Room_Rate__c?.value || 0,
128
- numberOfNights: p.Number_of_Nights__c?.value || 0,
129
- roomsResold: p.Rooms_Resold__c?.value || 0,
130
- penaltyCalculated: p.Penalty_Amount_Calculated__c?.value || 0,
131
- resalePolicy: "Partial Credit (50%)", // From contract
132
- }));
133
-
134
- // Transform disputes to match expected format
135
- const disputes = (liveData.disputes || []).map((d: any) => ({
136
- id: d.Id,
137
- title: d.Subject?.value || "Dispute",
138
- description: `${currentPartner.name} · ${d.Dispute_Type__c?.value || "General"}`,
139
- status: d.Status === "Open" || d.Status === "Escalated" ? "critical" : d.Priority === "High" ? "critical" : d.Priority === "Medium" ? "warning" : "info",
140
- badge: d.Status?.value || d.Status || "Open",
141
- amount: d.Disputed_Amount__c?.value || 0,
142
- agentHandled: d.Agent_Handled__c?.value || false,
143
- }));
144
-
145
- // Transform invoices to match expected format
146
- const invoices = (liveData.invoices || []).filter((i: any) => {
147
- const status = i.Invoice_Status__c?.value || i.Invoice_Status__c;
148
- return status !== "Paid";
149
- }).map((i: any) => ({
150
- id: i.Id,
151
- title: `${i.Name?.value || i.Name} - ${currentPartner.name}`,
152
- description: `${i.Invoice_Period_Start__c?.value || ""} to ${i.Invoice_Period_End__c?.value || ""}`,
153
- status: (i.Invoice_Status__c?.value || i.Invoice_Status__c) === "Overdue" ? "critical" : "default",
154
- badge: i.Invoice_Status__c?.value || i.Invoice_Status__c || "Draft",
155
- amount: i.Invoice_Total__c?.value || 0,
156
- due: i.Due_Date__c?.value || "",
157
- }));
158
-
159
- // Transform activity
160
- const activity = [
161
- ...disputes.slice(0, 2).map((d: any) => ({
162
- id: `dispute-${d.id}`,
163
- title: "Dispute Created",
164
- description: d.title,
165
- status: "alert",
166
- timestamp: "Recently",
167
- partner: currentPartner.name,
168
- })),
169
- ...invoices.slice(0, 2).map((i: any) => ({
170
- id: `invoice-${i.id}`,
171
- title: "Invoice Sent",
172
- description: `${i.title} ($${i.amount.toLocaleString()})`,
173
- status: i.status === "critical" ? "warning" : "info",
174
- timestamp: "Recently",
175
- partner: currentPartner.name,
176
- })),
177
- ];
178
-
179
- return {
180
- penalties,
181
- disputes,
182
- invoices,
183
- activity,
184
- };
185
- }, [currentPartner.name]);
186
-
187
- // Load data - use live data if available, otherwise sample
188
- const transformedLiveData = React.useMemo(() => {
189
- if (!ENABLE_SAMPLE_DATA_CACHE && liveData) {
190
- return transformLiveData(liveData);
191
- }
192
- return null;
193
- }, [liveData, transformLiveData]);
194
-
195
- const allPenalties = useDataSource({
196
- sample: PENALTY_TABLE_ITEMS,
197
- live: transformedLiveData?.penalties || []
198
- });
199
- const allDisputes = useDataSource({
200
- sample: DISPUTE_CARDS,
201
- live: transformedLiveData?.disputes || []
202
- });
203
- const allInvoices = useDataSource({
204
- sample: INVOICE_CARDS,
205
- live: transformedLiveData?.invoices || []
206
- });
207
- const allActivity = useDataSource({
208
- sample: RECENT_ACTIVITY,
209
- live: transformedLiveData?.activity || []
210
- });
211
-
212
- // Calculate revenue trend from invoices
213
- const revenueTrendByProperty = React.useMemo(() => {
214
- if (ENABLE_SAMPLE_DATA_CACHE) {
215
- return REVENUE_TREND_BY_PROPERTY;
216
- }
217
-
218
- if (!liveData?.invoices || liveData.invoices.length === 0 || !liveData?.properties) {
219
- return REVENUE_TREND_BY_PROPERTY;
220
- }
221
-
222
- // Group invoices by month and calculate totals
223
- const monthlyData = new Map();
224
- liveData.invoices.forEach((inv: any) => {
225
- const periodStart = inv.Invoice_Period_Start__c?.value;
226
- if (periodStart) {
227
- const date = new Date(periodStart);
228
- const monthKey = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
229
- const monthLabel = date.toLocaleDateString('en-US', { month: 'short', year: 'numeric' });
230
- const amount = inv.Invoice_Total__c?.value || 0;
231
-
232
- if (!monthlyData.has(monthKey)) {
233
- monthlyData.set(monthKey, { key: monthKey, label: monthLabel, total: 0 });
234
- }
235
- monthlyData.get(monthKey).total += amount;
236
- }
237
- });
238
-
239
- // Sort by date and convert to array
240
- const sortedMonths = Array.from(monthlyData.values())
241
- .sort((a, b) => a.key.localeCompare(b.key));
242
-
243
- const months = sortedMonths.map(m => m.label);
244
- const colors = ["var(--color-dash-accent)", "var(--color-dash-success)", "var(--color-dash-chart-3)", "var(--color-dash-chart-4)"];
245
-
246
- // Distribute monthly revenue across properties (weighted)
247
- const properties = (liveData.properties || []).map((prop: any, idx: number) => {
248
- const weights = [0.35, 0.28, 0.22, 0.15];
249
- const weight = weights[idx] || 0.10;
250
-
251
- return {
252
- name: prop.Property_Name__c?.value || prop.Name?.value || `Property ${idx + 1}`,
253
- color: colors[idx] || "#999",
254
- values: sortedMonths.map(month => Math.floor(month.total * weight))
255
- };
256
- });
257
-
258
- return { months, properties };
259
- }, [liveData?.invoices, liveData?.properties]);
260
-
261
- // Calculate total revenue first (needed by leaderboard)
262
- const myRevenue = React.useMemo(() => {
263
- if (ENABLE_SAMPLE_DATA_CACHE) {
264
- return 283000;
265
- }
266
- return (liveData?.invoices || []).reduce((sum: number, inv: any) => sum + (inv.Invoice_Total__c?.value || 0), 0);
267
- }, [liveData?.invoices]);
268
-
269
- // Calculate property leaderboard from real data
270
- const propertyLeaderboard = React.useMemo(() => {
271
- if (ENABLE_SAMPLE_DATA_CACHE) {
272
- return PROPERTY_LEADERBOARD;
273
- }
274
-
275
- if (!liveData?.properties || liveData.properties.length === 0) {
276
- return [];
277
- }
278
-
279
- // For demo purposes, distribute total revenue across properties
280
- // In a real system, you'd have property-level revenue tracking
281
- const totalRevenue = myRevenue;
282
- const properties = liveData.properties;
283
-
284
- return properties.map((prop: any, idx: number) => {
285
- // Distribute revenue with weighted randomness for demo
286
- const weights = [0.35, 0.28, 0.22, 0.15]; // First property gets most
287
- const weight = weights[idx] || 0.10;
288
- const propRevenue = Math.floor(totalRevenue * weight);
289
- const latestRevenue = Math.floor(propRevenue * 0.20); // 20% in latest period
290
- const growthRates = [60, 55, 75, 100];
291
- const growth = growthRates[idx] || 50;
292
-
293
- return {
294
- name: prop.Property_Name__c?.value || prop.Name?.value || `Property ${idx + 1}`,
295
- city: prop.City__c?.value || "",
296
- state: prop.State__c?.value || "",
297
- revenue: propRevenue,
298
- latestRevenue: latestRevenue,
299
- growth: growth,
300
- insight: idx === 0 ? `Highest booking volume through Engine, ${Math.round(weight * 100)}% of total` :
301
- idx === properties.length - 1 ? `Bookings doubled since October, ${growth}% growth` :
302
- `Strong booking growth in Q1 2026`
303
- };
304
- }).sort((a, b) => b.revenue - a.revenue);
305
- }, [liveData?.properties, myRevenue]);
306
-
307
- // Filter data for current partner (sample data needs filtering, live data is already filtered)
308
- const myPenalties = ENABLE_SAMPLE_DATA_CACHE
309
- ? allPenalties.filter((p: any) => p.partner === currentPartner.name)
310
- : allPenalties;
311
- const myDisputes = ENABLE_SAMPLE_DATA_CACHE
312
- ? allDisputes.filter((d: any) => d.description.includes(currentPartner.name))
313
- : allDisputes;
314
- const myInvoices = ENABLE_SAMPLE_DATA_CACHE
315
- ? allInvoices.filter((i: any) => i.title.includes(currentPartner.name))
316
- : allInvoices;
317
- const myActivity = ENABLE_SAMPLE_DATA_CACHE
318
- ? allActivity.filter((a: any) => a.partner === currentPartner.name)
319
- : allActivity;
320
-
321
- // Calculate partner-specific metrics
322
- const myProperties = ENABLE_SAMPLE_DATA_CACHE
323
- ? 4 // Sample data
324
- : (liveData?.partner?.Total_Properties__c?.value || liveData?.properties?.length || 0);
325
- const myReservations = ENABLE_SAMPLE_DATA_CACHE
326
- ? 10 // Sample data
327
- : (liveData?.partner?.Total_Reservations__c?.value || 0);
328
- const myOpenDisputes = myDisputes.length;
329
- const myPendingInvoices = myInvoices.length;
330
-
331
- // Custom horizontal bar chart renderer (unused for now, but kept for future use)
332
- const renderHorizontalBarChart = (svgEl: any, data: any, { width, height }: any, options: any = {}) => {
333
- const margin = options.margin || { top: 10, right: 30, bottom: 30, left: 150 };
334
- const innerWidth = width - margin.left - margin.right;
335
- const innerHeight = height - margin.top - margin.bottom;
336
-
337
- const svg = d3.select(svgEl);
338
- svg.selectAll("*").remove();
339
-
340
- const g = svg
341
- .append("g")
342
- .attr("transform", `translate(${margin.left},${margin.top})`);
343
-
344
- const x = d3
345
- .scaleLinear()
346
- .domain([0, d3.max(data, (d) => d.value) || 0])
347
- .range([0, innerWidth]);
348
-
349
- const y = d3
350
- .scaleBand()
351
- .domain(data.map((d) => d.label))
352
- .range([0, innerHeight])
353
- .padding(0.2);
354
-
355
- // Bars with gradient
356
- const gradient = svg.append("defs")
357
- .append("linearGradient")
358
- .attr("id", "barGradient")
359
- .attr("x1", "0%")
360
- .attr("x2", "100%");
361
-
362
- gradient.append("stop")
363
- .attr("offset", "0%")
364
- .attr("stop-color", "var(--color-dash-accent)");
365
-
366
- gradient.append("stop")
367
- .attr("offset", "100%")
368
- .attr("stop-color", "var(--color-dash-success)");
369
-
370
- g.selectAll(".bar")
371
- .data(data)
372
- .join("rect")
373
- .attr("class", "bar")
374
- .attr("x", 0)
375
- .attr("y", (d) => y(d.label) || 0)
376
- .attr("width", (d) => x(d.value))
377
- .attr("height", y.bandwidth())
378
- .attr("fill", "url(#barGradient)")
379
- .attr("rx", 4);
380
-
381
- // Y axis
382
- g.append("g")
383
- .call(d3.axisLeft(y))
384
- .selectAll("text")
385
- .style("font-size", "12px")
386
- .style("fill", "currentColor");
387
-
388
- // X axis
389
- g.append("g")
390
- .attr("transform", `translate(0,${innerHeight})`)
391
- .call(d3.axisBottom(x).ticks(5).tickFormat(d3.format("$~s")))
392
- .selectAll("text")
393
- .style("font-size", "11px")
394
- .style("fill", "currentColor");
395
- };
396
-
397
- // Custom donut chart renderer (unused for now, but kept for future use)
398
- const renderDonutChart = (svgEl: any, data: any, { width, height }: any) => {
399
- const svg = d3.select(svgEl);
400
- svg.selectAll("*").remove();
401
-
402
- const radius = Math.min(width, height) / 2 - 20;
403
- const g = svg
404
- .append("g")
405
- .attr("transform", `translate(${width / 2},${height / 2})`);
406
-
407
- const pie = d3.pie().value((d) => d.value);
408
- const arc = d3
409
- .arc()
410
- .innerRadius(radius * 0.6)
411
- .outerRadius(radius);
412
-
413
- const arcs = g
414
- .selectAll(".arc")
415
- .data(pie(data))
416
- .join("g")
417
- .attr("class", "arc");
418
-
419
- arcs
420
- .append("path")
421
- .attr("d", arc)
422
- .attr("fill", (d) => d.data.color)
423
- .attr("stroke", "var(--color-background)")
424
- .attr("stroke-width", 2);
425
-
426
- // Labels
427
- arcs
428
- .append("text")
429
- .attr("transform", (d) => `translate(${arc.centroid(d)})`)
430
- .attr("text-anchor", "middle")
431
- .attr("font-size", "14px")
432
- .attr("font-weight", "600")
433
- .attr("fill", "var(--color-dash-surface)")
434
- .attr("paint-order", "stroke")
435
- .attr("stroke", "rgba(0,0,0,0.3)")
436
- .attr("stroke-width", "2px")
437
- .text((d) => d.data.label);
438
-
439
- arcs
440
- .append("text")
441
- .attr("transform", (d) => {
442
- const [x, y] = arc.centroid(d);
443
- return `translate(${x},${y + 16})`;
444
- })
445
- .attr("text-anchor", "middle")
446
- .attr("font-size", "12px")
447
- .attr("fill", "var(--color-dash-surface)")
448
- .attr("paint-order", "stroke")
449
- .attr("stroke", "rgba(0,0,0,0.3)")
450
- .attr("stroke-width", "2px")
451
- .text((d) => d.data.value);
452
- };
453
-
454
- // Custom area chart renderer for invoice trend (unused for now, but kept for future use)
455
- const renderAreaChart = (svgEl: any, data: any, { width, height }: any) => {
456
- const margin = { top: 20, right: 20, bottom: 30, left: 60 };
457
- const innerWidth = width - margin.left - margin.right;
458
- const innerHeight = height - margin.top - margin.bottom;
459
-
460
- const svg = d3.select(svgEl);
461
- svg.selectAll("*").remove();
462
-
463
- const g = svg
464
- .append("g")
465
- .attr("transform", `translate(${margin.left},${margin.top})`);
466
-
467
- const x = d3
468
- .scaleBand()
469
- .domain(data.map((d) => d.month))
470
- .range([0, innerWidth])
471
- .padding(0.1);
472
-
473
- const y = d3
474
- .scaleLinear()
475
- .domain([0, d3.max(data, (d) => Math.max(d.total, d.commission)) || 0])
476
- .range([innerHeight, 0])
477
- .nice();
478
-
479
- // Area generator
480
- const areaTotal = d3
481
- .area()
482
- .x((d, i) => (x(d.month) || 0) + x.bandwidth() / 2)
483
- .y0(innerHeight)
484
- .y1((d) => y(d.total));
485
-
486
- const areaCommission = d3
487
- .area()
488
- .x((d, i) => (x(d.month) || 0) + x.bandwidth() / 2)
489
- .y0(innerHeight)
490
- .y1((d) => y(d.commission));
491
-
492
- // Total area
493
- g.append("path")
494
- .datum(data)
495
- .attr("fill", "var(--color-dash-accent)")
496
- .attr("fill-opacity", 0.3)
497
- .attr("d", areaTotal);
498
-
499
- // Commission area
500
- g.append("path")
501
- .datum(data)
502
- .attr("fill", "var(--color-dash-success)")
503
- .attr("fill-opacity", 0.5)
504
- .attr("d", areaCommission);
505
-
506
- // Total line
507
- g.append("path")
508
- .datum(data)
509
- .attr("fill", "none")
510
- .attr("stroke", "var(--color-dash-accent)")
511
- .attr("stroke-width", 2)
512
- .attr("d", d3.line()
513
- .x((d) => (x(d.month) || 0) + x.bandwidth() / 2)
514
- .y((d) => y(d.total))
515
- );
516
-
517
- // Commission line
518
- g.append("path")
519
- .datum(data)
520
- .attr("fill", "none")
521
- .attr("stroke", "var(--color-dash-success)")
522
- .attr("stroke-width", 2)
523
- .attr("d", d3.line()
524
- .x((d) => (x(d.month) || 0) + x.bandwidth() / 2)
525
- .y((d) => y(d.commission))
526
- );
527
-
528
- // X axis
529
- g.append("g")
530
- .attr("transform", `translate(0,${innerHeight})`)
531
- .call(d3.axisBottom(x))
532
- .selectAll("text")
533
- .style("font-size", "11px")
534
- .style("fill", "currentColor");
535
-
536
- // Y axis
537
- g.append("g")
538
- .call(d3.axisLeft(y).ticks(5).tickFormat(d3.format("$~s")))
539
- .selectAll("text")
540
- .style("font-size", "11px")
541
- .style("fill", "currentColor");
542
-
543
- // Legend
544
- const legend = svg
545
- .append("g")
546
- .attr("transform", `translate(${width - 180}, 10)`);
547
-
548
- legend
549
- .append("rect")
550
- .attr("width", 12)
551
- .attr("height", 12)
552
- .attr("fill", "var(--color-dash-accent)");
553
-
554
- legend
555
- .append("text")
556
- .attr("x", 18)
557
- .attr("y", 10)
558
- .attr("font-size", "12px")
559
- .attr("fill", "currentColor")
560
- .text("Total Invoiced");
561
-
562
- legend
563
- .append("rect")
564
- .attr("y", 20)
565
- .attr("width", 12)
566
- .attr("height", 12)
567
- .attr("fill", "var(--color-dash-success)");
568
-
569
- legend
570
- .append("text")
571
- .attr("x", 18)
572
- .attr("y", 30)
573
- .attr("font-size", "12px")
574
- .attr("fill", "currentColor")
575
- .text("Commission");
576
- };
577
-
578
- // Multi-line revenue trend by property renderer
579
- const renderRevenueTrendByProperty = React.useCallback((svgEl: any, data: any, { width, height }: any) => {
580
- if (!data || !data.months || !data.properties) {
581
- return;
582
- }
583
-
584
- const { months, properties } = data;
585
- const margin = { top: 20, right: 20, bottom: 40, left: 70 };
586
- const innerWidth = width - margin.left - margin.right;
587
- const innerHeight = height - margin.top - margin.bottom;
588
-
589
- if (innerWidth <= 0 || innerHeight <= 0) {
590
- return;
591
- }
592
-
593
- const svg = d3.select(svgEl);
594
- svg.selectAll("*").remove();
595
-
596
- const g = svg
597
- .append("g")
598
- .attr("transform", `translate(${margin.left},${margin.top})`);
599
-
600
- const x = d3
601
- .scalePoint()
602
- .domain(months)
603
- .range([0, innerWidth])
604
- .padding(0.1);
605
-
606
- const allValues = properties.flatMap((p) => p.values);
607
- const y = d3
608
- .scaleLinear()
609
- .domain([0, (d3.max(allValues) || 0) * 1.1])
610
- .range([innerHeight, 0])
611
- .nice();
612
-
613
- // Subtle grid lines
614
- g.append("g")
615
- .attr("class", "grid")
616
- .call(
617
- d3.axisLeft(y)
618
- .ticks(5)
619
- .tickSize(-innerWidth)
620
- .tickFormat(() => "")
621
- )
622
- .selectAll("line")
623
- .style("stroke", "currentColor")
624
- .style("stroke-opacity", "0.08");
625
- g.select(".grid .domain").remove();
626
-
627
- // Area + line per property
628
- properties.forEach((prop) => {
629
- const areaGen = d3
630
- .area<number>()
631
- .x((_d, i) => x(months[i]) ?? 0)
632
- .y0(innerHeight)
633
- .y1((d) => y(d))
634
- .curve(d3.curveMonotoneX);
635
-
636
- const lineGen = d3
637
- .line<number>()
638
- .x((_d, i) => x(months[i]) ?? 0)
639
- .y((d) => y(d))
640
- .curve(d3.curveMonotoneX);
641
-
642
- g.append("path")
643
- .datum(prop.values)
644
- .attr("fill", prop.color)
645
- .attr("fill-opacity", 0.08)
646
- .attr("d", areaGen);
647
-
648
- g.append("path")
649
- .datum(prop.values)
650
- .attr("fill", "none")
651
- .attr("stroke", prop.color)
652
- .attr("stroke-width", 2.5)
653
- .attr("d", lineGen);
654
-
655
- // Dots at last point
656
- const lastIdx = prop.values.length - 1;
657
- g.append("circle")
658
- .attr("cx", x(months[lastIdx]) ?? 0)
659
- .attr("cy", y(prop.values[lastIdx]))
660
- .attr("r", 4)
661
- .attr("fill", prop.color)
662
- .attr("stroke", "var(--color-background, #fff)")
663
- .attr("stroke-width", 2);
664
- });
665
-
666
- // X axis
667
- g.append("g")
668
- .attr("transform", `translate(0,${innerHeight})`)
669
- .call(d3.axisBottom(x))
670
- .selectAll("text")
671
- .style("font-size", "11px")
672
- .style("fill", "currentColor")
673
- .attr("dy", "1.2em");
674
- g.select(".domain").style("stroke-opacity", "0.2");
675
-
676
- // Y axis
677
- g.append("g")
678
- .call(d3.axisLeft(y).ticks(5).tickFormat(d3.format("$~s")))
679
- .selectAll("text")
680
- .style("font-size", "11px")
681
- .style("fill", "currentColor");
682
- }, []);
683
-
684
- const monthlyTotalRevenue = React.useMemo(() => {
685
- if (!revenueTrendByProperty?.months || !revenueTrendByProperty?.properties) return null;
686
- return revenueTrendByProperty.months.map((month: string, i: number) => ({
687
- month,
688
- total: revenueTrendByProperty.properties.reduce((sum: number, p: any) => sum + p.values[i], 0),
689
- }));
690
- }, [revenueTrendByProperty]);
691
-
692
- const renderTotalRevenueGrowth = React.useCallback((svgEl: any, data: any, { width, height }: any) => {
693
- if (!data || !data.length) return;
694
- const margin = { top: 20, right: 20, bottom: 40, left: 70 };
695
- const innerWidth = width - margin.left - margin.right;
696
- const innerHeight = height - margin.top - margin.bottom;
697
- if (innerWidth <= 0 || innerHeight <= 0) return;
698
-
699
- const svg = d3.select(svgEl);
700
- svg.selectAll("*").remove();
701
- const g = svg.append("g").attr("transform", `translate(${margin.left},${margin.top})`);
702
-
703
- const x = d3.scaleBand().domain(data.map((d: any) => d.month)).range([0, innerWidth]).padding(0.35);
704
- const maxVal = d3.max(data, (d: any) => d.total) || 0;
705
- const y = d3.scaleLinear().domain([0, maxVal * 1.15]).range([innerHeight, 0]).nice();
706
-
707
- g.append("g").attr("class", "grid")
708
- .call(d3.axisLeft(y).ticks(5).tickSize(-innerWidth).tickFormat(() => ""))
709
- .selectAll("line").style("stroke", "currentColor").style("stroke-opacity", "0.08");
710
- g.select(".grid .domain").remove();
711
-
712
- const accentColor = getComputedStyle(document.documentElement).getPropertyValue('--dash-accent').trim() || '#2563eb';
713
-
714
- g.selectAll(".bar").data(data).join("rect")
715
- .attr("x", (d: any) => x(d.month) ?? 0)
716
- .attr("y", (d: any) => y(d.total))
717
- .attr("width", x.bandwidth())
718
- .attr("height", (d: any) => innerHeight - y(d.total))
719
- .attr("rx", 4)
720
- .attr("fill", accentColor)
721
- .attr("fill-opacity", 0.85);
722
-
723
- g.selectAll(".label").data(data).join("text")
724
- .attr("x", (d: any) => (x(d.month) ?? 0) + x.bandwidth() / 2)
725
- .attr("y", (d: any) => y(d.total) - 8)
726
- .attr("text-anchor", "middle")
727
- .style("font-size", "11px")
728
- .style("font-weight", "600")
729
- .style("fill", "currentColor")
730
- .text((d: any) => `$${(d.total / 1000).toFixed(0)}K`);
731
-
732
- g.append("g").attr("transform", `translate(0,${innerHeight})`)
733
- .call(d3.axisBottom(x)).selectAll("text")
734
- .style("font-size", "11px").style("fill", "currentColor").attr("dy", "1.2em");
735
- g.select(".domain").style("stroke-opacity", "0.2");
736
-
737
- g.append("g").call(d3.axisLeft(y).ticks(5).tickFormat(d3.format("$~s")))
738
- .selectAll("text").style("font-size", "11px").style("fill", "currentColor");
739
- }, []);
740
-
741
- return (
742
- <div className="heroui-scope min-h-screen bg-[var(--color-dash-surface)] dark:bg-[var(--color-dash-text)] transition-colors duration-300">
743
- {/* Header - Refined Engine Brand */}
744
- <header className="bg-white/95 dark:bg-[var(--color-dash-text)]/95 backdrop-blur-xl border-b border-[var(--color-dash-label)]/20 dark:border-[var(--color-dash-muted)]/30 sticky top-0 z-50 shadow-sm">
745
- <div className="max-w-[1600px] mx-auto px-8 py-5">
746
- <div className="flex items-center justify-between">
747
- <div className="flex items-center gap-8">
748
- <img
749
- src={engineLogo}
750
- alt="Engine"
751
- className="h-14 w-auto dark:invert dark:brightness-0 dark:contrast-100 transition-all duration-300"
752
- />
753
- <div className="flex items-center gap-4">
754
- <div className="h-10 w-px bg-gradient-to-b from-transparent via-[var(--color-dash-label)]/40 to-transparent" />
755
- <div className="space-y-0.5">
756
- <p className="text-sm font-semibold text-[var(--color-dash-text)] dark:text-white transition-colors">
757
- {currentPartner.name}
758
- </p>
759
- <div className="flex items-center gap-2">
760
- <span className="relative flex h-2 w-2">
761
- <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-[var(--color-dash-success)] opacity-75"></span>
762
- <span className="relative inline-flex rounded-full h-2 w-2 bg-[var(--color-dash-success)]"></span>
763
- </span>
764
- <p className="text-xs font-medium text-[var(--color-dash-label)] uppercase tracking-wide">
765
- {currentPartner.tier} Partner
766
- </p>
767
- </div>
768
- </div>
769
- </div>
770
- </div>
771
- <div className="flex items-center gap-3">
772
- <button
773
- onClick={toggle}
774
- className="group p-3 rounded-xl hover:bg-[var(--color-dash-surface)] dark:hover:bg-[var(--color-dash-muted)]/20 transition-colors duration-200"
775
- aria-label="Toggle theme"
776
- >
777
- {mode === "dark" ? (
778
- <SunIcon className="h-5 w-5 text-[var(--color-dash-accent)] transition-transform group-hover:rotate-45 duration-300" />
779
- ) : (
780
- <MoonIcon className="h-5 w-5 text-[var(--color-dash-muted)] transition-transform group-hover:-rotate-12 duration-300" />
781
- )}
782
- </button>
783
-
784
- <Dropdown>
785
- <Button variant="ghost" size="sm" className="p-2">
786
- <UserCircleIcon className="h-6 w-6 text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]" />
787
- </Button>
788
- <Dropdown.Popover className="min-w-[200px]">
789
- <Dropdown.Menu
790
- className="p-2"
791
- onAction={(key) => {
792
- if (key === "settings") {
793
- toast.info("Opening settings...");
794
- } else if (key === "logout") {
795
- toast.success("Logged out successfully");
796
- }
797
- }}
798
- >
799
- <Dropdown.Item
800
- id="settings"
801
- textValue="Settings"
802
- className="px-3 py-2 rounded-lg hover:bg-[var(--color-dash-surface)] dark:hover:bg-[var(--color-dash-muted)]/20 cursor-pointer"
803
- >
804
- <div className="flex items-center gap-3">
805
- <Cog6ToothIcon className="h-5 w-5 text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]" />
806
- <span className="text-sm font-medium text-[var(--color-dash-text)] dark:text-white">Settings</span>
807
- </div>
808
- </Dropdown.Item>
809
- <Dropdown.Item
810
- id="logout"
811
- textValue="Logout"
812
- variant="danger"
813
- className="px-3 py-2 rounded-lg hover:bg-[var(--color-dash-danger)]/10 cursor-pointer"
814
- >
815
- <div className="flex items-center gap-3">
816
- <ArrowRightOnRectangleIcon className="h-5 w-5 text-[var(--color-dash-danger)]" />
817
- <span className="text-sm font-medium text-[var(--color-dash-danger)]">Logout</span>
818
- </div>
819
- </Dropdown.Item>
820
- </Dropdown.Menu>
821
- </Dropdown.Popover>
822
- </Dropdown>
823
- </div>
824
- </div>
825
- </div>
826
- </header>
827
-
828
- {/* Hero Section - Partnership Overview */}
829
- <div className="relative bg-gradient-to-br from-[var(--color-dash-text)] via-[var(--color-dash-dark)] to-[var(--color-dash-text)] dark:from-[var(--color-dash-text)] dark:via-[var(--color-dash-text)] dark:to-[var(--color-dash-darker)] overflow-hidden">
830
- {/* Subtle background pattern */}
831
- <div className="absolute inset-0 opacity-5">
832
- <div className="absolute inset-0" style={{
833
- backgroundImage: 'radial-gradient(circle at 1px 1px, white 1px, transparent 0)',
834
- backgroundSize: '40px 40px'
835
- }} />
836
- </div>
837
-
838
- {/* Gradient orbs for depth */}
839
- <div className="absolute top-0 right-0 w-96 h-96 bg-[var(--color-dash-accent)]/10 rounded-full blur-3xl" />
840
- <div className="absolute bottom-0 left-0 w-96 h-96 bg-[var(--color-dash-success)]/10 rounded-full blur-3xl" />
841
-
842
- <div className="relative max-w-[1600px] mx-auto px-8 py-12 pb-20">
843
- <div className="max-w-3xl space-y-3 animate-fade-in">
844
- <h1 className="text-3xl lg:text-4xl font-bold text-white tracking-tight leading-tight">
845
- Hey there Jamie! Here's what's happening with your properties
846
- </h1>
847
- <p className="text-lg text-white/70 leading-relaxed">
848
- Track service cases, monitor guest satisfaction, and manage your partnership with Engine, all in one place.
849
- </p>
850
- </div>
851
- </div>
852
- </div>
853
-
854
- {/* Main Content */}
855
- <div className="max-w-[1600px] mx-auto px-8 -mt-12 space-y-10">
856
- {/* Quick Stats - Uniform metrics grid */}
857
- <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-5 animate-slide-up relative z-10">
858
- {/* Open Cases */}
859
- <div
860
- onClick={() => !isLoading && setIsDisputesModalOpen(true)}
861
- className={isLoading ? "" : "cursor-pointer"}
862
- >
863
- <div className="bg-white dark:bg-[var(--color-dash-text)] rounded-xl p-6 shadow-sm hover:shadow-lg transition-all duration-300 border border-[var(--color-dash-warning)]/50 dark:border-[var(--color-dash-warning)]/30 h-full">
864
- {isLoading ? (
865
- <div className="space-y-3">
866
- <div className="flex items-center justify-between">
867
- <div className="bg-[var(--color-dash-label)]/20 rounded-lg h-9 w-9 animate-pulse"></div>
868
- <div className="bg-[var(--color-dash-label)]/20 rounded-full h-5 w-16 animate-pulse"></div>
869
- </div>
870
- <div className="h-3 w-24 bg-[var(--color-dash-label)]/20 rounded animate-pulse"></div>
871
- <div className="h-10 w-32 bg-[var(--color-dash-label)]/20 rounded animate-pulse"></div>
872
- <div className="h-3 w-28 bg-[var(--color-dash-label)]/20 rounded animate-pulse"></div>
873
- </div>
874
- ) : (
875
- <>
876
- <div className="flex items-center justify-between mb-3">
877
- <div className="bg-[var(--color-dash-warning)]/10 rounded-lg p-2">
878
- <ExclamationTriangleIcon className="h-5 w-5 text-[var(--color-dash-warning)]" />
879
- </div>
880
- <span className="inline-flex items-center rounded-full px-2 py-0.5 text-xs font-bold bg-[var(--color-dash-danger)] text-white">
881
- 3 URGENT
882
- </span>
883
- </div>
884
- <p className="text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] text-sm font-semibold mb-2 uppercase tracking-wider">Open Cases</p>
885
- <p className="font-black text-[var(--color-dash-text)] dark:text-white mb-1 leading-tight" style={{ fontSize: 'var(--dash-metric-size)' }}>7</p>
886
- <p className="text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]">across your properties</p>
887
- </>
888
- )}
889
- </div>
890
- </div>
891
-
892
- {/* Guest Satisfaction */}
893
- <div className="cursor-pointer">
894
- <div className="bg-white dark:bg-[var(--color-dash-text)] rounded-xl p-6 shadow-sm hover:shadow-lg transition-all duration-300 border border-[var(--color-dash-label)]/20 dark:border-[var(--color-dash-muted)]/30 h-full">
895
- {isLoading ? (
896
- <CardSkeleton lines={4} />
897
- ) : (
898
- <>
899
- <div className="flex items-center justify-between mb-3">
900
- <div className="bg-[var(--color-dash-success)]/10 rounded-lg p-2">
901
- <StarIcon className="h-5 w-5 text-[var(--color-dash-success)]" />
902
- </div>
903
- <span className="inline-flex items-center rounded-full px-2 py-0.5 text-xs font-bold bg-[var(--color-dash-success)]/10 text-[var(--color-dash-success)]">
904
- +0.3
905
- </span>
906
- </div>
907
- <p className="text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] text-sm font-semibold mb-2 uppercase tracking-wider">Guest Satisfaction</p>
908
- <p className="font-black text-[var(--color-dash-text)] dark:text-white mb-1 leading-tight" style={{ fontSize: 'var(--dash-metric-size)' }}>4.6</p>
909
- <p className="text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]">out of 5.0 average</p>
910
- </>
911
- )}
912
- </div>
913
- </div>
914
-
915
- {/* Properties */}
916
- <div
917
- onClick={() => !isLoading && setIsPropertiesModalOpen(true)}
918
- className={isLoading ? "" : "cursor-pointer"}
919
- >
920
- <div className="bg-white dark:bg-[var(--color-dash-text)] rounded-xl p-6 shadow-sm hover:shadow-lg transition-all duration-300 border border-[var(--color-dash-label)]/20 dark:border-[var(--color-dash-muted)]/30 h-full">
921
- {isLoading ? (
922
- <CardSkeleton lines={4} />
923
- ) : (
924
- <>
925
- <div className="flex items-center justify-between mb-3">
926
- <div className="bg-[var(--color-dash-accent)]/10 rounded-lg p-2">
927
- <BuildingOfficeIcon className="h-5 w-5 text-[var(--color-dash-accent)]" />
928
- </div>
929
- </div>
930
- <p className="text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] text-sm font-semibold mb-2 uppercase tracking-wider">Properties</p>
931
- <p className="font-black text-[var(--color-dash-text)] dark:text-white mb-1 leading-tight" style={{ fontSize: 'var(--dash-metric-size)' }}>{myProperties}</p>
932
- <p className="text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]">active locations</p>
933
- </>
934
- )}
935
- </div>
936
- </div>
937
-
938
- {/* Avg Response Time */}
939
- <div className="cursor-pointer">
940
- <div className="bg-white dark:bg-[var(--color-dash-text)] rounded-xl p-6 shadow-sm hover:shadow-lg transition-all duration-300 border border-[var(--color-dash-label)]/20 dark:border-[var(--color-dash-muted)]/30 h-full">
941
- {isLoading ? (
942
- <CardSkeleton lines={4} />
943
- ) : (
944
- <>
945
- <div className="flex items-center justify-between mb-3">
946
- <div className="bg-[var(--color-dash-info)]/10 rounded-lg p-2">
947
- <ClockIcon className="h-5 w-5 text-[var(--color-dash-info)]" />
948
- </div>
949
- <span className="inline-flex items-center rounded-full px-2 py-0.5 text-xs font-bold bg-[var(--color-dash-success)]/10 text-[var(--color-dash-success)]">
950
- -18%
951
- </span>
952
- </div>
953
- <p className="text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] text-sm font-semibold mb-2 uppercase tracking-wider">Avg Response Time</p>
954
- <p className="font-black text-[var(--color-dash-text)] dark:text-white mb-1 leading-tight" style={{ fontSize: 'var(--dash-metric-size)' }}>2.4h</p>
955
- <p className="text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]">across all cases</p>
956
- </>
957
- )}
958
- </div>
959
- </div>
960
-
961
- {/* SLA Compliance */}
962
- <div className="cursor-pointer">
963
- <div className="bg-white dark:bg-[var(--color-dash-text)] rounded-xl p-6 shadow-sm hover:shadow-lg transition-all duration-300 border border-[var(--color-dash-label)]/20 dark:border-[var(--color-dash-muted)]/30 h-full">
964
- {isLoading ? (
965
- <CardSkeleton lines={4} />
966
- ) : (
967
- <>
968
- <div className="flex items-center justify-between mb-3">
969
- <div className="bg-[var(--color-dash-success)]/10 rounded-lg p-2">
970
- <ShieldCheckIcon className="h-5 w-5 text-[var(--color-dash-success)]" />
971
- </div>
972
- <span className="inline-flex items-center rounded-full px-2 py-0.5 text-xs font-bold bg-[var(--color-dash-success)]/10 text-[var(--color-dash-success)]">
973
- ON TRACK
974
- </span>
975
- </div>
976
- <p className="text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] text-sm font-semibold mb-2 uppercase tracking-wider">SLA Compliance</p>
977
- <p className="font-black text-[var(--color-dash-text)] dark:text-white mb-1 leading-tight" style={{ fontSize: 'var(--dash-metric-size)' }}>94%</p>
978
- <p className="text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]">cases resolved on time</p>
979
- </>
980
- )}
981
- </div>
982
- </div>
983
- </div>
984
-
985
- {/* Tab Navigation */}
986
- <div className="border-b border-[var(--color-dash-label)]/20 dark:border-[var(--color-dash-muted)]/30">
987
- <nav className="flex items-center gap-8">
988
- {([
989
- { id: "overview" as TabId, label: "Overview" },
990
- { id: "cases" as TabId, label: "Cases", count: 7 },
991
- { id: "properties" as TabId, label: "Properties" },
992
- { id: "analytics" as TabId, label: "Analytics" },
993
- ]).map((tab) => (
994
- <button
995
- key={tab.id}
996
- onClick={() => switchTab(tab.id)}
997
- className={`relative pb-3 text-sm font-medium transition-colors whitespace-nowrap ${
998
- activeTab === tab.id
999
- ? "text-[var(--color-dash-text)] dark:text-white"
1000
- : "text-[var(--color-dash-label)] hover:text-[var(--color-dash-muted)] dark:hover:text-white/70"
1001
- }`}
1002
- >
1003
- {tab.label}
1004
- {tab.count ? (
1005
- <span className="ml-1.5 inline-flex items-center justify-center h-5 min-w-[20px] px-1.5 rounded-full text-[10px] font-bold bg-[var(--color-dash-danger)]/10 text-[var(--color-dash-danger)]">
1006
- {tab.count}
1007
- </span>
1008
- ) : null}
1009
- {activeTab === tab.id && (
1010
- <span className="absolute bottom-0 left-0 right-0 h-0.5 bg-[var(--color-dash-text)] dark:bg-white rounded-full" />
1011
- )}
1012
- </button>
1013
- ))}
1014
- </nav>
1015
- </div>
1016
-
1017
- {/* Tab loading skeletons */}
1018
- {tabLoading && (
1019
- <div className="space-y-6 animate-pulse">
1020
- {activeTab === "overview" && <>
1021
- <div className="bg-white dark:bg-[var(--color-dash-text)] rounded-xl border border-[var(--color-dash-label)]/20 dark:border-[var(--color-dash-muted)]/30 p-6 space-y-4">
1022
- <div className="flex items-center gap-2">
1023
- <div className="h-4 w-4 rounded bg-[var(--color-dash-accent)]/20" />
1024
- <div className="h-4 w-20 rounded bg-[var(--color-dash-label)]/20" />
1025
- <div className="h-4 w-16 rounded bg-[var(--color-dash-accent)]/10" />
1026
- </div>
1027
- <div className="space-y-4">
1028
- {[1, 2, 3].map(i => (
1029
- <div key={i} className="flex gap-4 items-start">
1030
- <div className="h-8 w-8 rounded-lg bg-[var(--color-dash-label)]/15" />
1031
- <div className="flex-1 space-y-2">
1032
- <div className="h-4 w-3/4 rounded bg-[var(--color-dash-label)]/20" />
1033
- <div className="h-3 w-full rounded bg-[var(--color-dash-label)]/10" />
1034
- </div>
1035
- </div>
1036
- ))}
1037
- </div>
1038
- </div>
1039
- <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
1040
- {[1, 2].map(i => (
1041
- <div key={i} className="bg-white dark:bg-[var(--color-dash-text)] rounded-2xl p-8 border border-[var(--color-dash-label)]/20 dark:border-[var(--color-dash-muted)]/30 space-y-4">
1042
- <div className="h-6 w-48 rounded bg-[var(--color-dash-label)]/20" />
1043
- <div className="h-4 w-64 rounded bg-[var(--color-dash-label)]/10" />
1044
- {[1, 2, 3, 4].map(j => (
1045
- <div key={j} className="flex items-center gap-3 p-3 rounded-lg bg-[var(--color-dash-surface)]/50 dark:bg-[var(--color-dash-dark)]/50">
1046
- <div className="h-6 w-6 rounded-full bg-[var(--color-dash-label)]/20" />
1047
- <div className="flex-1 space-y-1.5">
1048
- <div className="h-4 w-40 rounded bg-[var(--color-dash-label)]/20" />
1049
- <div className="h-3 w-28 rounded bg-[var(--color-dash-label)]/10" />
1050
- </div>
1051
- <div className="h-5 w-12 rounded-full bg-[var(--color-dash-label)]/15" />
1052
- </div>
1053
- ))}
1054
- </div>
1055
- ))}
1056
- </div>
1057
- </>}
1058
-
1059
- {activeTab === "cases" && <>
1060
- <div className="bg-gradient-to-r from-[var(--color-dash-dark)] to-[var(--color-dash-dark)]/90 rounded-xl p-5 flex gap-4">
1061
- <div className="h-9 w-9 rounded-lg bg-white/10" />
1062
- <div className="flex-1 space-y-2">
1063
- <div className="h-4 w-2/3 rounded bg-white/15" />
1064
- <div className="h-3 w-full rounded bg-white/8" />
1065
- </div>
1066
- </div>
1067
- <div className="grid grid-cols-4 gap-4">
1068
- {[1, 2, 3, 4].map(i => (
1069
- <div key={i} className="bg-white dark:bg-[var(--color-dash-text)] rounded-xl p-5 border border-[var(--color-dash-label)]/20 dark:border-[var(--color-dash-muted)]/30 space-y-2">
1070
- <div className="h-3 w-16 rounded bg-[var(--color-dash-label)]/20" />
1071
- <div className="h-8 w-10 rounded bg-[var(--color-dash-label)]/15" />
1072
- </div>
1073
- ))}
1074
- </div>
1075
- <div className="bg-white dark:bg-[var(--color-dash-text)] rounded-xl border border-[var(--color-dash-label)]/20 dark:border-[var(--color-dash-muted)]/30 overflow-hidden">
1076
- <div className="p-4 space-y-3 bg-[var(--color-dash-surface)]/50 dark:bg-[var(--color-dash-muted)]/10">
1077
- <div className="flex gap-4">
1078
- {[1, 2, 3, 4, 5, 6, 7, 8, 9].map(i => (
1079
- <div key={i} className="h-3 w-16 rounded bg-[var(--color-dash-label)]/20" />
1080
- ))}
1081
- </div>
1082
- </div>
1083
- {[1, 2, 3, 4, 5, 6].map(i => (
1084
- <div key={i} className="px-4 py-3 flex gap-4 border-t border-[var(--color-dash-label)]/10 dark:border-[var(--color-dash-muted)]/20">
1085
- <div className="h-4 w-14 rounded bg-[var(--color-dash-accent)]/15" />
1086
- <div className="h-4 w-48 rounded bg-[var(--color-dash-label)]/15" />
1087
- <div className="h-4 w-32 rounded bg-[var(--color-dash-label)]/10" />
1088
- <div className="h-4 w-20 rounded bg-[var(--color-dash-label)]/10" />
1089
- <div className="h-4 w-12 rounded bg-[var(--color-dash-label)]/10" />
1090
- <div className="h-4 w-16 rounded bg-[var(--color-dash-label)]/10" />
1091
- </div>
1092
- ))}
1093
- </div>
1094
- </>}
1095
-
1096
- {activeTab === "properties" && <>
1097
- <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
1098
- {[1, 2, 3, 4].map(i => (
1099
- <div key={i} className="bg-white dark:bg-[var(--color-dash-text)] rounded-xl border border-[var(--color-dash-label)]/20 dark:border-[var(--color-dash-muted)]/30 p-6 space-y-4">
1100
- <div className="flex items-start justify-between">
1101
- <div className="space-y-2">
1102
- <div className="h-5 w-48 rounded bg-[var(--color-dash-label)]/20" />
1103
- <div className="h-3 w-32 rounded bg-[var(--color-dash-label)]/10" />
1104
- </div>
1105
- <div className="h-6 w-20 rounded-full bg-[var(--color-dash-label)]/15" />
1106
- </div>
1107
- <div className="grid grid-cols-3 gap-4">
1108
- {[1, 2, 3].map(j => (
1109
- <div key={j} className="space-y-1.5">
1110
- <div className="h-3 w-16 rounded bg-[var(--color-dash-label)]/15" />
1111
- <div className="h-6 w-10 rounded bg-[var(--color-dash-label)]/20" />
1112
- </div>
1113
- ))}
1114
- </div>
1115
- <div className="pt-4 border-t border-[var(--color-dash-label)]/10 dark:border-[var(--color-dash-muted)]/20 flex items-center justify-between">
1116
- <div className="space-y-1.5">
1117
- <div className="h-3 w-20 rounded bg-[var(--color-dash-label)]/10" />
1118
- <div className="h-5 w-16 rounded bg-[var(--color-dash-label)]/20" />
1119
- </div>
1120
- <div className="h-5 w-12 rounded-full bg-[var(--color-dash-success)]/15" />
1121
- </div>
1122
- <div className="bg-[var(--color-dash-surface)]/70 dark:bg-[var(--color-dash-dark)]/50 rounded-lg p-3 flex gap-2">
1123
- <div className="h-3.5 w-3.5 rounded bg-[var(--color-dash-accent)]/20 flex-shrink-0" />
1124
- <div className="flex-1 space-y-1.5">
1125
- <div className="h-3 w-full rounded bg-[var(--color-dash-label)]/10" />
1126
- <div className="h-3 w-2/3 rounded bg-[var(--color-dash-label)]/8" />
1127
- </div>
1128
- </div>
1129
- </div>
1130
- ))}
1131
- </div>
1132
- </>}
1133
-
1134
- {activeTab === "analytics" && <>
1135
- <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
1136
- {[1, 2, 3].map(i => (
1137
- <div key={i} className="bg-white dark:bg-[var(--color-dash-text)] rounded-xl border border-[var(--color-dash-label)]/20 dark:border-[var(--color-dash-muted)]/30 p-5 space-y-3">
1138
- <div className="h-3 w-24 rounded bg-[var(--color-dash-label)]/15" />
1139
- <div className="h-8 w-20 rounded bg-[var(--color-dash-label)]/20" />
1140
- <div className="h-3 w-16 rounded bg-[var(--color-dash-label)]/10" />
1141
- </div>
1142
- ))}
1143
- </div>
1144
- <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
1145
- {[1, 2].map(i => (
1146
- <div key={i} className="bg-white dark:bg-[var(--color-dash-text)] rounded-2xl border border-[var(--color-dash-label)]/20 dark:border-[var(--color-dash-muted)]/30 p-8 space-y-4">
1147
- <div className="h-6 w-48 rounded bg-[var(--color-dash-label)]/20" />
1148
- <div className="h-4 w-32 rounded bg-[var(--color-dash-label)]/10" />
1149
- <div className="h-48 w-full rounded-lg bg-[var(--color-dash-surface)]/70 dark:bg-[var(--color-dash-dark)]/40" />
1150
- </div>
1151
- ))}
1152
- </div>
1153
- <div className="bg-white dark:bg-[var(--color-dash-text)] rounded-2xl border border-[var(--color-dash-label)]/20 dark:border-[var(--color-dash-muted)]/30 p-8 space-y-4">
1154
- <div className="h-6 w-56 rounded bg-[var(--color-dash-label)]/20" />
1155
- <div className="h-64 w-full rounded-lg bg-[var(--color-dash-surface)]/70 dark:bg-[var(--color-dash-dark)]/40" />
1156
- </div>
1157
- </>}
1158
-
1159
- {/* data360 skeleton placeholder: added by headless-data-360 skill during demo */}
1160
- </div>
1161
- )}
1162
-
1163
- {/* ============== OVERVIEW TAB ============== */}
1164
- {activeTab === "overview" && !tabLoading && <>
1165
-
1166
- {/* AI Insights */}
1167
- {!isLoading && (
1168
- <div className="bg-white dark:bg-[var(--color-dash-text)] rounded-xl border border-[var(--color-dash-label)]/20 dark:border-[var(--color-dash-muted)]/30 shadow-sm overflow-hidden">
1169
- <div className="px-6 py-4 border-b border-[var(--color-dash-label)]/10 dark:border-[var(--color-dash-muted)]/20 flex items-center gap-2">
1170
- <SparklesIcon className="h-4 w-4 text-[var(--color-dash-accent)]" />
1171
- <h3 className="text-sm font-semibold text-[var(--color-dash-text)] dark:text-white">AI Insights</h3>
1172
- <span className="text-[10px] font-medium px-1.5 py-0.5 rounded bg-[var(--color-dash-accent)]/10 text-[var(--color-dash-accent)]">Agentforce</span>
1173
- </div>
1174
- <div className="divide-y divide-[var(--color-dash-label)]/10 dark:divide-[var(--color-dash-muted)]/20">
1175
- {[
1176
- {
1177
- action: "Launch promotional rate for 40 open rooms at Austin Convention Center",
1178
- reason: "TechCorp canceled their May 19-21 block. Based on Engine network demand patterns, a $159/night promo could fill 70% of rooms and recover $12,287 in revenue.",
1179
- type: "revenue",
1180
- priority: "high",
1181
- },
1182
- {
1183
- action: "Escalate SF Bay response times before SLA breach",
1184
- reason: "San Francisco Bay is averaging 3.2h response time, approaching the 4h SLA limit. Two cases are at risk. Reassigning to a dedicated agent could reduce this by 40%.",
1185
- type: "service",
1186
- priority: "high",
1187
- },
1188
- {
1189
- action: "Review $2,400 resale credit on ATR-00001",
1190
- reason: "Your contract specifies a 50% resale credit for resold rooms. 8 rooms were resold but the credit was not applied. This could reduce your penalty from $9,000 to $6,600.",
1191
- type: "billing",
1192
- priority: "medium",
1193
- },
1194
- ].map((insight, i) => (
1195
- <div key={i} className="px-6 py-4 flex items-start gap-4 hover:bg-[var(--color-dash-surface)]/50 dark:hover:bg-[var(--color-dash-dark)]/30 transition-colors cursor-pointer group">
1196
- <div className={`flex-shrink-0 mt-0.5 h-8 w-8 rounded-lg flex items-center justify-center ${
1197
- insight.priority === "high" ? "bg-[var(--color-dash-danger)]/10" : "bg-[var(--color-dash-warning)]/10"
1198
- }`}>
1199
- <SparklesIcon className={`h-4 w-4 ${
1200
- insight.priority === "high" ? "text-[var(--color-dash-danger)]" : "text-[var(--color-dash-warning)]"
1201
- }`} />
1202
- </div>
1203
- <div className="flex-1 min-w-0">
1204
- <p className="text-sm font-semibold text-[var(--color-dash-text)] dark:text-white mb-1">{insight.action}</p>
1205
- <p className="text-xs text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] leading-relaxed">{insight.reason}</p>
1206
- </div>
1207
- <button className="flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity text-xs font-semibold text-[var(--color-dash-accent)] hover:underline whitespace-nowrap mt-1">
1208
- Take action
1209
- </button>
1210
- </div>
1211
- ))}
1212
- </div>
1213
- </div>
1214
- )}
1215
-
1216
- {/* ===== PROPERTY LEADERBOARD + SERVICE PERFORMANCE (side by side) ===== */}
1217
- <div className="grid grid-cols-1 lg:grid-cols-2 gap-6 animate-slide-up">
1218
- {/* Property Leaderboard (compact) */}
1219
- <div className="bg-white dark:bg-[var(--color-dash-text)] rounded-2xl p-8 shadow-sm border border-[var(--color-dash-label)]/20 dark:border-[var(--color-dash-muted)]/30">
1220
- {isLoading ? (
1221
- <div className="space-y-4">
1222
- <div className="h-8 w-64 bg-[var(--color-dash-label)]/20 rounded animate-pulse"></div>
1223
- <CardSkeleton lines={5} />
1224
- </div>
1225
- ) : (
1226
- <>
1227
- <div className="flex items-center justify-between mb-6">
1228
- <div>
1229
- <h3 className="text-xl font-bold text-[var(--color-dash-text)] dark:text-white">Property Leaderboard</h3>
1230
- <p className="text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] mt-1">Ranked by service quality & revenue (6 months)</p>
1231
- </div>
1232
- </div>
1233
- <div className="space-y-3">
1234
- {propertyLeaderboard.map((property, idx) => {
1235
- const rankLabels = ["1", "2", "3", "4"];
1236
- return (
1237
- <div key={idx} className="flex items-center gap-3 p-3 rounded-lg bg-[var(--color-dash-surface)]/50 dark:bg-[var(--color-dash-dark)]/50 hover:bg-[var(--color-dash-surface)] dark:hover:bg-[var(--color-dash-dark)] transition-colors cursor-pointer">
1238
- <span className="text-sm font-bold flex-shrink-0 w-6 h-6 rounded-full bg-[var(--color-dash-dark)] text-white flex items-center justify-center">{rankLabels[idx]}</span>
1239
- <div className="flex-1 min-w-0">
1240
- <p className="font-semibold text-[var(--color-dash-text)] dark:text-white text-sm truncate">{property.name.replace('Summit ', '')}</p>
1241
- <div className="flex items-center gap-3 text-xs text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] mt-0.5">
1242
- <span className="inline-flex items-center gap-0.5">
1243
- <StarIcon className="h-3 w-3 text-[var(--color-dash-accent)]" />
1244
- <strong className="text-[var(--color-dash-text)] dark:text-white">{property.satisfaction || '4.5'}</strong>
1245
- </span>
1246
- <span>{property.responseTime || '2.4h'} avg</span>
1247
- <span>${(property.revenue / 1000).toFixed(0)}K rev</span>
1248
- </div>
1249
- </div>
1250
- <span className="inline-flex items-center rounded-full px-2 py-0.5 text-xs font-bold bg-[var(--color-dash-success)]/10 text-[var(--color-dash-success)] flex-shrink-0">
1251
- +{property.growth}%
1252
- </span>
1253
- </div>
1254
- );
1255
- })}
1256
- </div>
1257
- <div className="mt-4 p-3 rounded-lg bg-[var(--color-dash-info)]/5 border border-[var(--color-dash-info)]/20">
1258
- <p className="text-xs text-[var(--color-dash-text)] dark:text-white">
1259
- <LightBulbIcon className="h-3.5 w-3.5 inline mr-1 text-[var(--color-dash-info)]" />
1260
- <strong>Austin</strong> leads with 4.8 satisfaction and fastest response time (1.8h).
1261
- </p>
1262
- </div>
1263
- </>
1264
- )}
1265
- </div>
1266
-
1267
- {/* Service Performance by Property */}
1268
- <div className="bg-white dark:bg-[var(--color-dash-text)] rounded-2xl p-8 shadow-sm border border-[var(--color-dash-label)]/20 dark:border-[var(--color-dash-muted)]/30">
1269
- {isLoading ? (
1270
- <div className="space-y-4">
1271
- <div className="h-8 w-64 bg-[var(--color-dash-label)]/20 rounded animate-pulse"></div>
1272
- <CardSkeleton lines={5} />
1273
- </div>
1274
- ) : (
1275
- <>
1276
- <div className="flex items-center justify-between mb-6">
1277
- <div>
1278
- <h3 className="text-xl font-bold text-[var(--color-dash-text)] dark:text-white">Service Performance</h3>
1279
- <p className="text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] mt-1">Guest satisfaction & response times by property</p>
1280
- </div>
1281
- </div>
1282
- <div className="space-y-4">
1283
- {[
1284
- { name: "Summit Austin Convention Center", satisfaction: 4.8, responseTime: "1.8h", cases: 2, trend: "+0.4" },
1285
- { name: "Summit Midtown NYC", satisfaction: 4.7, responseTime: "2.1h", cases: 1, trend: "+0.3" },
1286
- { name: "Summit Chicago Downtown", satisfaction: 4.5, responseTime: "2.6h", cases: 1, trend: "+0.2" },
1287
- { name: "Summit San Francisco Bay", satisfaction: 4.3, responseTime: "3.2h", cases: 3, trend: "+0.5" },
1288
- ].map((prop) => (
1289
- <div key={prop.name} className="flex items-center justify-between p-3 rounded-lg bg-[var(--color-dash-surface)]/50 dark:bg-[var(--color-dash-dark)]/50 hover:bg-[var(--color-dash-surface)] dark:hover:bg-[var(--color-dash-dark)] transition-colors cursor-pointer">
1290
- <div className="flex-1 min-w-0">
1291
- <p className="font-semibold text-[var(--color-dash-text)] dark:text-white text-sm truncate">{prop.name}</p>
1292
- <p className="text-xs text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]">{prop.cases} open cases · Avg {prop.responseTime}</p>
1293
- </div>
1294
- <div className="flex items-center gap-4 ml-4">
1295
- <div className="text-right">
1296
- <div className="flex items-center gap-1">
1297
- <StarIcon className="h-4 w-4 text-[var(--color-dash-accent)]" />
1298
- <span className="font-bold text-[var(--color-dash-text)] dark:text-white text-sm">{prop.satisfaction}</span>
1299
- </div>
1300
- <span className="text-xs text-[var(--color-dash-success)] font-semibold">{prop.trend}</span>
1301
- </div>
1302
- </div>
1303
- </div>
1304
- ))}
1305
- </div>
1306
- <div className="mt-5 p-3 rounded-lg bg-[var(--color-dash-accent)]/5 border border-[var(--color-dash-accent)]/20">
1307
- <div className="flex items-start gap-2">
1308
- <LightBulbIcon className="h-5 w-5 text-[var(--color-dash-accent)] flex-shrink-0 mt-0.5" />
1309
- <p className="text-sm text-[var(--color-dash-text)] dark:text-white">
1310
- <span className="font-semibold">Key Insight:</span> Austin leads in guest satisfaction at 4.8 with the fastest response time (1.8h). SF Bay improved the most (+0.5) but still has room to reduce response times.
1311
- </p>
1312
- </div>
1313
- </div>
1314
- </>
1315
- )}
1316
- </div>
1317
- </div>
1318
-
1319
- {/* Open Cases by Category */}
1320
- <div className="bg-white dark:bg-[var(--color-dash-text)] rounded-2xl p-8 shadow-sm border border-[var(--color-dash-label)]/20 dark:border-[var(--color-dash-muted)]/30 animate-slide-up">
1321
- {isLoading ? (
1322
- <div className="space-y-4">
1323
- <div className="h-8 w-64 bg-[var(--color-dash-label)]/20 rounded animate-pulse"></div>
1324
- <CardSkeleton lines={5} />
1325
- </div>
1326
- ) : (
1327
- <>
1328
- <div className="flex items-center justify-between mb-6">
1329
- <div>
1330
- <h3 className="text-xl font-bold text-[var(--color-dash-text)] dark:text-white">Open Cases by Category</h3>
1331
- <p className="text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] mt-1">7 open cases across your properties</p>
1332
- </div>
1333
- <button className="text-sm font-semibold text-[var(--color-dash-accent)] hover:underline">View All</button>
1334
- </div>
1335
- <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
1336
- {[
1337
- { category: "Billing Disputes", count: 3, color: "var(--color-dash-warning)", urgent: true },
1338
- { category: "Guest Complaints", count: 2, color: "var(--color-dash-danger)", urgent: true },
1339
- { category: "Maintenance Requests", count: 1, color: "var(--color-dash-info)", urgent: false },
1340
- { category: "Booking Modifications", count: 1, color: "var(--color-dash-accent)", urgent: false },
1341
- ].map((item) => (
1342
- <div key={item.category} className="flex items-center gap-3 p-4 rounded-xl bg-[var(--color-dash-surface)]/50 dark:bg-[var(--color-dash-dark)]/50 hover:bg-[var(--color-dash-surface)] dark:hover:bg-[var(--color-dash-dark)] transition-colors cursor-pointer border border-[var(--color-dash-label)]/10 dark:border-[var(--color-dash-muted)]/20">
1343
- <div className="w-1.5 h-10 rounded-full flex-shrink-0" style={{ backgroundColor: item.color }} />
1344
- <div className="flex-1">
1345
- <p className="font-semibold text-[var(--color-dash-text)] dark:text-white text-sm">{item.category}</p>
1346
- <div className="flex items-center gap-2 mt-0.5">
1347
- <span className="text-lg font-bold text-[var(--color-dash-text)] dark:text-white">{item.count}</span>
1348
- {item.urgent && (
1349
- <span className="text-[10px] font-bold px-1.5 py-0.5 rounded-full bg-[var(--color-dash-danger)]/10 text-[var(--color-dash-danger)]">URGENT</span>
1350
- )}
1351
- </div>
1352
- </div>
1353
- </div>
1354
- ))}
1355
- </div>
1356
- </>
1357
- )}
1358
- </div>
1359
-
1360
- {/* ===== SERVICE ACTIVITY (moved up for service-first layout) ===== */}
1361
- <div className="pt-4 space-y-2">
1362
- <h2 className="text-3xl font-bold text-[var(--color-dash-text)] dark:text-white tracking-tight">
1363
- Service activity
1364
- </h2>
1365
- <p className="text-lg text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]">
1366
- Recent cases, guest feedback, and service updates across your properties
1367
- </p>
1368
- </div>
1369
-
1370
- {/* Action Items Grid (moved up) */}
1371
- {isLoading ? (
1372
- <div className="grid grid-cols-1 md:grid-cols-2 gap-5">
1373
- <CardSkeleton lines={4} />
1374
- <CardSkeleton lines={4} />
1375
- </div>
1376
- ) : myDisputes.length > 0 ? (
1377
- <div className="space-y-5">
1378
- <h3 className="text-xl font-bold text-[var(--color-dash-text)] dark:text-white">
1379
- Open cases & items needing attention
1380
- </h3>
1381
- <div className="grid grid-cols-1 md:grid-cols-2 gap-5">
1382
- {myDisputes.map((d) => (
1383
- <div
1384
- key={d.id}
1385
- onClick={() => {
1386
- toast.info(`Opening ${d.title}`);
1387
- setSelectedDispute(d);
1388
- }}
1389
- className="group bg-white dark:bg-[var(--color-dash-text)] border border-[var(--color-dash-label)]/20 dark:border-[var(--color-dash-muted)]/30 rounded-xl p-6 hover:border-[var(--color-dash-accent)] dark:hover:border-[var(--color-dash-accent)] transition-all duration-300 hover:shadow-lg cursor-pointer"
1390
- >
1391
- <div className="flex items-start justify-between gap-3 mb-3">
1392
- <h4 className="font-semibold text-[var(--color-dash-text)] dark:text-white flex-1">
1393
- {d.title}
1394
- </h4>
1395
- <span
1396
- className={`inline-flex items-center rounded-full px-2 py-1 text-xs font-medium flex-shrink-0 ${
1397
- d.status === "critical"
1398
- ? "bg-[var(--color-dash-danger)]/10 text-[var(--color-dash-danger)] border border-[var(--color-dash-danger)]/30"
1399
- : d.status === "warning"
1400
- ? "bg-[var(--color-dash-warning)]/10 text-[var(--color-dash-warning)] border border-[var(--color-dash-warning)]/30"
1401
- : "bg-[var(--color-dash-info)]/10 text-[var(--color-dash-info)] border border-[var(--color-dash-info)]/30"
1402
- }`}
1403
- >
1404
- {d.badge}
1405
- </span>
1406
- </div>
1407
- <p className="text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] mb-4">
1408
- {d.description}
1409
- </p>
1410
- <div className="flex items-center justify-between">
1411
- <span className="text-lg font-bold text-[var(--color-dash-text)] dark:text-white">
1412
- ${d.amount.toLocaleString()}
1413
- </span>
1414
- {d.agentHandled && (
1415
- <span className="text-xs text-[var(--color-dash-accent)] flex items-center gap-1">
1416
- <svg className="h-3 w-3" fill="currentColor" viewBox="0 0 20 20">
1417
- <path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
1418
- </svg>
1419
- Agent reviewed
1420
- </span>
1421
- )}
1422
- </div>
1423
- </div>
1424
- ))}
1425
- </div>
1426
- </div>
1427
- ) : null}
1428
-
1429
- {/* Service Timeline (moved up) */}
1430
- <div className="pt-4 space-y-2">
1431
- <h3 className="text-xl font-bold text-[var(--color-dash-text)] dark:text-white">
1432
- Service timeline
1433
- </h3>
1434
- <p className="text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]">
1435
- Cases, guest feedback, and updates across your properties
1436
- </p>
1437
- </div>
1438
-
1439
- {/* Activity Feed (moved up) */}
1440
- {isLoading ? (
1441
- <CardSkeleton lines={6} />
1442
- ) : (
1443
- <ActivityCard
1444
- title="Recent updates"
1445
- actions={myActivity.map((a) => ({
1446
- id: a.id,
1447
- status:
1448
- a.status === "alert"
1449
- ? "error"
1450
- : a.status === "warning"
1451
- ? "pending"
1452
- : a.status === "success"
1453
- ? "complete"
1454
- : "working",
1455
- title: a.title,
1456
- subtitle: a.description,
1457
- timestamp: a.timestamp,
1458
- }))}
1459
- />
1460
- )}
1461
-
1462
- {/* ===== BILLING & OPERATIONS (moved down) ===== */}
1463
-
1464
- {/* Penalty Calculation Issue Card */}
1465
- {!isLoading && myPenalties.filter((p: any) => p.isHero).length > 0 && (
1466
- <div className="bg-white dark:bg-[var(--color-dash-text)] border-2 border-[var(--color-dash-warning)]/50 rounded-2xl p-8 shadow-lg hover:shadow-xl transition-shadow duration-300">
1467
- <div className="flex items-start gap-4 mb-6">
1468
- <div className="flex-shrink-0">
1469
- <div className="bg-[var(--color-dash-warning)]/10 rounded-xl p-3">
1470
- <ExclamationTriangleIcon className="h-8 w-8 text-[var(--color-dash-warning)]" />
1471
- </div>
1472
- </div>
1473
- <div className="flex-1">
1474
- <div className="flex items-start justify-between gap-4 mb-2">
1475
- <div>
1476
- <h3 className="text-2xl font-bold text-[var(--color-dash-text)] dark:text-white mb-1">
1477
- Penalty Calculation Issue
1478
- </h3>
1479
- <p className="text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]">
1480
- ATR-00001 / Summit Austin Convention Center / TechCorp Inc. booking
1481
- </p>
1482
- </div>
1483
- <span className="inline-flex items-center rounded-full px-3 py-1 text-xs font-bold bg-[var(--color-dash-warning)] text-white flex-shrink-0">
1484
- NEEDS REVIEW
1485
- </span>
1486
- </div>
1487
- </div>
1488
- </div>
1489
-
1490
- <div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
1491
- <div className="bg-[var(--color-dash-surface)] dark:bg-[var(--color-dash-muted)]/10 rounded-xl p-5 border border-[var(--color-dash-label)]/20 dark:border-[var(--color-dash-muted)]/30">
1492
- <p className="text-xs font-semibold text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] mb-2 uppercase tracking-wider">Current Penalty</p>
1493
- <p className="font-black text-[var(--color-dash-text)] dark:text-white mb-1 leading-tight" style={{ fontSize: 'var(--dash-metric-sub)' }}>$9,000</p>
1494
- <p className="text-xs text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]">You owe Engine</p>
1495
- </div>
1496
- <div className="bg-[var(--color-dash-success)]/5 dark:bg-[var(--color-dash-success)]/10 rounded-xl p-5 border border-[var(--color-dash-success)]/30">
1497
- <p className="text-xs font-semibold text-[var(--color-dash-success)] mb-2 uppercase tracking-wider">Resale Credit Missing</p>
1498
- <p className="font-black text-[var(--color-dash-success)] mb-1 leading-tight" style={{ fontSize: 'var(--dash-metric-sub)' }}>-$2,400</p>
1499
- <p className="text-xs text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]">Should reduce penalty</p>
1500
- </div>
1501
- <div className="bg-[var(--color-dash-accent)]/5 dark:bg-[var(--color-dash-accent)]/10 rounded-xl p-5 border border-[var(--color-dash-accent)]/30">
1502
- <p className="text-xs font-semibold text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] mb-2 uppercase tracking-wider">Corrected Amount</p>
1503
- <p className="font-black text-[var(--color-dash-accent)] mb-1 leading-tight" style={{ fontSize: 'var(--dash-metric-sub)' }}>$6,600</p>
1504
- <p className="text-xs text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]">What you should owe</p>
1505
- </div>
1506
- </div>
1507
-
1508
- <div className="bg-[var(--color-dash-warning)]/10 border-l-4 border-[var(--color-dash-warning)] rounded-r-lg p-4 mb-6">
1509
- <p className="text-sm text-[var(--color-dash-text)] dark:text-white leading-relaxed">
1510
- Your contract specifies a <strong>50% resale credit</strong> for rooms that were resold. Based on <strong>8 rooms at $200/night for 3 nights</strong>,
1511
- you should receive a <strong className="text-[var(--color-dash-success)]">$2,400 credit</strong> that wasn't applied. This should reduce your penalty from $9,000 to <strong>$6,600</strong>.
1512
- </p>
1513
- </div>
1514
-
1515
- <button
1516
- onClick={() => {
1517
- setSelectedPenalty(myPenalties.find((p: any) => p.isHero));
1518
- setIsPenaltyModalOpen(true);
1519
- }}
1520
- className="inline-flex items-center gap-2 px-6 py-3 bg-[var(--color-dash-text)] dark:bg-white hover:bg-[var(--color-dash-muted)] dark:hover:bg-[var(--color-dash-surface)] text-white dark:text-[var(--color-dash-text)] font-semibold rounded-xl transition-all duration-200 hover:shadow-lg group"
1521
- >
1522
- <ExclamationTriangleIcon className="h-5 w-5" />
1523
- Review This Calculation
1524
- <svg className="h-4 w-4 transition-transform group-hover:translate-x-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
1525
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
1526
- </svg>
1527
- </button>
1528
- </div>
1529
- )}
1530
-
1531
- {/* Section - Billing & Contract Details */}
1532
- <div className="pt-8 space-y-2">
1533
- <h2 className="text-3xl font-bold text-[var(--color-dash-text)] dark:text-white tracking-tight">
1534
- Billing & contract details
1535
- </h2>
1536
- <p className="text-lg text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]">
1537
- Keep track of invoices and your partnership terms
1538
- </p>
1539
- </div>
1540
-
1541
- {/* Two Charts Grid */}
1542
- {isLoading ? (
1543
- <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
1544
- <CardSkeleton lines={6} />
1545
- <CardSkeleton lines={6} />
1546
- </div>
1547
- ) : (
1548
- <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
1549
- {/* Revenue Trend by Property (multi-line) */}
1550
- <div className="bg-white dark:bg-[var(--color-dash-text)] border border-[var(--color-dash-label)]/20 dark:border-[var(--color-dash-muted)]/30 rounded-xl shadow-sm hover:shadow-lg transition-shadow duration-300 overflow-hidden">
1551
- <div className="p-6 border-b border-[var(--color-dash-label)]/20 dark:border-[var(--color-dash-muted)]/30">
1552
- <h3 className="text-xl font-bold text-[var(--color-dash-text)] dark:text-white mb-1">
1553
- Revenue by property
1554
- </h3>
1555
- <p className="text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]">
1556
- Monthly trend across {revenueTrendByProperty.properties?.length || 4} properties
1557
- </p>
1558
- <div className="flex flex-wrap gap-3 mt-3">
1559
- {revenueTrendByProperty.properties.map((prop) => (
1560
- <div key={prop.name} className="flex items-center gap-1.5">
1561
- <span className="inline-block h-2 w-2 rounded-full flex-shrink-0" style={{ backgroundColor: prop.color }} />
1562
- <span className="text-xs font-medium text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]">{prop.name.replace('Summit ', '')}</span>
1563
- </div>
1564
- ))}
1565
- </div>
1566
- </div>
1567
- <div className="p-4 w-full">
1568
- {revenueTrendByProperty?.months && revenueTrendByProperty?.properties ? (
1569
- <div className="w-full" style={{ minHeight: '240px' }}>
1570
- <D3Chart data={revenueTrendByProperty} renderChart={renderRevenueTrendByProperty} height={240} responsive={true} ariaLabel="Revenue trend by property" />
1571
- </div>
1572
- ) : (
1573
- <div className="h-[240px] flex items-center justify-center text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]">Loading chart data...</div>
1574
- )}
1575
- </div>
1576
- </div>
1577
-
1578
- {/* Total Monthly Revenue Growth (bar chart) */}
1579
- <div className="bg-white dark:bg-[var(--color-dash-text)] border border-[var(--color-dash-label)]/20 dark:border-[var(--color-dash-muted)]/30 rounded-xl shadow-sm hover:shadow-lg transition-shadow duration-300 overflow-hidden">
1580
- <div className="p-6 border-b border-[var(--color-dash-label)]/20 dark:border-[var(--color-dash-muted)]/30">
1581
- <h3 className="text-xl font-bold text-[var(--color-dash-text)] dark:text-white mb-1">
1582
- Total revenue growth
1583
- </h3>
1584
- <p className="text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]">
1585
- Combined monthly revenue, up 73% over 6 months
1586
- </p>
1587
- </div>
1588
- <div className="p-4 w-full">
1589
- {monthlyTotalRevenue ? (
1590
- <div className="w-full" style={{ minHeight: '240px' }}>
1591
- <D3Chart data={monthlyTotalRevenue} renderChart={renderTotalRevenueGrowth} height={240} responsive={true} ariaLabel="Total revenue growth" />
1592
- </div>
1593
- ) : (
1594
- <div className="h-[240px] flex items-center justify-center text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]">Loading chart data...</div>
1595
- )}
1596
- </div>
1597
- </div>
1598
- </div>
1599
- )}
1600
-
1601
- {/* Invoices & Contract */}
1602
- {isLoading ? (
1603
- <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
1604
- <CardSkeleton lines={5} />
1605
- <CardSkeleton lines={5} />
1606
- </div>
1607
- ) : (
1608
- <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
1609
- <ListCard
1610
- title="Your invoices"
1611
- subtitle="Recent statements"
1612
- items={myInvoices.map((inv) => ({
1613
- id: inv.id,
1614
- title: inv.title,
1615
- description: inv.description,
1616
- status: inv.status,
1617
- value: `$${inv.amount.toLocaleString()}`,
1618
- timestamp: `Due ${inv.due}`,
1619
- }))}
1620
- maxBodyHeight={300}
1621
- showStatus={true}
1622
- showTimestamp={true}
1623
- dense={false}
1624
- divided={true}
1625
- onItemClick={(item) => {
1626
- toast.info(`Opening invoice ${item.title.split(' - ')[0]}`);
1627
- }}
1628
- emptyMessage="No invoices right now."
1629
- loading={isLoading}
1630
- />
1631
-
1632
- <div className="bg-white dark:bg-[var(--color-dash-text)] border border-[var(--color-dash-label)]/20 dark:border-[var(--color-dash-muted)]/30 rounded-xl shadow-sm hover:shadow-lg transition-shadow duration-300 overflow-hidden">
1633
- <div className="p-6 border-b border-[var(--color-dash-label)]/20 dark:border-[var(--color-dash-muted)]/30">
1634
- <h3 className="text-xl font-bold text-[var(--color-dash-text)] dark:text-white mb-1">
1635
- Your Contract
1636
- </h3>
1637
- <p className="text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]">
1638
- Partnership terms
1639
- </p>
1640
- </div>
1641
- <div className="p-6 space-y-4">
1642
- <div className="flex items-center justify-between pb-4 border-b border-[var(--color-dash-label)]/20 dark:border-[var(--color-dash-muted)]/30">
1643
- <div>
1644
- <p className="text-xs text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] mb-1 uppercase tracking-wider">Contract ID</p>
1645
- <p className="text-lg font-bold text-[var(--color-dash-text)] dark:text-white">CNTR-00002</p>
1646
- </div>
1647
- <span className="inline-flex items-center rounded-full px-3 py-1 text-xs font-medium bg-[var(--color-dash-success)]/10 text-[var(--color-dash-success)] border border-[var(--color-dash-success)]/30">
1648
- Active
1649
- </span>
1650
- </div>
1651
-
1652
- <div className="grid grid-cols-2 gap-4">
1653
- <div>
1654
- <p className="text-xs text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] mb-1 uppercase tracking-wider">Commission Rate</p>
1655
- <p className="text-2xl font-bold text-[var(--color-dash-success)]">17%</p>
1656
- </div>
1657
- <div>
1658
- <p className="text-xs text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] mb-1 uppercase tracking-wider">Contract Term</p>
1659
- <p className="text-sm font-semibold text-[var(--color-dash-text)] dark:text-white">Mar 2025 - Feb 2027</p>
1660
- </div>
1661
- </div>
1662
-
1663
- <div className="bg-[var(--color-dash-surface)] dark:bg-[var(--color-dash-muted)]/10 rounded-lg p-4 space-y-3">
1664
- <div>
1665
- <p className="text-xs text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] mb-1 uppercase tracking-wider">Attrition Method</p>
1666
- <p className="text-sm font-semibold text-[var(--color-dash-text)] dark:text-white">Per Night (80% threshold)</p>
1667
- </div>
1668
- <div>
1669
- <p className="text-xs text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] mb-1 uppercase tracking-wider">Resale Credit Policy</p>
1670
- <p className="text-sm font-semibold text-[var(--color-dash-text)] dark:text-white">Partial Credit (50%)</p>
1671
- </div>
1672
- </div>
1673
-
1674
- <div className="pt-2">
1675
- <button
1676
- onClick={() => toast.info("Opening full contract PDF...")}
1677
- className="w-full inline-flex items-center justify-center gap-2 px-4 py-3 bg-[var(--color-dash-surface)] dark:bg-[var(--color-dash-muted)]/20 hover:bg-[var(--color-dash-accent)]/10 dark:hover:bg-[var(--color-dash-accent)]/10 text-[var(--color-dash-text)] dark:text-white font-semibold rounded-lg border border-[var(--color-dash-label)]/30 dark:border-[var(--color-dash-muted)]/50 hover:border-[var(--color-dash-accent)] transition-colors text-sm"
1678
- >
1679
- <DocumentArrowDownIcon className="h-4 w-4" />
1680
- Download Full Contract
1681
- </button>
1682
- </div>
1683
- </div>
1684
- </div>
1685
- </div>
1686
- )}
1687
-
1688
- {/* Help Section - Enhanced */}
1689
- <div className="bg-gradient-to-br from-white to-[var(--color-dash-surface)] dark:from-[var(--color-dash-text)] dark:to-[var(--color-dash-dark)] rounded-2xl p-12 border border-[var(--color-dash-label)]/20 dark:border-[var(--color-dash-muted)]/30 shadow-lg">
1690
- <div className="text-center max-w-2xl mx-auto space-y-6">
1691
- <h3 className="text-3xl font-bold text-[var(--color-dash-text)] dark:text-white tracking-tight">
1692
- Questions? We're here to help
1693
- </h3>
1694
- <p className="text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] text-xl leading-relaxed">
1695
- Our partner team is standing by if you need anything, from service cases to billing questions.
1696
- </p>
1697
- <div className="flex gap-4 justify-center flex-wrap pt-2">
1698
- <Modal>
1699
- <button className="group px-8 py-4 bg-[var(--color-dash-text)] dark:bg-white hover:bg-[var(--color-dash-muted)] dark:hover:bg-[var(--color-dash-surface)] text-white dark:text-[var(--color-dash-text)] font-semibold rounded-xl transition-colors duration-200 hover:shadow-xl">
1700
- Get in touch
1701
- </button>
1702
- <Modal.Backdrop>
1703
- <Modal.Container>
1704
- <Modal.Dialog className="max-w-2xl">
1705
- <Modal.CloseTrigger />
1706
- <Modal.Header>
1707
- <Modal.Heading className="text-2xl font-bold text-[var(--color-dash-text)] dark:text-white">
1708
- Contact Support
1709
- </Modal.Heading>
1710
- </Modal.Header>
1711
- <Modal.Body className="space-y-6">
1712
- <p className="text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]">
1713
- Our partner support team is here to help with billing questions, contract details, or any other issues you may have.
1714
- </p>
1715
-
1716
- {/* Contact Options */}
1717
- <div className="space-y-4">
1718
- <div className="bg-[var(--color-dash-surface)] dark:bg-[var(--color-dash-muted)]/10 rounded-xl p-6 hover:border hover:border-[var(--color-dash-accent)] transition-colors cursor-pointer">
1719
- <div className="flex items-start gap-4">
1720
- <div className="bg-[var(--color-dash-accent)]/20 rounded-lg p-3">
1721
- <svg className="h-6 w-6 text-[var(--color-dash-accent)]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
1722
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
1723
- </svg>
1724
- </div>
1725
- <div>
1726
- <h4 className="font-semibold text-[var(--color-dash-text)] dark:text-white mb-1">Email Support</h4>
1727
- <p className="text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] mb-2">
1728
- Get help via email - we typically respond within 2-4 hours
1729
- </p>
1730
- <a href="mailto:partners@engine.com" className="text-sm font-medium text-[var(--color-dash-accent)] hover:underline">
1731
- partners@engine.com
1732
- </a>
1733
- </div>
1734
- </div>
1735
- </div>
1736
-
1737
- <div className="bg-[var(--color-dash-surface)] dark:bg-[var(--color-dash-muted)]/10 rounded-xl p-6 hover:border hover:border-[var(--color-dash-accent)] transition-colors cursor-pointer">
1738
- <div className="flex items-start gap-4">
1739
- <div className="bg-[var(--color-dash-success)]/20 rounded-lg p-3">
1740
- <svg className="h-6 w-6 text-[var(--color-dash-success)]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
1741
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" />
1742
- </svg>
1743
- </div>
1744
- <div>
1745
- <h4 className="font-semibold text-[var(--color-dash-text)] dark:text-white mb-1">Phone Support</h4>
1746
- <p className="text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] mb-2">
1747
- Speak with a partner specialist directly
1748
- </p>
1749
- <a href="tel:+18005551234" className="text-sm font-medium text-[var(--color-dash-success)] hover:underline">
1750
- 1-800-555-1234
1751
- </a>
1752
- <p className="text-xs text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] mt-1">
1753
- Mon-Fri, 8am-6pm EST
1754
- </p>
1755
- </div>
1756
- </div>
1757
- </div>
1758
- </div>
1759
- </Modal.Body>
1760
- <Modal.Footer>
1761
- <div className="flex gap-3 justify-end">
1762
- <Modal.CloseTrigger className="px-6 py-2 bg-[var(--color-dash-text)] dark:bg-white hover:bg-[var(--color-dash-muted)] dark:hover:bg-[var(--color-dash-surface)] text-white dark:text-[var(--color-dash-text)] font-semibold rounded-lg transition-colors">
1763
- Close
1764
- </Modal.CloseTrigger>
1765
- </div>
1766
- </Modal.Footer>
1767
- </Modal.Dialog>
1768
- </Modal.Container>
1769
- </Modal.Backdrop>
1770
- </Modal>
1771
-
1772
- <button
1773
- onClick={() => toast.info("Opening help documentation...")}
1774
- className="group px-8 py-4 bg-transparent hover:bg-[var(--color-dash-surface)] dark:hover:bg-[var(--color-dash-muted)]/20 text-[var(--color-dash-text)] dark:text-white font-semibold rounded-xl border-2 border-[var(--color-dash-label)]/30 dark:border-[var(--color-dash-muted)]/50 hover:border-[var(--color-dash-accent)] transition-colors duration-200"
1775
- >
1776
- Browse help docs
1777
- </button>
1778
- </div>
1779
- </div>
1780
- </div>
1781
-
1782
- </>}
1783
-
1784
- {/* ============== CASES TAB ============== */}
1785
- {activeTab === "cases" && !tabLoading && (
1786
- <div className="space-y-6 animate-fade-in">
1787
- <div className="flex items-center justify-between">
1788
- <div>
1789
- <h2 className="text-2xl font-bold text-[var(--color-dash-text)] dark:text-white">Service Cases</h2>
1790
- <p className="text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] mt-1">All open and recent cases across your properties</p>
1791
- </div>
1792
- <div className="flex items-center gap-3">
1793
- <select className="bg-[var(--color-dash-surface)] dark:bg-[var(--color-dash-muted)]/10 border border-[var(--color-dash-label)]/20 dark:border-[var(--color-dash-muted)]/30 rounded-lg px-3 py-2 text-sm text-[var(--color-dash-text)] dark:text-white">
1794
- <option>All Properties</option>
1795
- <option>Austin Convention Center</option>
1796
- <option>Midtown NYC</option>
1797
- <option>Chicago Downtown</option>
1798
- <option>San Francisco Bay</option>
1799
- </select>
1800
- <select className="bg-[var(--color-dash-surface)] dark:bg-[var(--color-dash-muted)]/10 border border-[var(--color-dash-label)]/20 dark:border-[var(--color-dash-muted)]/30 rounded-lg px-3 py-2 text-sm text-[var(--color-dash-text)] dark:text-white">
1801
- <option>All Statuses</option>
1802
- <option>Open</option>
1803
- <option>In Progress</option>
1804
- <option>Escalated</option>
1805
- <option>Resolved</option>
1806
- </select>
1807
- </div>
1808
- </div>
1809
-
1810
- {/* Cases Summary Cards */}
1811
- <div className="grid grid-cols-1 sm:grid-cols-4 gap-4">
1812
- {[
1813
- { label: "Open", count: 3, color: "var(--color-dash-warning)", bg: "var(--color-dash-warning)" },
1814
- { label: "In Progress", count: 2, color: "var(--color-dash-info)", bg: "var(--color-dash-info)" },
1815
- { label: "Escalated", count: 2, color: "var(--color-dash-danger)", bg: "var(--color-dash-danger)" },
1816
- { label: "Resolved (30d)", count: 22, color: "var(--color-dash-success)", bg: "var(--color-dash-success)" },
1817
- ].map((s) => (
1818
- <div key={s.label} className="bg-white dark:bg-[var(--color-dash-text)] rounded-xl p-5 border border-[var(--color-dash-label)]/20 dark:border-[var(--color-dash-muted)]/30 shadow-sm">
1819
- <p className="text-xs font-semibold uppercase tracking-wider mb-1" style={{ color: s.color }}>{s.label}</p>
1820
- <p className="text-3xl font-black text-[var(--color-dash-text)] dark:text-white">{s.count}</p>
1821
- </div>
1822
- ))}
1823
- </div>
1824
-
1825
- {/* Cases Data Table */}
1826
- <div className="bg-white dark:bg-[var(--color-dash-text)] border border-[var(--color-dash-label)]/20 dark:border-[var(--color-dash-muted)]/30 rounded-xl overflow-hidden shadow-sm">
1827
- <div className="overflow-x-auto">
1828
- <table className="w-full table-fixed">
1829
- <thead className="bg-[var(--color-dash-surface)] dark:bg-[var(--color-dash-muted)]/10 border-b border-[var(--color-dash-label)]/20 dark:border-[var(--color-dash-muted)]/30">
1830
- <tr>
1831
- <th className="w-[88px] px-4 py-3 text-left text-xs font-semibold text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] uppercase tracking-wider">Case #</th>
1832
- <th className="px-4 py-3 text-left text-xs font-semibold text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] uppercase tracking-wider">Subject</th>
1833
- <th className="w-[160px] px-4 py-3 text-left text-xs font-semibold text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] uppercase tracking-wider">Property</th>
1834
- <th className="w-[120px] px-4 py-3 text-left text-xs font-semibold text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] uppercase tracking-wider">Category</th>
1835
- <th className="w-[80px] px-4 py-3 text-left text-xs font-semibold text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] uppercase tracking-wider">Priority</th>
1836
- <th className="w-[100px] px-4 py-3 text-left text-xs font-semibold text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] uppercase tracking-wider">Status</th>
1837
- <th className="w-[110px] px-4 py-3 text-left text-xs font-semibold text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] uppercase tracking-wider">Assigned To</th>
1838
- <th className="w-[72px] px-4 py-3 text-left text-xs font-semibold text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] uppercase tracking-wider">SLA</th>
1839
- </tr>
1840
- </thead>
1841
- <tbody className="divide-y divide-[var(--color-dash-label)]/10 dark:divide-[var(--color-dash-muted)]/20">
1842
- {[
1843
- { id: "CS-1042", subject: "Noise complaint, room 412 adjacent to event space", property: "Austin Conv. Ctr", category: "Guest Complaint", priority: "High", status: "Resolved", assignedTo: "Sarah Chen", sla: "Met" },
1844
- { id: "CS-1041", subject: "HVAC unit failure in conference room B", property: "SF Bay", category: "Maintenance", priority: "Medium", status: "In Progress", assignedTo: "Mike Torres", sla: "On Track" },
1845
- { id: "CS-1040", subject: "Billing dispute, missing resale credit ATR-00001", property: "Austin Conv. Ctr", category: "Billing", priority: "High", status: "Open", assignedTo: "Agent", sla: "At Risk" },
1846
- { id: "CS-1039", subject: "Room cleanliness, repeat complaint, 3rd occurrence", property: "Chicago Dtwn", category: "Guest Complaint", priority: "High", status: "Escalated", assignedTo: "Regional Mgr", sla: "Breached" },
1847
- { id: "CS-1038", subject: "Late checkout request for VIP guest, corporate account", property: "SF Bay", category: "Guest Request", priority: "Low", status: "Open", assignedTo: "Front Desk", sla: "On Track" },
1848
- { id: "CS-1037", subject: "Commission rate discrepancy on March invoice", property: "Midtown NYC", category: "Billing", priority: "Medium", status: "Open", assignedTo: "Agent", sla: "On Track" },
1849
- { id: "CS-1036", subject: "Group booking modification, TechCorp adding 12 rooms", property: "Austin Conv. Ctr", category: "Booking", priority: "Medium", status: "Resolved", assignedTo: "Reservations", sla: "Met" },
1850
- { id: "CS-1035", subject: "Pool area maintenance, heater malfunction", property: "SF Bay", category: "Maintenance", priority: "Low", status: "In Progress", assignedTo: "Facilities", sla: "On Track" },
1851
- { id: "CS-1034", subject: "Penalty calculation review, revenue-based method request", property: "Austin Conv. Ctr", category: "Billing", priority: "High", status: "Escalated", assignedTo: "Partner Mgr", sla: "Breached" },
1852
- { id: "CS-1033", subject: "Positive guest feedback, front desk team recognition", property: "Midtown NYC", category: "Guest Feedback", priority: "Low", status: "Resolved", assignedTo: "HR", sla: "Met" },
1853
- ].map((c) => (
1854
- <tr key={c.id} className="hover:bg-[var(--color-dash-surface)]/50 dark:hover:bg-[var(--color-dash-muted)]/5 transition-colors cursor-pointer">
1855
- <td className="px-4 py-3 text-sm font-medium text-[var(--color-dash-accent)] whitespace-nowrap">{c.id}</td>
1856
- <td className="px-4 py-3 text-sm text-[var(--color-dash-text)] dark:text-white leading-snug">{c.subject}</td>
1857
- <td className="px-4 py-3 text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]">{c.property}</td>
1858
- <td className="px-4 py-3">
1859
- <span className={`inline-flex whitespace-nowrap text-xs font-medium px-2 py-0.5 rounded-full ${
1860
- c.category === "Guest Complaint" ? "bg-[var(--color-dash-danger)]/10 text-[var(--color-dash-danger)]" :
1861
- c.category === "Billing" ? "bg-[var(--color-dash-warning)]/10 text-[var(--color-dash-warning)]" :
1862
- c.category === "Maintenance" ? "bg-[var(--color-dash-info)]/10 text-[var(--color-dash-info)]" :
1863
- "bg-[var(--color-dash-label)]/10 text-[var(--color-dash-muted)]"
1864
- }`}>{c.category}</span>
1865
- </td>
1866
- <td className="px-4 py-3">
1867
- <span className={`text-xs font-bold whitespace-nowrap ${
1868
- c.priority === "High" ? "text-[var(--color-dash-danger)]" :
1869
- c.priority === "Medium" ? "text-[var(--color-dash-warning)]" :
1870
- "text-[var(--color-dash-label)]"
1871
- }`}>{c.priority}</span>
1872
- </td>
1873
- <td className="px-4 py-3">
1874
- <span className={`inline-flex whitespace-nowrap items-center rounded-full px-2 py-0.5 text-xs font-medium ${
1875
- c.status === "Open" ? "bg-[var(--color-dash-warning)]/10 text-[var(--color-dash-warning)] border border-[var(--color-dash-warning)]/30" :
1876
- c.status === "In Progress" ? "bg-[var(--color-dash-info)]/10 text-[var(--color-dash-info)] border border-[var(--color-dash-info)]/30" :
1877
- c.status === "Escalated" ? "bg-[var(--color-dash-danger)]/10 text-[var(--color-dash-danger)] border border-[var(--color-dash-danger)]/30" :
1878
- "bg-[var(--color-dash-success)]/10 text-[var(--color-dash-success)] border border-[var(--color-dash-success)]/30"
1879
- }`}>{c.status}</span>
1880
- </td>
1881
- <td className="px-4 py-3 text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] whitespace-nowrap">{c.assignedTo}</td>
1882
- <td className="px-4 py-3">
1883
- <span className={`text-xs font-semibold whitespace-nowrap ${
1884
- c.sla === "Met" ? "text-[var(--color-dash-success)]" :
1885
- c.sla === "On Track" ? "text-[var(--color-dash-info)]" :
1886
- c.sla === "At Risk" ? "text-[var(--color-dash-warning)]" :
1887
- "text-[var(--color-dash-danger)]"
1888
- }`}>{c.sla}</span>
1889
- </td>
1890
- </tr>
1891
- ))}
1892
- </tbody>
1893
- </table>
1894
- </div>
1895
- <div className="px-6 py-4 border-t border-[var(--color-dash-label)]/10 dark:border-[var(--color-dash-muted)]/20 flex items-center justify-between">
1896
- <p className="text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]">Showing 10 of 30 cases</p>
1897
- <div className="flex items-center gap-2">
1898
- <button className="px-3 py-1.5 text-sm rounded-lg border border-[var(--color-dash-label)]/20 dark:border-[var(--color-dash-muted)]/30 text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] hover:bg-[var(--color-dash-surface)] dark:hover:bg-[var(--color-dash-muted)]/10 transition-colors">Previous</button>
1899
- <span className="px-3 py-1.5 text-sm rounded-lg bg-[var(--color-dash-dark)] text-white font-semibold">1</span>
1900
- <button className="px-3 py-1.5 text-sm rounded-lg border border-[var(--color-dash-label)]/20 dark:border-[var(--color-dash-muted)]/30 text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] hover:bg-[var(--color-dash-surface)] dark:hover:bg-[var(--color-dash-muted)]/10 transition-colors">2</button>
1901
- <button className="px-3 py-1.5 text-sm rounded-lg border border-[var(--color-dash-label)]/20 dark:border-[var(--color-dash-muted)]/30 text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] hover:bg-[var(--color-dash-surface)] dark:hover:bg-[var(--color-dash-muted)]/10 transition-colors">Next</button>
1902
- </div>
1903
- </div>
1904
- </div>
1905
- </div>
1906
- )}
1907
-
1908
- {/* ============== PROPERTIES TAB ============== */}
1909
- {activeTab === "properties" && !tabLoading && (
1910
- <div className="space-y-6 animate-fade-in">
1911
- <div className="flex items-center justify-between">
1912
- <div>
1913
- <h2 className="text-2xl font-bold text-[var(--color-dash-text)] dark:text-white">Your Properties</h2>
1914
- <p className="text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] mt-1">Manage and monitor all your properties in the Engine network</p>
1915
- </div>
1916
- </div>
1917
-
1918
- <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
1919
- {[
1920
- { name: "Summit Austin Convention Center", city: "Austin, TX", rooms: 320, satisfaction: 4.8, responseTime: "1.8h", openCases: 2, revenue: "$97,600", growth: "+60%", status: "Excellent", aiRec: "40-room cancellation creates promo opportunity. Launch Engine network promotion to recover up to $12,287 in revenue." },
1921
- { name: "Summit Midtown NYC", city: "New York, NY", rooms: 280, satisfaction: 4.7, responseTime: "2.1h", openCases: 1, revenue: "$73,100", growth: "+55%", status: "Good", aiRec: "Strong performance. Consider requesting a rate increase for Q3 based on 55% growth trajectory and high guest satisfaction." },
1922
- { name: "Summit Chicago Downtown", city: "Chicago, IL", rooms: 240, satisfaction: 4.5, responseTime: "2.6h", openCases: 1, revenue: "$59,000", growth: "+75%", status: "Good", aiRec: "Satisfaction trending down (4.7 to 4.5). Housekeeping audit recommended based on repeat cleanliness complaint." },
1923
- { name: "Summit San Francisco Bay", city: "San Francisco, CA", rooms: 200, satisfaction: 4.3, responseTime: "3.2h", openCases: 3, revenue: "$53,300", growth: "+100%", status: "Needs Attention", aiRec: "Response time (3.2h) approaching SLA limit. Assign dedicated service agent to prevent breaches and protect 100% growth momentum." },
1924
- ].map((prop) => (
1925
- <div key={prop.name} className="bg-white dark:bg-[var(--color-dash-text)] rounded-xl border border-[var(--color-dash-label)]/20 dark:border-[var(--color-dash-muted)]/30 shadow-sm hover:shadow-lg transition-all duration-300 overflow-hidden cursor-pointer">
1926
- <div className="p-6">
1927
- <div className="flex items-start justify-between mb-4">
1928
- <div>
1929
- <h3 className="text-lg font-bold text-[var(--color-dash-text)] dark:text-white">{prop.name}</h3>
1930
- <p className="text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]">{prop.city} / {prop.rooms} rooms</p>
1931
- </div>
1932
- <span className={`inline-flex items-center rounded-full px-2.5 py-1 text-xs font-medium ${
1933
- prop.status === "Excellent" ? "bg-[var(--color-dash-success)]/10 text-[var(--color-dash-success)] border border-[var(--color-dash-success)]/30" :
1934
- prop.status === "Good" ? "bg-[var(--color-dash-info)]/10 text-[var(--color-dash-info)] border border-[var(--color-dash-info)]/30" :
1935
- "bg-[var(--color-dash-warning)]/10 text-[var(--color-dash-warning)] border border-[var(--color-dash-warning)]/30"
1936
- }`}>{prop.status}</span>
1937
- </div>
1938
-
1939
- <div className="grid grid-cols-3 gap-4 mb-4">
1940
- <div>
1941
- <p className="text-xs text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] uppercase tracking-wider mb-1">Satisfaction</p>
1942
- <div className="flex items-center gap-1">
1943
- <StarIcon className="h-4 w-4 text-[var(--color-dash-accent)]" />
1944
- <span className="text-xl font-bold text-[var(--color-dash-text)] dark:text-white">{prop.satisfaction}</span>
1945
- </div>
1946
- </div>
1947
- <div>
1948
- <p className="text-xs text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] uppercase tracking-wider mb-1">Avg Response</p>
1949
- <span className="text-xl font-bold text-[var(--color-dash-text)] dark:text-white">{prop.responseTime}</span>
1950
- </div>
1951
- <div>
1952
- <p className="text-xs text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] uppercase tracking-wider mb-1">Open Cases</p>
1953
- <span className="text-xl font-bold text-[var(--color-dash-text)] dark:text-white">{prop.openCases}</span>
1954
- </div>
1955
- </div>
1956
-
1957
- <div className="flex items-center justify-between pt-4 border-t border-[var(--color-dash-label)]/10 dark:border-[var(--color-dash-muted)]/20 mb-4">
1958
- <div>
1959
- <p className="text-xs text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]">Revenue (6 mo)</p>
1960
- <p className="text-lg font-bold text-[var(--color-dash-text)] dark:text-white">{prop.revenue}</p>
1961
- </div>
1962
- <span className="inline-flex items-center rounded-full px-2 py-0.5 text-xs font-bold bg-[var(--color-dash-success)]/10 text-[var(--color-dash-success)]">
1963
- {prop.growth}
1964
- </span>
1965
- </div>
1966
-
1967
- {/* AI Recommendation */}
1968
- <div className="bg-[var(--color-dash-surface)]/70 dark:bg-[var(--color-dash-dark)]/50 rounded-lg p-3 flex items-start gap-2.5">
1969
- <SparklesIcon className="h-3.5 w-3.5 text-[var(--color-dash-accent)] flex-shrink-0 mt-0.5" />
1970
- <p className="text-xs text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] leading-relaxed">{prop.aiRec}</p>
1971
- </div>
1972
- </div>
1973
- </div>
1974
- ))}
1975
- </div>
1976
- </div>
1977
- )}
1978
-
1979
- {/* ============== ANALYTICS TAB ============== */}
1980
- {activeTab === "analytics" && !tabLoading && (
1981
- <div className="space-y-6 animate-fade-in">
1982
- <div className="flex items-center justify-between">
1983
- <div>
1984
- <h2 className="text-2xl font-bold text-[var(--color-dash-text)] dark:text-white">Analytics</h2>
1985
- <p className="text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] mt-1">
1986
- Performance analytics powered by Tableau
1987
- </p>
1988
- </div>
1989
- <div className="flex items-center gap-2 text-xs text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]">
1990
- <span className="inline-flex items-center gap-1 px-2 py-1 rounded bg-[var(--color-dash-surface)] dark:bg-[var(--color-dash-muted)]/10 border border-[var(--color-dash-label)]/20 dark:border-[var(--color-dash-muted)]/30">
1991
- <span className="h-1.5 w-1.5 rounded-full bg-[var(--color-dash-success)] animate-pulse" />
1992
- Live from Tableau
1993
- </span>
1994
- </div>
1995
- </div>
1996
-
1997
- {/* Key Metrics Row */}
1998
- <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
1999
- {[
2000
- { label: "Total Revenue (6mo)", value: "$283,000", delta: "+73%", deltaColor: "var(--color-dash-success)" },
2001
- { label: "Avg Occupancy Rate", value: "77.8%", delta: "+5.2%", deltaColor: "var(--color-dash-success)" },
2002
- { label: "RevPAR", value: "$149.25", delta: "+12%", deltaColor: "var(--color-dash-success)" },
2003
- ].map((m) => (
2004
- <div key={m.label} className="bg-white dark:bg-[var(--color-dash-text)] rounded-xl p-5 border border-[var(--color-dash-label)]/20 dark:border-[var(--color-dash-muted)]/30 shadow-sm">
2005
- <p className="text-xs font-semibold text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] uppercase tracking-wider mb-1">{m.label}</p>
2006
- <div className="flex items-baseline gap-2">
2007
- <p className="text-2xl font-black text-[var(--color-dash-text)] dark:text-white">{m.value}</p>
2008
- <span className="text-xs font-bold" style={{ color: m.deltaColor }}>{m.delta}</span>
2009
- </div>
2010
- </div>
2011
- ))}
2012
- </div>
2013
-
2014
- {/* Charts Row 1: Revenue Trend + Booking Pipeline */}
2015
- <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
2016
- {/* Revenue Trend (multi-line) */}
2017
- <div className="bg-white dark:bg-[var(--color-dash-text)] rounded-2xl p-8 border border-[var(--color-dash-label)]/20 dark:border-[var(--color-dash-muted)]/30 shadow-sm">
2018
- <div className="flex items-center justify-between mb-2">
2019
- <div>
2020
- <h3 className="text-lg font-bold text-[var(--color-dash-text)] dark:text-white">Revenue Trend</h3>
2021
- <p className="text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] mt-1">Monthly revenue by property</p>
2022
- </div>
2023
- </div>
2024
- <div className="flex gap-4 mb-4 text-xs">
2025
- {[{ label: "Austin", color: "#5BC8C8" }, { label: "NYC", color: "#6366f1" }, { label: "Chicago", color: "#f59e0b" }, { label: "SF Bay", color: "#ef4444" }].map(l => (
2026
- <span key={l.label} className="flex items-center gap-1.5 text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]">
2027
- <span className="h-2 w-2 rounded-full" style={{ backgroundColor: l.color }} />
2028
- {l.label}
2029
- </span>
2030
- ))}
2031
- </div>
2032
- <D3Chart
2033
- data={[
2034
- { x: 0, austin: 13800, nyc: 9800, chicago: 7200, sf: 5400 },
2035
- { x: 1, austin: 13800, nyc: 10200, chicago: 8100, sf: 6200 },
2036
- { x: 2, austin: 15200, nyc: 11400, chicago: 9600, sf: 7100 },
2037
- { x: 3, austin: 16900, nyc: 12600, chicago: 10400, sf: 8800 },
2038
- { x: 4, austin: 18100, nyc: 13900, chicago: 11100, sf: 10800 },
2039
- { x: 5, austin: 19800, nyc: 15200, chicago: 12600, sf: 15000 },
2040
- ]}
2041
- renderChart={(svg, data, dims) => {
2042
- const sel = d3.select(svg);
2043
- sel.selectAll("*").remove();
2044
- const m = { top: 12, right: 16, bottom: 28, left: 48 };
2045
- const w = dims.width, h = dims.height;
2046
- const iw = w - m.left - m.right, ih = h - m.top - m.bottom;
2047
- sel.attr("viewBox", `0 0 ${w} ${h}`);
2048
- const g = sel.append("g").attr("transform", `translate(${m.left},${m.top})`);
2049
- const months = ["Oct", "Nov", "Dec", "Jan", "Feb", "Mar"];
2050
- const x = d3.scaleLinear().domain([0, 5]).range([0, iw]);
2051
- const allVals = data.flatMap(d => [d.austin, d.nyc, d.chicago, d.sf]);
2052
- const y = d3.scaleLinear().domain([d3.min(allVals) * 0.9, d3.max(allVals) * 1.05]).range([ih, 0]);
2053
- g.append("g").call(d3.axisLeft(y).ticks(5).tickFormat(d => `$${(d/1000).toFixed(0)}K`).tickSize(-iw))
2054
- .call(a => a.selectAll("text").attr("fill", "#94a3b8").attr("font-size", "10px"))
2055
- .call(a => a.selectAll(".tick line").attr("stroke", "#e2e8f0").attr("stroke-dasharray", "2,2"))
2056
- .call(a => a.select(".domain").remove());
2057
- g.append("g").attr("transform", `translate(0,${ih})`).call(d3.axisBottom(x).ticks(6).tickFormat((_, i) => months[i]).tickSize(0))
2058
- .call(a => a.selectAll("text").attr("fill", "#94a3b8").attr("font-size", "11px"))
2059
- .call(a => a.select(".domain").remove());
2060
- const series = [{ key: "austin", color: "#5BC8C8" }, { key: "nyc", color: "#6366f1" }, { key: "chicago", color: "#f59e0b" }, { key: "sf", color: "#ef4444" }];
2061
- series.forEach(s => {
2062
- const area = d3.area().x(d => x(d.x)).y0(ih).y1(d => y(d[s.key])).curve(d3.curveMonotoneX);
2063
- g.append("path").datum(data).attr("fill", s.color).attr("opacity", 0.08).attr("d", area);
2064
- const line = d3.line().x(d => x(d.x)).y(d => y(d[s.key])).curve(d3.curveMonotoneX);
2065
- g.append("path").datum(data).attr("fill", "none").attr("stroke", s.color).attr("stroke-width", 2.5).attr("d", line);
2066
- });
2067
- }}
2068
- height={240}
2069
- responsive
2070
- />
2071
- </div>
2072
-
2073
- {/* Booking Pipeline (grouped bar) */}
2074
- <div className="bg-white dark:bg-[var(--color-dash-text)] rounded-2xl p-8 border border-[var(--color-dash-label)]/20 dark:border-[var(--color-dash-muted)]/30 shadow-sm">
2075
- <div className="flex items-center justify-between mb-2">
2076
- <div>
2077
- <h3 className="text-lg font-bold text-[var(--color-dash-text)] dark:text-white">Booking Pipeline</h3>
2078
- <p className="text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] mt-1">Upcoming reservations by category</p>
2079
- </div>
2080
- </div>
2081
- <div className="flex gap-4 mb-4 text-xs">
2082
- <span className="flex items-center gap-1.5 text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]"><span className="h-2 w-2 rounded-full bg-[#5BC8C8]" />Confirmed</span>
2083
- <span className="flex items-center gap-1.5 text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]"><span className="h-2 w-2 rounded-full bg-[#f59e0b]" />Tentative</span>
2084
- </div>
2085
- <D3Chart
2086
- data={[
2087
- { x: "Corporate", confirmed: 142, tentative: 38 },
2088
- { x: "Leisure", confirmed: 89, tentative: 22 },
2089
- { x: "Group", confirmed: 34, tentative: 12 },
2090
- { x: "Events", confirmed: 18, tentative: 8 },
2091
- { x: "Last-Min", confirmed: 56, tentative: 14 },
2092
- ]}
2093
- renderChart={(svg, data, dims) => {
2094
- const sel = d3.select(svg);
2095
- sel.selectAll("*").remove();
2096
- const m = { top: 12, right: 16, bottom: 32, left: 48 };
2097
- const w = dims.width, h = dims.height;
2098
- const iw = w - m.left - m.right, ih = h - m.top - m.bottom;
2099
- sel.attr("viewBox", `0 0 ${w} ${h}`);
2100
- const g = sel.append("g").attr("transform", `translate(${m.left},${m.top})`);
2101
- const keys = ["confirmed", "tentative"];
2102
- const colors = ["#5BC8C8", "#f59e0b"];
2103
- const x0 = d3.scaleBand().domain(data.map(d => d.x)).range([0, iw]).padding(0.3);
2104
- const x1 = d3.scaleBand().domain(keys).range([0, x0.bandwidth()]).padding(0.05);
2105
- const yMax = d3.max(data, d => d3.max(keys, k => d[k])) * 1.15;
2106
- const y = d3.scaleLinear().domain([0, yMax]).range([ih, 0]);
2107
- g.append("g").call(d3.axisLeft(y).ticks(5).tickFormat(d3.format(",.0f")).tickSize(-iw))
2108
- .call(a => a.selectAll("text").attr("fill", "#94a3b8").attr("font-size", "10px"))
2109
- .call(a => a.selectAll(".tick line").attr("stroke", "#e2e8f0").attr("stroke-dasharray", "2,2"))
2110
- .call(a => a.select(".domain").remove());
2111
- g.append("g").attr("transform", `translate(0,${ih})`).call(d3.axisBottom(x0).tickSize(0))
2112
- .call(a => a.selectAll("text").attr("fill", "#94a3b8").attr("font-size", "11px"))
2113
- .call(a => a.select(".domain").remove());
2114
- const rows = g.selectAll(".bar-group").data(data).join("g").attr("transform", d => `translate(${x0(d.x)},0)`);
2115
- keys.forEach((key, i) => {
2116
- rows.append("rect").attr("x", x1(key)).attr("y", d => y(d[key])).attr("width", x1.bandwidth()).attr("height", d => ih - y(d[key])).attr("rx", 4).attr("fill", colors[i]);
2117
- });
2118
- }}
2119
- height={240}
2120
- responsive
2121
- />
2122
- </div>
2123
- </div>
2124
-
2125
- {/* Charts Row 2: Guest Satisfaction Trend + Occupancy */}
2126
- <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
2127
- {/* Guest Satisfaction */}
2128
- <div className="bg-white dark:bg-[var(--color-dash-text)] rounded-2xl p-8 border border-[var(--color-dash-label)]/20 dark:border-[var(--color-dash-muted)]/30 shadow-sm">
2129
- <div className="flex items-center justify-between mb-2">
2130
- <div>
2131
- <h3 className="text-lg font-bold text-[var(--color-dash-text)] dark:text-white">Guest Satisfaction Trend</h3>
2132
- <p className="text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] mt-1">Average rating over the last 6 months</p>
2133
- </div>
2134
- <div className="flex items-center gap-1.5 text-sm text-[var(--color-dash-success)] font-semibold">
2135
- <SparklesIcon className="h-4 w-4" />
2136
- Trending up
2137
- </div>
2138
- </div>
2139
- <div className="flex gap-4 mb-4 text-xs">
2140
- {[{ label: "Austin", color: "#5BC8C8" }, { label: "NYC", color: "#6366f1" }, { label: "Chicago", color: "#f59e0b" }, { label: "SF Bay", color: "#ef4444" }].map(l => (
2141
- <span key={l.label} className="flex items-center gap-1.5 text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]">
2142
- <span className="h-2 w-2 rounded-full" style={{ backgroundColor: l.color }} />
2143
- {l.label}
2144
- </span>
2145
- ))}
2146
- </div>
2147
- <D3Chart
2148
- data={[
2149
- { x: 0, austin: 4.5, nyc: 4.3, chicago: 4.6, sf: 3.8 },
2150
- { x: 1, austin: 4.6, nyc: 4.3, chicago: 4.6, sf: 3.9 },
2151
- { x: 2, austin: 4.6, nyc: 4.4, chicago: 4.5, sf: 4.0 },
2152
- { x: 3, austin: 4.7, nyc: 4.5, chicago: 4.5, sf: 4.1 },
2153
- { x: 4, austin: 4.7, nyc: 4.6, chicago: 4.5, sf: 4.2 },
2154
- { x: 5, austin: 4.8, nyc: 4.7, chicago: 4.5, sf: 4.3 },
2155
- ]}
2156
- renderChart={(svg, data, dims) => {
2157
- const sel = d3.select(svg);
2158
- sel.selectAll("*").remove();
2159
- const m = { top: 12, right: 16, bottom: 28, left: 36 };
2160
- const w = dims.width, h = dims.height;
2161
- const iw = w - m.left - m.right, ih = h - m.top - m.bottom;
2162
- sel.attr("viewBox", `0 0 ${w} ${h}`);
2163
- const g = sel.append("g").attr("transform", `translate(${m.left},${m.top})`);
2164
- const months = ["Oct", "Nov", "Dec", "Jan", "Feb", "Mar"];
2165
- const x = d3.scaleLinear().domain([0, 5]).range([0, iw]);
2166
- const y = d3.scaleLinear().domain([3.5, 5.0]).range([ih, 0]);
2167
- g.append("g").call(d3.axisLeft(y).ticks(4).tickSize(-iw))
2168
- .call(a => a.selectAll("text").attr("fill", "#94a3b8").attr("font-size", "10px"))
2169
- .call(a => a.selectAll(".tick line").attr("stroke", "#e2e8f0").attr("stroke-dasharray", "2,2"))
2170
- .call(a => a.select(".domain").remove());
2171
- g.append("g").attr("transform", `translate(0,${ih})`).call(d3.axisBottom(x).ticks(6).tickFormat((_, i) => months[i]).tickSize(0))
2172
- .call(a => a.selectAll("text").attr("fill", "#94a3b8").attr("font-size", "11px"))
2173
- .call(a => a.select(".domain").remove());
2174
- const series = [{ key: "austin", color: "#5BC8C8" }, { key: "nyc", color: "#6366f1" }, { key: "chicago", color: "#f59e0b" }, { key: "sf", color: "#ef4444" }];
2175
- series.forEach(s => {
2176
- const line = d3.line().x(d => x(d.x)).y(d => y(d[s.key])).curve(d3.curveMonotoneX);
2177
- g.append("path").datum(data).attr("fill", "none").attr("stroke", s.color).attr("stroke-width", 2.5).attr("d", line);
2178
- g.selectAll(`.dot-${s.key}`).data(data).join("circle").attr("cx", d => x(d.x)).attr("cy", d => y(d[s.key])).attr("r", 3).attr("fill", s.color);
2179
- });
2180
- }}
2181
- height={240}
2182
- responsive
2183
- />
2184
- </div>
2185
-
2186
- {/* Occupancy by Day of Week */}
2187
- <div className="bg-white dark:bg-[var(--color-dash-text)] rounded-2xl p-8 border border-[var(--color-dash-label)]/20 dark:border-[var(--color-dash-muted)]/30 shadow-sm">
2188
- <div className="flex items-center justify-between mb-6">
2189
- <div>
2190
- <h3 className="text-lg font-bold text-[var(--color-dash-text)] dark:text-white">Occupancy by Day</h3>
2191
- <p className="text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] mt-1">Average occupancy rate by day of the week</p>
2192
- </div>
2193
- </div>
2194
- <D3Chart
2195
- data={[
2196
- { x: "Mon", value: 68 },
2197
- { x: "Tue", value: 72 },
2198
- { x: "Wed", value: 76 },
2199
- { x: "Thu", value: 82 },
2200
- { x: "Fri", value: 91 },
2201
- { x: "Sat", value: 94 },
2202
- { x: "Sun", value: 85 },
2203
- ]}
2204
- renderChart={(svg, data, dims) => {
2205
- const sel = d3.select(svg);
2206
- sel.selectAll("*").remove();
2207
- const m = { top: 12, right: 16, bottom: 32, left: 40 };
2208
- const w = dims.width, h = dims.height;
2209
- const iw = w - m.left - m.right, ih = h - m.top - m.bottom;
2210
- sel.attr("viewBox", `0 0 ${w} ${h}`);
2211
- const g = sel.append("g").attr("transform", `translate(${m.left},${m.top})`);
2212
- const x = d3.scaleBand().domain(data.map(d => d.x)).range([0, iw]).padding(0.35);
2213
- const y = d3.scaleLinear().domain([0, 100]).range([ih, 0]);
2214
- g.append("g").call(d3.axisLeft(y).ticks(5).tickFormat(d => d + "%").tickSize(-iw))
2215
- .call(a => a.selectAll("text").attr("fill", "#94a3b8").attr("font-size", "10px"))
2216
- .call(a => a.selectAll(".tick line").attr("stroke", "#e2e8f0").attr("stroke-dasharray", "2,2"))
2217
- .call(a => a.select(".domain").remove());
2218
- g.append("g").attr("transform", `translate(0,${ih})`).call(d3.axisBottom(x).tickSize(0))
2219
- .call(a => a.selectAll("text").attr("fill", "#94a3b8").attr("font-size", "11px"))
2220
- .call(a => a.select(".domain").remove());
2221
- g.selectAll("rect").data(data).join("rect")
2222
- .attr("x", d => x(d.x)).attr("y", d => y(d.value))
2223
- .attr("width", x.bandwidth()).attr("height", d => ih - y(d.value))
2224
- .attr("rx", 4).attr("fill", d => d.value >= 90 ? "#5BC8C8" : d.value >= 80 ? "#5BC8C8cc" : "#5BC8C880");
2225
- g.selectAll(".val-label").data(data).join("text")
2226
- .attr("x", d => x(d.x) + x.bandwidth() / 2).attr("y", d => y(d.value) - 6)
2227
- .attr("text-anchor", "middle").attr("fill", "#64748b").attr("font-size", "10px").attr("font-weight", "600")
2228
- .text(d => d.value + "%");
2229
- }}
2230
- height={240}
2231
- responsive
2232
- />
2233
- </div>
2234
- </div>
2235
-
2236
- {/* Revenue Breakdown Table */}
2237
- <div className="bg-white dark:bg-[var(--color-dash-text)] rounded-2xl p-8 border border-[var(--color-dash-label)]/20 dark:border-[var(--color-dash-muted)]/30 shadow-sm">
2238
- <div className="flex items-center justify-between mb-6">
2239
- <div>
2240
- <h3 className="text-lg font-bold text-[var(--color-dash-text)] dark:text-white">Revenue Breakdown by Property</h3>
2241
- <p className="text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] mt-1">6-month performance comparison across all properties</p>
2242
- </div>
2243
- </div>
2244
- <div className="overflow-x-auto">
2245
- <table className="w-full">
2246
- <thead className="border-b border-[var(--color-dash-label)]/20 dark:border-[var(--color-dash-muted)]/30">
2247
- <tr>
2248
- {["Property", "Revenue", "ADR", "Occupancy", "RevPAR", "Bookings", "Cancellations", "Net Growth"].map((h) => (
2249
- <th key={h} className="px-4 py-3 text-left text-xs font-semibold text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] uppercase tracking-wider">{h}</th>
2250
- ))}
2251
- </tr>
2252
- </thead>
2253
- <tbody className="divide-y divide-[var(--color-dash-label)]/10 dark:divide-[var(--color-dash-muted)]/20">
2254
- {[
2255
- { name: "Austin Convention Center", revenue: "$97,600", adr: "$189", occupancy: "82%", revpar: "$155", bookings: 516, cancellations: 12, growth: "+60%" },
2256
- { name: "Midtown NYC", revenue: "$73,100", adr: "$215", occupancy: "79%", revpar: "$170", bookings: 340, cancellations: 8, growth: "+55%" },
2257
- { name: "Chicago Downtown", revenue: "$59,000", adr: "$165", occupancy: "76%", revpar: "$125", bookings: 358, cancellations: 6, growth: "+75%" },
2258
- { name: "San Francisco Bay", revenue: "$53,300", adr: "$198", occupancy: "74%", revpar: "$147", bookings: 269, cancellations: 15, growth: "+100%" },
2259
- ].map((r) => (
2260
- <tr key={r.name} className="hover:bg-[var(--color-dash-surface)]/50 dark:hover:bg-[var(--color-dash-muted)]/5 transition-colors">
2261
- <td className="px-4 py-3 text-sm font-semibold text-[var(--color-dash-text)] dark:text-white">{r.name}</td>
2262
- <td className="px-4 py-3 text-sm font-bold text-[var(--color-dash-text)] dark:text-white">{r.revenue}</td>
2263
- <td className="px-4 py-3 text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]">{r.adr}</td>
2264
- <td className="px-4 py-3 text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]">{r.occupancy}</td>
2265
- <td className="px-4 py-3 text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]">{r.revpar}</td>
2266
- <td className="px-4 py-3 text-sm text-[var(--color-dash-text)] dark:text-white">{r.bookings}</td>
2267
- <td className="px-4 py-3 text-sm text-[var(--color-dash-danger)]">{r.cancellations}</td>
2268
- <td className="px-4 py-3 text-sm font-bold text-[var(--color-dash-success)]">{r.growth}</td>
2269
- </tr>
2270
- ))}
2271
- </tbody>
2272
- </table>
2273
- </div>
2274
- </div>
2275
-
2276
- {/* AI Analytics Insight */}
2277
- <div className="bg-gradient-to-r from-[var(--color-dash-dark)] to-[var(--color-dash-dark)]/90 rounded-xl p-5 flex items-start gap-4">
2278
- <div className="flex-shrink-0 h-9 w-9 rounded-lg bg-white/10 flex items-center justify-center">
2279
- <SparklesIcon className="h-5 w-5 text-[var(--color-dash-accent)]" />
2280
- </div>
2281
- <div className="flex-1">
2282
- <p className="text-sm font-semibold text-white mb-1">Revenue Acceleration Opportunity</p>
2283
- <p className="text-xs text-white/60 leading-relaxed">Based on Tableau data, San Francisco Bay has the highest growth rate (+100%) but lowest absolute revenue. Increasing its occupancy from 74% to 82% (matching Austin) would generate an additional $14,200/month. The main constraint is response time (3.2h), which correlates with lower rebooking rates.</p>
2284
- </div>
2285
- </div>
2286
- </div>
2287
- )}
2288
-
2289
- {/* DATA 360 TAB: added by headless-data-360 skill during demo */}
2290
-
2291
- {/* Footer */}
2292
- <div className="text-center py-8 text-sm text-[var(--color-dash-label)]">
2293
- <p>© 2026 Engine · Welcome to modern travel management</p>
2294
- </div>
2295
- </div>
2296
-
2297
- {/* Properties Modal */}
2298
- <Modal isOpen={isPropertiesModalOpen} onOpenChange={setIsPropertiesModalOpen}>
2299
- <Modal.Backdrop>
2300
- <Modal.Container>
2301
- <Modal.Dialog className="max-w-4xl">
2302
- <Modal.CloseTrigger />
2303
- <Modal.Header>
2304
- <Modal.Heading className="text-2xl font-bold text-[var(--color-dash-text)] dark:text-white">
2305
- Your Properties
2306
- </Modal.Heading>
2307
- </Modal.Header>
2308
- <Modal.Body className="space-y-4">
2309
- <p className="text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] mb-4">
2310
- {myProperties} active properties in your portfolio
2311
- </p>
2312
- <div className="space-y-3">
2313
- {revenueTrendByProperty.properties?.map((prop, idx) => (
2314
- <div
2315
- key={idx}
2316
- className="bg-[var(--color-dash-surface)] dark:bg-[var(--color-dash-muted)]/10 rounded-xl p-5 border border-[var(--color-dash-label)]/20 dark:border-[var(--color-dash-muted)]/30 hover:border-[var(--color-dash-accent)] transition-colors"
2317
- >
2318
- <div className="flex items-start justify-between gap-4">
2319
- <div className="flex items-start gap-3 flex-1">
2320
- <div className="mt-1">
2321
- <BuildingOfficeIcon className="h-5 w-5 text-[var(--color-dash-accent)]" />
2322
- </div>
2323
- <div>
2324
- <h4 className="font-semibold text-[var(--color-dash-text)] dark:text-white mb-1">
2325
- {prop.name}
2326
- </h4>
2327
- <p className="text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]">
2328
- Latest revenue: ${prop.values[prop.values.length - 1].toLocaleString()}
2329
- </p>
2330
- </div>
2331
- </div>
2332
- <span
2333
- className="inline-block h-3 w-3 rounded-full flex-shrink-0 mt-2"
2334
- style={{ backgroundColor: prop.color }}
2335
- />
2336
- </div>
2337
- </div>
2338
- ))}
2339
- </div>
2340
- </Modal.Body>
2341
- <Modal.Footer>
2342
- <Modal.CloseTrigger asChild>
2343
- <button className="px-6 py-2 bg-[var(--color-dash-text)] dark:bg-white hover:bg-[var(--color-dash-muted)] dark:hover:bg-[var(--color-dash-surface)] text-white dark:text-[var(--color-dash-text)] font-semibold rounded-lg transition-colors">
2344
- Close
2345
- </button>
2346
- </Modal.CloseTrigger>
2347
- </Modal.Footer>
2348
- </Modal.Dialog>
2349
- </Modal.Container>
2350
- </Modal.Backdrop>
2351
- </Modal>
2352
-
2353
- {/* Revenue Modal */}
2354
- <Modal isOpen={isRevenueModalOpen} onOpenChange={setIsRevenueModalOpen}>
2355
- <Modal.Backdrop>
2356
- <Modal.Container>
2357
- <Modal.Dialog className="max-w-4xl">
2358
- <Modal.CloseTrigger />
2359
- <Modal.Header>
2360
- <Modal.Heading className="text-2xl font-bold text-[var(--color-dash-text)] dark:text-white">
2361
- Revenue Breakdown
2362
- </Modal.Heading>
2363
- </Modal.Header>
2364
- <Modal.Body className="space-y-4">
2365
- <div className="bg-gradient-to-br from-[var(--color-dash-success)]/10 to-[var(--color-dash-accent)]/10 rounded-xl p-6 mb-4">
2366
- <p className="text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] mb-2">Total Revenue</p>
2367
- <p className="text-4xl font-bold text-[var(--color-dash-text)] dark:text-white">
2368
- ${myRevenue.toLocaleString()}
2369
- </p>
2370
- </div>
2371
- <h3 className="font-semibold text-[var(--color-dash-text)] dark:text-white mb-3">By Property</h3>
2372
- <div className="space-y-3">
2373
- {revenueTrendByProperty.properties?.map((prop, idx) => {
2374
- const totalRevenue = prop.values.reduce((sum, val) => sum + val, 0);
2375
- const percentage = ((totalRevenue / myRevenue) * 100).toFixed(1);
2376
- return (
2377
- <div
2378
- key={idx}
2379
- className="bg-[var(--color-dash-surface)] dark:bg-[var(--color-dash-muted)]/10 rounded-xl p-5 border border-[var(--color-dash-label)]/20 dark:border-[var(--color-dash-muted)]/30"
2380
- >
2381
- <div className="flex items-center justify-between mb-3">
2382
- <div className="flex items-center gap-3">
2383
- <span
2384
- className="inline-block h-3 w-3 rounded-full"
2385
- style={{ backgroundColor: prop.color }}
2386
- />
2387
- <span className="font-semibold text-[var(--color-dash-text)] dark:text-white">
2388
- {prop.name}
2389
- </span>
2390
- </div>
2391
- <span className="text-lg font-bold text-[var(--color-dash-text)] dark:text-white">
2392
- ${totalRevenue.toLocaleString()}
2393
- </span>
2394
- </div>
2395
- <div className="flex items-center gap-3">
2396
- <div className="flex-1 bg-[var(--color-dash-label)]/20 rounded-full h-2">
2397
- <div
2398
- className="h-2 rounded-full"
2399
- style={{
2400
- width: `${percentage}%`,
2401
- backgroundColor: prop.color,
2402
- }}
2403
- />
2404
- </div>
2405
- <span className="text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] w-12 text-right">
2406
- {percentage}%
2407
- </span>
2408
- </div>
2409
- </div>
2410
- );
2411
- })}
2412
- </div>
2413
- </Modal.Body>
2414
- <Modal.Footer>
2415
- <button
2416
- onClick={() => setIsRevenueModalOpen(false)}
2417
- className="px-6 py-2 bg-[var(--color-dash-text)] dark:bg-white hover:bg-[var(--color-dash-muted)] dark:hover:bg-[var(--color-dash-surface)] text-white dark:text-[var(--color-dash-text)] font-semibold rounded-lg transition-colors"
2418
- >
2419
- Close
2420
- </button>
2421
- </Modal.Footer>
2422
- </Modal.Dialog>
2423
- </Modal.Container>
2424
- </Modal.Backdrop>
2425
- </Modal>
2426
-
2427
- {/* Reservations Modal */}
2428
- <Modal isOpen={isReservationsModalOpen} onOpenChange={setIsReservationsModalOpen}>
2429
- <Modal.Backdrop>
2430
- <Modal.Container>
2431
- <Modal.Dialog className="max-w-4xl">
2432
- <Modal.CloseTrigger />
2433
- <Modal.Header>
2434
- <Modal.Heading className="text-2xl font-bold text-[var(--color-dash-text)] dark:text-white">
2435
- Reservations
2436
- </Modal.Heading>
2437
- </Modal.Header>
2438
- <Modal.Body>
2439
- <p className="text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] mb-4">
2440
- {myReservations} total reservations through Engine
2441
- </p>
2442
- <div className="bg-[var(--color-dash-surface)] dark:bg-[var(--color-dash-muted)]/10 rounded-xl p-8 text-center">
2443
- <ClockIcon className="h-12 w-12 text-[var(--color-dash-info)] mx-auto mb-3" />
2444
- <p className="text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]">
2445
- Detailed reservation data is available in your full property management dashboard
2446
- </p>
2447
- </div>
2448
- </Modal.Body>
2449
- <Modal.Footer>
2450
- <button
2451
- onClick={() => setIsReservationsModalOpen(false)}
2452
- className="px-6 py-2 bg-[var(--color-dash-text)] dark:bg-white hover:bg-[var(--color-dash-muted)] dark:hover:bg-[var(--color-dash-surface)] text-white dark:text-[var(--color-dash-text)] font-semibold rounded-lg transition-colors"
2453
- >
2454
- Close
2455
- </button>
2456
- </Modal.Footer>
2457
- </Modal.Dialog>
2458
- </Modal.Container>
2459
- </Modal.Backdrop>
2460
- </Modal>
2461
-
2462
- {/* Disputes Modal */}
2463
- <Modal isOpen={isDisputesModalOpen} onOpenChange={setIsDisputesModalOpen}>
2464
- <Modal.Backdrop>
2465
- <Modal.Container>
2466
- <Modal.Dialog className="max-w-4xl">
2467
- <Modal.CloseTrigger />
2468
- <Modal.Header>
2469
- <Modal.Heading className="text-2xl font-bold text-[var(--color-dash-text)] dark:text-white">
2470
- Items to Review
2471
- </Modal.Heading>
2472
- </Modal.Header>
2473
- <Modal.Body className="space-y-4">
2474
- <p className="text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] mb-4">
2475
- {myOpenDisputes} {myOpenDisputes === 1 ? 'item needs' : 'items need'} your attention
2476
- </p>
2477
- {myDisputes.length > 0 ? (
2478
- <div className="space-y-3">
2479
- {myDisputes.map((d) => (
2480
- <div
2481
- key={d.id}
2482
- className="bg-[var(--color-dash-surface)] dark:bg-[var(--color-dash-muted)]/10 rounded-xl p-5 border border-[var(--color-dash-label)]/20 dark:border-[var(--color-dash-muted)]/30 hover:border-[var(--color-dash-warning)] transition-colors"
2483
- >
2484
- <div className="flex items-start justify-between gap-3 mb-2">
2485
- <h4 className="font-semibold text-[var(--color-dash-text)] dark:text-white flex-1">
2486
- {d.title}
2487
- </h4>
2488
- <span
2489
- className={`inline-flex items-center rounded-full px-2 py-1 text-xs font-medium flex-shrink-0 ${
2490
- d.status === "critical"
2491
- ? "bg-[var(--color-dash-danger)]/10 text-[var(--color-dash-danger)] border border-[var(--color-dash-danger)]/30"
2492
- : d.status === "warning"
2493
- ? "bg-[var(--color-dash-warning)]/10 text-[var(--color-dash-warning)] border border-[var(--color-dash-warning)]/30"
2494
- : "bg-[var(--color-dash-info)]/10 text-[var(--color-dash-info)] border border-[var(--color-dash-info)]/30"
2495
- }`}
2496
- >
2497
- {d.badge}
2498
- </span>
2499
- </div>
2500
- <p className="text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] mb-3">
2501
- {d.description}
2502
- </p>
2503
- <div className="flex items-center justify-between">
2504
- <span className="text-lg font-bold text-[var(--color-dash-text)] dark:text-white">
2505
- ${d.amount.toLocaleString()}
2506
- </span>
2507
- {d.agentHandled && (
2508
- <span className="text-xs text-[var(--color-dash-accent)] flex items-center gap-1">
2509
- <svg className="h-3 w-3" fill="currentColor" viewBox="0 0 20 20">
2510
- <path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
2511
- </svg>
2512
- Agent reviewed
2513
- </span>
2514
- )}
2515
- </div>
2516
- </div>
2517
- ))}
2518
- </div>
2519
- ) : (
2520
- <div className="bg-[var(--color-dash-surface)] dark:bg-[var(--color-dash-muted)]/10 rounded-xl p-8 text-center">
2521
- <p className="text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]">No items need attention</p>
2522
- </div>
2523
- )}
2524
- </Modal.Body>
2525
- <Modal.Footer>
2526
- <button
2527
- onClick={() => setIsDisputesModalOpen(false)}
2528
- className="px-6 py-2 bg-[var(--color-dash-text)] dark:bg-white hover:bg-[var(--color-dash-muted)] dark:hover:bg-[var(--color-dash-surface)] text-white dark:text-[var(--color-dash-text)] font-semibold rounded-lg transition-colors"
2529
- >
2530
- Close
2531
- </button>
2532
- </Modal.Footer>
2533
- </Modal.Dialog>
2534
- </Modal.Container>
2535
- </Modal.Backdrop>
2536
- </Modal>
2537
-
2538
- {/* Penalty Modal */}
2539
- <Modal isOpen={isPenaltyModalOpen} onOpenChange={setIsPenaltyModalOpen}>
2540
- <Modal.Backdrop>
2541
- <Modal.Container>
2542
- <Modal.Dialog className="w-[95vw] max-w-4xl max-h-[90vh] overflow-y-auto">
2543
- <Modal.CloseTrigger />
2544
- <Modal.Header className="p-8 pb-4">
2545
- <Modal.Heading className="text-2xl font-bold text-[var(--color-dash-text)] dark:text-white pr-8">
2546
- Penalty Review: {selectedPenalty?.name}
2547
- </Modal.Heading>
2548
- </Modal.Header>
2549
- <Modal.Body className="space-y-6 px-8 py-4">
2550
- {selectedPenalty && (
2551
- <>
2552
- {/* Property & Customer Info */}
2553
- <div className="grid grid-cols-2 gap-4">
2554
- <div>
2555
- <p className="text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] mb-1">Property</p>
2556
- <p className="font-semibold text-[var(--color-dash-text)] dark:text-white">{selectedPenalty.property}</p>
2557
- </div>
2558
- <div>
2559
- <p className="text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] mb-1">Customer</p>
2560
- <p className="font-semibold text-[var(--color-dash-text)] dark:text-white">{selectedPenalty.customer}</p>
2561
- </div>
2562
- </div>
2563
-
2564
- {/* Booking Details */}
2565
- <div className="bg-[var(--color-dash-surface)] dark:bg-[var(--color-dash-muted)]/10 rounded-xl p-6">
2566
- <h4 className="font-semibold text-[var(--color-dash-text)] dark:text-white mb-4">Booking Details</h4>
2567
- <div className="grid grid-cols-3 gap-6">
2568
- <div>
2569
- <p className="text-xs text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] mb-2 uppercase tracking-wider">Original Block</p>
2570
- <p className="text-xl font-bold text-[var(--color-dash-text)] dark:text-white">{selectedPenalty.originalRoomBlock} rooms</p>
2571
- </div>
2572
- <div>
2573
- <p className="text-xs text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] mb-2 uppercase tracking-wider">Rooms Used</p>
2574
- <p className="text-xl font-bold text-[var(--color-dash-success)]">{selectedPenalty.actualRoomsUsed} rooms</p>
2575
- </div>
2576
- <div>
2577
- <p className="text-xs text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] mb-2 uppercase tracking-wider">Unused</p>
2578
- <p className="text-xl font-bold text-[var(--color-dash-danger)]">{selectedPenalty.unusedRooms} rooms</p>
2579
- </div>
2580
- </div>
2581
- <div className="grid grid-cols-3 gap-6 mt-6 pt-6 border-t border-[var(--color-dash-label)]/20 dark:border-[var(--color-dash-muted)]/30">
2582
- <div>
2583
- <p className="text-xs text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] mb-2 uppercase tracking-wider">Room Rate</p>
2584
- <p className="text-xl font-bold text-[var(--color-dash-text)] dark:text-white">${selectedPenalty.roomRate}/night</p>
2585
- </div>
2586
- <div>
2587
- <p className="text-xs text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] mb-2 uppercase tracking-wider">Number of Nights</p>
2588
- <p className="text-xl font-bold text-[var(--color-dash-text)] dark:text-white">{selectedPenalty.numberOfNights}</p>
2589
- </div>
2590
- <div>
2591
- <p className="text-xs text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] mb-2 uppercase tracking-wider">Rooms Resold</p>
2592
- <p className="text-xl font-bold text-[var(--color-dash-accent)]">{selectedPenalty.roomsResold} rooms</p>
2593
- </div>
2594
- </div>
2595
- </div>
2596
-
2597
- {/* Calculation Method & Policy */}
2598
- <div className="grid grid-cols-2 gap-6">
2599
- <div>
2600
- <p className="text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] mb-2">Calculation Method</p>
2601
- <p className="text-lg font-semibold text-[var(--color-dash-text)] dark:text-white">{selectedPenalty.method}</p>
2602
- </div>
2603
- <div>
2604
- <p className="text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] mb-2">Resale Policy</p>
2605
- <p className="text-lg font-semibold text-[var(--color-dash-text)] dark:text-white">{selectedPenalty.resalePolicy}</p>
2606
- </div>
2607
- </div>
2608
-
2609
- {/* Financial Details */}
2610
- <div>
2611
- <h4 className="font-semibold text-[var(--color-dash-text)] dark:text-white mb-4">Financial Summary</h4>
2612
- <div className="grid grid-cols-3 gap-6">
2613
- <div className="bg-[var(--color-dash-surface)] dark:bg-[var(--color-dash-muted)]/10 rounded-lg p-5">
2614
- <p className="text-xs text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] mb-2 uppercase tracking-wider">Calculated Penalty</p>
2615
- <p className="text-2xl font-bold text-[var(--color-dash-text)] dark:text-white">${selectedPenalty.penaltyCalculated.toLocaleString()}</p>
2616
- </div>
2617
- <div className="bg-[var(--color-dash-surface)] dark:bg-[var(--color-dash-muted)]/10 rounded-lg p-5">
2618
- <p className="text-xs text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] mb-2 uppercase tracking-wider">Credit Applied</p>
2619
- <p className="text-2xl font-bold text-[var(--color-dash-success)]">${selectedPenalty.credit.toLocaleString()}</p>
2620
- </div>
2621
- <div className={`rounded-lg p-5 border ${selectedPenalty.isHero ? 'bg-[var(--color-dash-warning)]/10 border-[var(--color-dash-warning)]/30' : 'bg-[var(--color-dash-surface)] dark:bg-[var(--color-dash-muted)]/10 border-transparent'}`}>
2622
- <p className="text-xs text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] mb-2 uppercase tracking-wider">Final Penalty</p>
2623
- <p className={`text-2xl font-bold ${selectedPenalty.isHero ? 'text-[var(--color-dash-warning)]' : 'text-[var(--color-dash-text)] dark:text-white'}`}>${selectedPenalty.penalty.toLocaleString()}</p>
2624
- </div>
2625
- </div>
2626
- </div>
2627
-
2628
- {/* Issue Description */}
2629
- {selectedPenalty.isHero && (
2630
- <div className="bg-[var(--color-dash-warning)]/10 border-l-4 border-[var(--color-dash-warning)] rounded-lg p-5">
2631
- <p className="font-semibold text-[var(--color-dash-text)] dark:text-white mb-3 flex items-center gap-2">
2632
- <ExclamationTriangleIcon className="h-5 w-5 text-[var(--color-dash-warning)]" />
2633
- Why this needs review:
2634
- </p>
2635
- <p className="text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] leading-relaxed">
2636
- Your contract specifies a {selectedPenalty.resalePolicy.toLowerCase()}. Based on {selectedPenalty.roomsResold} rooms resold at ${selectedPenalty.roomRate}/night for {selectedPenalty.numberOfNights} {selectedPenalty.numberOfNights === 1 ? 'night' : 'nights'},
2637
- we'd expect to see a ${(selectedPenalty.roomsResold * selectedPenalty.roomRate * selectedPenalty.numberOfNights * 0.5).toLocaleString()} credit that doesn't appear to be fully applied.
2638
- </p>
2639
- </div>
2640
- )}
2641
-
2642
- {/* Status */}
2643
- <div>
2644
- <p className="text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] mb-2">Status</p>
2645
- <span
2646
- className={`inline-flex items-center rounded-full px-3 py-1 text-sm font-medium ${
2647
- selectedPenalty.status === "Approved"
2648
- ? "bg-[var(--color-dash-success)]/10 text-[var(--color-dash-success)] border border-[var(--color-dash-success)]/30"
2649
- : selectedPenalty.status === "Reviewed"
2650
- ? "bg-[var(--color-dash-info)]/10 text-[var(--color-dash-info)] border border-[var(--color-dash-info)]/30"
2651
- : selectedPenalty.status === "Calculated"
2652
- ? "bg-[var(--color-dash-label)]/10 text-[var(--color-dash-muted)] border border-[var(--color-dash-label)]/30"
2653
- : "bg-[var(--color-dash-warning)]/10 text-[var(--color-dash-warning)] border border-[var(--color-dash-warning)]/30"
2654
- }`}
2655
- >
2656
- {selectedPenalty.status}
2657
- </span>
2658
- </div>
2659
- </>
2660
- )}
2661
- </Modal.Body>
2662
- <Modal.Footer className="p-8 pt-4">
2663
- <div className="flex gap-4 justify-end flex-wrap">
2664
- <button
2665
- onClick={() => setIsPenaltyModalOpen(false)}
2666
- className="px-6 py-2.5 bg-transparent hover:bg-[var(--color-dash-surface)] dark:hover:bg-[var(--color-dash-muted)]/20 text-[var(--color-dash-text)] dark:text-white font-semibold rounded-lg border border-[var(--color-dash-label)]/30 dark:border-[var(--color-dash-muted)]/50 transition-colors"
2667
- >
2668
- Close
2669
- </button>
2670
- <button
2671
- onClick={() => {
2672
- toast.success("Dispute has been filed. Support team will follow up.");
2673
- setIsPenaltyModalOpen(false);
2674
- }}
2675
- className="px-6 py-2.5 bg-[var(--color-dash-text)] dark:bg-white hover:bg-[var(--color-dash-muted)] dark:hover:bg-[var(--color-dash-surface)] text-white dark:text-[var(--color-dash-text)] font-semibold rounded-lg transition-colors whitespace-nowrap"
2676
- >
2677
- File Dispute
2678
- </button>
2679
- </div>
2680
- </Modal.Footer>
2681
- </Modal.Dialog>
2682
- </Modal.Container>
2683
- </Modal.Backdrop>
2684
- </Modal>
2685
-
2686
- {/* Invoices Modal */}
2687
- <Modal isOpen={isInvoicesModalOpen} onOpenChange={setIsInvoicesModalOpen}>
2688
- <Modal.Backdrop>
2689
- <Modal.Container>
2690
- <Modal.Dialog className="max-w-4xl">
2691
- <Modal.CloseTrigger />
2692
- <Modal.Header>
2693
- <Modal.Heading className="text-2xl font-bold text-[var(--color-dash-text)] dark:text-white">
2694
- Your Invoices
2695
- </Modal.Heading>
2696
- </Modal.Header>
2697
- <Modal.Body className="space-y-4">
2698
- <p className="text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] mb-4">
2699
- {myPendingInvoices} {myPendingInvoices === 1 ? 'invoice' : 'invoices'} ready to pay
2700
- </p>
2701
- {myInvoices.length > 0 ? (
2702
- <div className="space-y-3">
2703
- {myInvoices.map((inv) => (
2704
- <div
2705
- key={inv.id}
2706
- className="bg-[var(--color-dash-surface)] dark:bg-[var(--color-dash-muted)]/10 rounded-xl p-5 border border-[var(--color-dash-label)]/20 dark:border-[var(--color-dash-muted)]/30 hover:border-[var(--color-dash-success)] transition-colors"
2707
- >
2708
- <div className="flex items-start justify-between gap-3 mb-2">
2709
- <div>
2710
- <h4 className="font-semibold text-[var(--color-dash-text)] dark:text-white">
2711
- {inv.title}
2712
- </h4>
2713
- <p className="text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] mt-1">
2714
- {inv.description}
2715
- </p>
2716
- </div>
2717
- <span
2718
- className={`inline-flex items-center rounded-full px-2 py-1 text-xs font-medium flex-shrink-0 ${
2719
- inv.status === "critical"
2720
- ? "bg-[var(--color-dash-danger)]/10 text-[var(--color-dash-danger)] border border-[var(--color-dash-danger)]/30"
2721
- : "bg-[var(--color-dash-info)]/10 text-[var(--color-dash-info)] border border-[var(--color-dash-info)]/30"
2722
- }`}
2723
- >
2724
- {inv.badge}
2725
- </span>
2726
- </div>
2727
- <div className="flex items-center justify-between mt-3">
2728
- <span className="text-lg font-bold text-[var(--color-dash-text)] dark:text-white">
2729
- ${inv.amount.toLocaleString()}
2730
- </span>
2731
- <span className="text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]">
2732
- Due {inv.due}
2733
- </span>
2734
- </div>
2735
- </div>
2736
- ))}
2737
- </div>
2738
- ) : (
2739
- <div className="bg-[var(--color-dash-surface)] dark:bg-[var(--color-dash-muted)]/10 rounded-xl p-8 text-center">
2740
- <p className="text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]">No pending invoices</p>
2741
- </div>
2742
- )}
2743
- </Modal.Body>
2744
- <Modal.Footer>
2745
- <button
2746
- onClick={() => setIsInvoicesModalOpen(false)}
2747
- className="px-6 py-2 bg-[var(--color-dash-text)] dark:bg-white hover:bg-[var(--color-dash-muted)] dark:hover:bg-[var(--color-dash-surface)] text-white dark:text-[var(--color-dash-text)] font-semibold rounded-lg transition-colors"
2748
- >
2749
- Close
2750
- </button>
2751
- </Modal.Footer>
2752
- </Modal.Dialog>
2753
- </Modal.Container>
2754
- </Modal.Backdrop>
2755
- </Modal>
2756
-
2757
- <AgentPanel />
2758
- </div>
2759
- );
2760
- }