@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.
- package/CHANGELOG.md +16 -0
- package/CLAUDE.md +12 -13
- package/README.md +0 -15
- package/dist/styles/global.css +44 -57
- package/package.json +1 -2
- package/scripts/apply-brand.mjs +47 -30
- package/scripts/postinstall.mjs +1 -11
- package/src/styles/global.css +44 -57
- package/brands/engine/PARTNER_HUB_PRD.md +0 -584
- package/brands/engine/agentApiConfig.ts +0 -36
- package/brands/engine/app/api/graphql-operations-types.ts +0 -11260
- package/brands/engine/app/api/graphqlClient.ts +0 -25
- package/brands/engine/app/api/partnerQueries.ts +0 -212
- package/brands/engine/app/appLayout.tsx +0 -5
- package/brands/engine/app/components/AgentPanel.tsx +0 -541
- package/brands/engine/app/components/AgentforceConversationClient.tsx +0 -201
- package/brands/engine/app/components/Data360Widget.tsx +0 -301
- package/brands/engine/app/components/__inherit_AgentforceConversationClient.tsx +0 -3
- package/brands/engine/app/components/alerts/status-alert.tsx +0 -49
- package/brands/engine/app/components/layouts/card-layout.tsx +0 -29
- package/brands/engine/app/components/workspace/CommandCenter.tsx +0 -16
- package/brands/engine/app/config/agentApi.ts +0 -36
- package/brands/engine/app/data/partner-hub-sample-data.js +0 -297
- package/brands/engine/app/features/object-search/__examples__/api/accountSearchService.ts +0 -46
- package/brands/engine/app/features/object-search/__examples__/api/query/distinctAccountIndustries.graphql +0 -19
- package/brands/engine/app/features/object-search/__examples__/api/query/distinctAccountTypes.graphql +0 -19
- package/brands/engine/app/features/object-search/__examples__/api/query/getAccountDetail.graphql +0 -121
- package/brands/engine/app/features/object-search/__examples__/api/query/searchAccounts.graphql +0 -51
- package/brands/engine/app/features/object-search/__examples__/pages/AccountObjectDetailPage.tsx +0 -357
- package/brands/engine/app/features/object-search/__examples__/pages/AccountSearch.tsx +0 -312
- package/brands/engine/app/features/object-search/__examples__/pages/Home.tsx +0 -34
- package/brands/engine/app/features/object-search/api/objectSearchService.ts +0 -84
- package/brands/engine/app/features/object-search/components/ActiveFilters.tsx +0 -89
- package/brands/engine/app/features/object-search/components/FilterContext.tsx +0 -83
- package/brands/engine/app/features/object-search/components/ObjectBreadcrumb.tsx +0 -66
- package/brands/engine/app/features/object-search/components/PaginationControls.tsx +0 -109
- package/brands/engine/app/features/object-search/components/SearchBar.tsx +0 -41
- package/brands/engine/app/features/object-search/components/SortControl.tsx +0 -143
- package/brands/engine/app/features/object-search/components/filters/BooleanFilter.tsx +0 -78
- package/brands/engine/app/features/object-search/components/filters/DateFilter.tsx +0 -128
- package/brands/engine/app/features/object-search/components/filters/DateRangeFilter.tsx +0 -70
- package/brands/engine/app/features/object-search/components/filters/FilterFieldWrapper.tsx +0 -33
- package/brands/engine/app/features/object-search/components/filters/MultiSelectFilter.tsx +0 -97
- package/brands/engine/app/features/object-search/components/filters/NumericRangeFilter.tsx +0 -163
- package/brands/engine/app/features/object-search/components/filters/SearchFilter.tsx +0 -50
- package/brands/engine/app/features/object-search/components/filters/SelectFilter.tsx +0 -97
- package/brands/engine/app/features/object-search/components/filters/TextFilter.tsx +0 -91
- package/brands/engine/app/features/object-search/hooks/useAsyncData.ts +0 -54
- package/brands/engine/app/features/object-search/hooks/useCachedAsyncData.ts +0 -184
- package/brands/engine/app/features/object-search/hooks/useDebouncedCallback.ts +0 -34
- package/brands/engine/app/features/object-search/hooks/useObjectSearchParams.ts +0 -252
- package/brands/engine/app/features/object-search/utils/debounce.ts +0 -25
- package/brands/engine/app/features/object-search/utils/fieldUtils.ts +0 -29
- package/brands/engine/app/features/object-search/utils/filterUtils.ts +0 -404
- package/brands/engine/app/features/object-search/utils/sortUtils.ts +0 -38
- package/brands/engine/app/hooks/useEngineLiveData.ts +0 -49
- package/brands/engine/app/hooks/useEvaAgent.ts +0 -288
- package/brands/engine/app/hooks/usePartnerDashboardData.ts +0 -141
- package/brands/engine/app/navigationMenu.tsx +0 -80
- package/brands/engine/app/pages/AccountObjectDetailPage.tsx +0 -361
- package/brands/engine/app/pages/AccountSearch.tsx +0 -305
- package/brands/engine/app/pages/BlankDashboard.tsx +0 -15
- package/brands/engine/app/pages/DataTest.tsx +0 -78
- package/brands/engine/app/pages/Home.tsx +0 -5
- package/brands/engine/app/pages/NotFound.tsx +0 -19
- package/brands/engine/app/pages/PartnerHubDashboard.tsx +0 -2760
- package/brands/engine/app/pages/Search.tsx +0 -13
- package/brands/engine/app/router-utils.tsx +0 -35
- package/brands/engine/app/routes.tsx +0 -39
- package/brands/engine/app/styles/global.css +0 -269
- package/brands/engine/brand.css +0 -40
- package/brands/engine/engine-command-center-prd.md +0 -575
- package/brands/engine/engine-live-data.js +0 -135
- package/brands/engine/engine-sample-data.js +0 -378
- package/brands/engine/engine_logo.png +0 -0
- package/brands/engine/global.css +0 -269
- package/brands/engine/partner-hub-sample-data.js +0 -281
- package/brands/engine/schema.graphql +0 -292
- package/brands/engine/useEngineLiveData.ts +0 -49
- 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
|
-
}
|