@schandlergarcia/sf-web-components 2.3.16 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/CLAUDE.md +12 -13
  3. package/README.md +0 -15
  4. package/dist/styles/global.css +46 -48
  5. package/package.json +1 -2
  6. package/scripts/apply-brand.mjs +47 -30
  7. package/scripts/postinstall.mjs +1 -11
  8. package/src/styles/global.css +46 -48
  9. package/brands/engine/PARTNER_HUB_PRD.md +0 -584
  10. package/brands/engine/agentApiConfig.ts +0 -36
  11. package/brands/engine/app/api/graphql-operations-types.ts +0 -11260
  12. package/brands/engine/app/api/graphqlClient.ts +0 -25
  13. package/brands/engine/app/api/partnerQueries.ts +0 -212
  14. package/brands/engine/app/appLayout.tsx +0 -5
  15. package/brands/engine/app/components/AgentPanel.tsx +0 -402
  16. package/brands/engine/app/components/AgentforceConversationClient.tsx +0 -201
  17. package/brands/engine/app/components/__inherit_AgentforceConversationClient.tsx +0 -3
  18. package/brands/engine/app/components/alerts/status-alert.tsx +0 -49
  19. package/brands/engine/app/components/layouts/card-layout.tsx +0 -29
  20. package/brands/engine/app/components/workspace/CommandCenter.tsx +0 -16
  21. package/brands/engine/app/config/agentApi.ts +0 -36
  22. package/brands/engine/app/features/object-search/__examples__/api/accountSearchService.ts +0 -46
  23. package/brands/engine/app/features/object-search/__examples__/api/query/distinctAccountIndustries.graphql +0 -19
  24. package/brands/engine/app/features/object-search/__examples__/api/query/distinctAccountTypes.graphql +0 -19
  25. package/brands/engine/app/features/object-search/__examples__/api/query/getAccountDetail.graphql +0 -121
  26. package/brands/engine/app/features/object-search/__examples__/api/query/searchAccounts.graphql +0 -51
  27. package/brands/engine/app/features/object-search/__examples__/pages/AccountObjectDetailPage.tsx +0 -357
  28. package/brands/engine/app/features/object-search/__examples__/pages/AccountSearch.tsx +0 -312
  29. package/brands/engine/app/features/object-search/__examples__/pages/Home.tsx +0 -34
  30. package/brands/engine/app/features/object-search/api/objectSearchService.ts +0 -84
  31. package/brands/engine/app/features/object-search/components/ActiveFilters.tsx +0 -89
  32. package/brands/engine/app/features/object-search/components/FilterContext.tsx +0 -83
  33. package/brands/engine/app/features/object-search/components/ObjectBreadcrumb.tsx +0 -66
  34. package/brands/engine/app/features/object-search/components/PaginationControls.tsx +0 -109
  35. package/brands/engine/app/features/object-search/components/SearchBar.tsx +0 -41
  36. package/brands/engine/app/features/object-search/components/SortControl.tsx +0 -143
  37. package/brands/engine/app/features/object-search/components/filters/BooleanFilter.tsx +0 -78
  38. package/brands/engine/app/features/object-search/components/filters/DateFilter.tsx +0 -128
  39. package/brands/engine/app/features/object-search/components/filters/DateRangeFilter.tsx +0 -70
  40. package/brands/engine/app/features/object-search/components/filters/FilterFieldWrapper.tsx +0 -33
  41. package/brands/engine/app/features/object-search/components/filters/MultiSelectFilter.tsx +0 -97
  42. package/brands/engine/app/features/object-search/components/filters/NumericRangeFilter.tsx +0 -163
  43. package/brands/engine/app/features/object-search/components/filters/SearchFilter.tsx +0 -50
  44. package/brands/engine/app/features/object-search/components/filters/SelectFilter.tsx +0 -97
  45. package/brands/engine/app/features/object-search/components/filters/TextFilter.tsx +0 -91
  46. package/brands/engine/app/features/object-search/hooks/useAsyncData.ts +0 -54
  47. package/brands/engine/app/features/object-search/hooks/useCachedAsyncData.ts +0 -184
  48. package/brands/engine/app/features/object-search/hooks/useDebouncedCallback.ts +0 -34
  49. package/brands/engine/app/features/object-search/hooks/useObjectSearchParams.ts +0 -252
  50. package/brands/engine/app/features/object-search/utils/debounce.ts +0 -25
  51. package/brands/engine/app/features/object-search/utils/fieldUtils.ts +0 -29
  52. package/brands/engine/app/features/object-search/utils/filterUtils.ts +0 -404
  53. package/brands/engine/app/features/object-search/utils/sortUtils.ts +0 -38
  54. package/brands/engine/app/hooks/useEngineLiveData.ts +0 -49
  55. package/brands/engine/app/hooks/useEvaAgent.ts +0 -288
  56. package/brands/engine/app/hooks/usePartnerDashboardData.ts +0 -141
  57. package/brands/engine/app/navigationMenu.tsx +0 -80
  58. package/brands/engine/app/pages/AccountObjectDetailPage.tsx +0 -361
  59. package/brands/engine/app/pages/AccountSearch.tsx +0 -305
  60. package/brands/engine/app/pages/BlankDashboard.tsx +0 -15
  61. package/brands/engine/app/pages/DataTest.tsx +0 -78
  62. package/brands/engine/app/pages/Home.tsx +0 -5
  63. package/brands/engine/app/pages/NotFound.tsx +0 -19
  64. package/brands/engine/app/pages/PartnerHubDashboard.tsx +0 -2077
  65. package/brands/engine/app/pages/Search.tsx +0 -13
  66. package/brands/engine/app/router-utils.tsx +0 -35
  67. package/brands/engine/app/routes.tsx +0 -39
  68. package/brands/engine/app/styles/global.css +0 -269
  69. package/brands/engine/brand.css +0 -40
  70. package/brands/engine/engine-command-center-prd.md +0 -575
  71. package/brands/engine/engine-live-data.js +0 -135
  72. package/brands/engine/engine-sample-data.js +0 -378
  73. package/brands/engine/engine_logo.png +0 -0
  74. package/brands/engine/global.css +0 -269
  75. package/brands/engine/partner-hub-sample-data.js +0 -281
  76. package/brands/engine/schema.graphql +0 -292
  77. package/brands/engine/useEngineLiveData.ts +0 -49
  78. package/brands/engine/useEvaAgent.ts +0 -288
@@ -1,2077 +0,0 @@
1
- import { ListCard, ActivityCard, D3Chart, Dropdown, Button, Modal, CardSkeleton } 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
- } from "@heroicons/react/24/outline";
34
- import * as d3 from "d3";
35
- import engineLogo from "@/assets/images/engine_logo.png";
36
-
37
- /**
38
- * Partner Hub Dashboard
39
- *
40
- * Partner-facing portal where hotel partners (Marriott, Hilton, etc.) log in
41
- * to view their business relationship with Engine:
42
- * - Their properties and performance
43
- * - Their invoices and payments
44
- * - Their attrition penalties and disputes
45
- * - Their contract details
46
- * - Communication with Engine
47
- */
48
- export default function PartnerHubDashboard() {
49
- const { mode, toggle } = useThemeMode();
50
- const [selectedPenalty, setSelectedPenalty] = React.useState(null);
51
- const [isPenaltyModalOpen, setIsPenaltyModalOpen] = React.useState(false);
52
- const [isPropertiesModalOpen, setIsPropertiesModalOpen] = React.useState(false);
53
- const [isRevenueModalOpen, setIsRevenueModalOpen] = React.useState(false);
54
- const [isReservationsModalOpen, setIsReservationsModalOpen] = React.useState(false);
55
- const [isDisputesModalOpen, setIsDisputesModalOpen] = React.useState(false);
56
- const [isInvoicesModalOpen, setIsInvoicesModalOpen] = React.useState(false);
57
-
58
- // Simulated logged-in partner (in real app, this comes from auth context)
59
- const currentPartner = {
60
- name: "Summit Hotels & Resorts",
61
- tier: "Gold",
62
- logo: null, // Could add partner logo here
63
- };
64
-
65
- // Fetch live data from Salesforce
66
- const { data: liveData, loading: liveLoading, error: liveError } = usePartnerDashboardData(
67
- ENABLE_SAMPLE_DATA_CACHE ? null : null // Fetch for current partner
68
- );
69
-
70
- // Determine if we're loading (only in live mode)
71
- const isLoading = !ENABLE_SAMPLE_DATA_CACHE && liveLoading;
72
-
73
- // Show error toast if live data fails
74
- React.useEffect(() => {
75
- if (!ENABLE_SAMPLE_DATA_CACHE && liveError) {
76
- toast.error(`Failed to load data: ${liveError.message}`);
77
- console.error("Live Data Error:", liveError);
78
- }
79
- }, [liveError]);
80
-
81
- // Log live data status
82
- React.useEffect(() => {
83
- if (!ENABLE_SAMPLE_DATA_CACHE) {
84
- console.log("Live Data Mode - Loading:", liveLoading);
85
- console.log("Live Data Mode - Error:", liveError);
86
- console.log("Live Data Mode - Data:", liveData);
87
- }
88
- }, [liveLoading, liveError, liveData]);
89
-
90
- // Transform live data to match sample data format
91
- const transformLiveData = React.useCallback((liveData: any) => {
92
- if (!liveData) return null;
93
-
94
- // Transform penalties to match expected format
95
- const penalties = (liveData.penalties || []).map((p: any) => ({
96
- id: p.Id,
97
- name: p.Name?.value || p.Name,
98
- partner: currentPartner.name,
99
- property: p.Property__r?.Property_Name__c?.value || p.Property__r?.Name?.value || "Unknown Property",
100
- customer: p.Customer_Company__r?.Name?.value || "Unknown Customer",
101
- status: p.Penalty_Status__c?.value || "Unknown",
102
- penalty: p.Final_Penalty_Amount__c?.value || 0,
103
- credit: p.Resale_Credit_Applied__c?.value || 0,
104
- method: "Per Night", // From contract
105
- isHero: p.Resale_Credit_Applied__c?.value === 0 && (p.Rooms_Resold__c?.value || 0) > 0,
106
- originalRoomBlock: p.Original_Room_Block__c?.value || 0,
107
- actualRoomsUsed: p.Actual_Rooms_Used__c?.value || 0,
108
- unusedRooms: p.Unused_Rooms__c?.value || 0,
109
- roomRate: p.Room_Rate__c?.value || 0,
110
- numberOfNights: p.Number_of_Nights__c?.value || 0,
111
- roomsResold: p.Rooms_Resold__c?.value || 0,
112
- penaltyCalculated: p.Penalty_Amount_Calculated__c?.value || 0,
113
- resalePolicy: "Partial Credit (50%)", // From contract
114
- }));
115
-
116
- // Transform disputes to match expected format
117
- const disputes = (liveData.disputes || []).map((d: any) => ({
118
- id: d.Id,
119
- title: d.Subject?.value || "Dispute",
120
- description: `${currentPartner.name} · ${d.Dispute_Type__c?.value || "General"}`,
121
- status: d.Status === "Open" || d.Status === "Escalated" ? "critical" : d.Priority === "High" ? "critical" : d.Priority === "Medium" ? "warning" : "info",
122
- badge: d.Status?.value || d.Status || "Open",
123
- amount: d.Disputed_Amount__c?.value || 0,
124
- agentHandled: d.Agent_Handled__c?.value || false,
125
- }));
126
-
127
- // Transform invoices to match expected format
128
- const invoices = (liveData.invoices || []).filter((i: any) => {
129
- const status = i.Invoice_Status__c?.value || i.Invoice_Status__c;
130
- return status !== "Paid";
131
- }).map((i: any) => ({
132
- id: i.Id,
133
- title: `${i.Name?.value || i.Name} — ${currentPartner.name}`,
134
- description: `${i.Invoice_Period_Start__c?.value || ""} to ${i.Invoice_Period_End__c?.value || ""}`,
135
- status: (i.Invoice_Status__c?.value || i.Invoice_Status__c) === "Overdue" ? "critical" : "default",
136
- badge: i.Invoice_Status__c?.value || i.Invoice_Status__c || "Draft",
137
- amount: i.Invoice_Total__c?.value || 0,
138
- due: i.Due_Date__c?.value || "",
139
- }));
140
-
141
- // Transform activity
142
- const activity = [
143
- ...disputes.slice(0, 2).map((d: any) => ({
144
- id: `dispute-${d.id}`,
145
- title: "Dispute Created",
146
- description: d.title,
147
- status: "alert",
148
- timestamp: "Recently",
149
- partner: currentPartner.name,
150
- })),
151
- ...invoices.slice(0, 2).map((i: any) => ({
152
- id: `invoice-${i.id}`,
153
- title: "Invoice Sent",
154
- description: `${i.title} ($${i.amount.toLocaleString()})`,
155
- status: i.status === "critical" ? "warning" : "info",
156
- timestamp: "Recently",
157
- partner: currentPartner.name,
158
- })),
159
- ];
160
-
161
- return {
162
- penalties,
163
- disputes,
164
- invoices,
165
- activity,
166
- };
167
- }, [currentPartner.name]);
168
-
169
- // Load data - use live data if available, otherwise sample
170
- const transformedLiveData = React.useMemo(() => {
171
- if (!ENABLE_SAMPLE_DATA_CACHE && liveData) {
172
- return transformLiveData(liveData);
173
- }
174
- return null;
175
- }, [liveData, transformLiveData]);
176
-
177
- const allPenalties = useDataSource({
178
- sample: PENALTY_TABLE_ITEMS,
179
- live: transformedLiveData?.penalties || []
180
- });
181
- const allDisputes = useDataSource({
182
- sample: DISPUTE_CARDS,
183
- live: transformedLiveData?.disputes || []
184
- });
185
- const allInvoices = useDataSource({
186
- sample: INVOICE_CARDS,
187
- live: transformedLiveData?.invoices || []
188
- });
189
- const allActivity = useDataSource({
190
- sample: RECENT_ACTIVITY,
191
- live: transformedLiveData?.activity || []
192
- });
193
-
194
- // Calculate revenue trend from invoices
195
- const revenueTrendByProperty = React.useMemo(() => {
196
- if (ENABLE_SAMPLE_DATA_CACHE) {
197
- return REVENUE_TREND_BY_PROPERTY;
198
- }
199
-
200
- if (!liveData?.invoices || liveData.invoices.length === 0 || !liveData?.properties) {
201
- return REVENUE_TREND_BY_PROPERTY;
202
- }
203
-
204
- // Group invoices by month and calculate totals
205
- const monthlyData = new Map();
206
- liveData.invoices.forEach((inv: any) => {
207
- const periodStart = inv.Invoice_Period_Start__c?.value;
208
- if (periodStart) {
209
- const date = new Date(periodStart);
210
- const monthKey = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
211
- const monthLabel = date.toLocaleDateString('en-US', { month: 'short', year: 'numeric' });
212
- const amount = inv.Invoice_Total__c?.value || 0;
213
-
214
- if (!monthlyData.has(monthKey)) {
215
- monthlyData.set(monthKey, { key: monthKey, label: monthLabel, total: 0 });
216
- }
217
- monthlyData.get(monthKey).total += amount;
218
- }
219
- });
220
-
221
- // Sort by date and convert to array
222
- const sortedMonths = Array.from(monthlyData.values())
223
- .sort((a, b) => a.key.localeCompare(b.key));
224
-
225
- const months = sortedMonths.map(m => m.label);
226
- const colors = ["var(--color-dash-accent)", "var(--color-dash-success)", "var(--color-dash-chart-3)", "var(--color-dash-chart-4)"];
227
-
228
- // Distribute monthly revenue across properties (weighted)
229
- const properties = (liveData.properties || []).map((prop: any, idx: number) => {
230
- const weights = [0.35, 0.28, 0.22, 0.15];
231
- const weight = weights[idx] || 0.10;
232
-
233
- return {
234
- name: prop.Property_Name__c?.value || prop.Name?.value || `Property ${idx + 1}`,
235
- color: colors[idx] || "#999",
236
- values: sortedMonths.map(month => Math.floor(month.total * weight))
237
- };
238
- });
239
-
240
- return { months, properties };
241
- }, [liveData?.invoices, liveData?.properties]);
242
-
243
- // Calculate total revenue first (needed by leaderboard)
244
- const myRevenue = React.useMemo(() => {
245
- if (ENABLE_SAMPLE_DATA_CACHE) {
246
- return 238000;
247
- }
248
- return (liveData?.invoices || []).reduce((sum: number, inv: any) => sum + (inv.Invoice_Total__c?.value || 0), 0);
249
- }, [liveData?.invoices]);
250
-
251
- // Calculate property leaderboard from real data
252
- const propertyLeaderboard = React.useMemo(() => {
253
- if (ENABLE_SAMPLE_DATA_CACHE) {
254
- return PROPERTY_LEADERBOARD;
255
- }
256
-
257
- if (!liveData?.properties || liveData.properties.length === 0) {
258
- return [];
259
- }
260
-
261
- // For demo purposes, distribute total revenue across properties
262
- // In a real system, you'd have property-level revenue tracking
263
- const totalRevenue = myRevenue;
264
- const properties = liveData.properties;
265
-
266
- return properties.map((prop: any, idx: number) => {
267
- // Distribute revenue with weighted randomness for demo
268
- const weights = [0.35, 0.28, 0.22, 0.15]; // First property gets most
269
- const weight = weights[idx] || 0.10;
270
- const propRevenue = Math.floor(totalRevenue * weight);
271
- const latestRevenue = Math.floor(propRevenue * 0.20); // 20% in latest period
272
- const growthRates = [60, 55, 75, 100];
273
- const growth = growthRates[idx] || 50;
274
-
275
- return {
276
- name: prop.Property_Name__c?.value || prop.Name?.value || `Property ${idx + 1}`,
277
- city: prop.City__c?.value || "",
278
- state: prop.State__c?.value || "",
279
- revenue: propRevenue,
280
- latestRevenue: latestRevenue,
281
- growth: growth,
282
- insight: idx === 0 ? `Highest booking volume through Engine — ${Math.round(weight * 100)}% of total` :
283
- idx === properties.length - 1 ? `Bookings doubled since October — ${growth}% growth` :
284
- `Strong booking growth in Q1 2026`
285
- };
286
- }).sort((a, b) => b.revenue - a.revenue);
287
- }, [liveData?.properties, myRevenue]);
288
-
289
- // Filter data for current partner (sample data needs filtering, live data is already filtered)
290
- const myPenalties = ENABLE_SAMPLE_DATA_CACHE
291
- ? allPenalties.filter((p: any) => p.partner === currentPartner.name)
292
- : allPenalties;
293
- const myDisputes = ENABLE_SAMPLE_DATA_CACHE
294
- ? allDisputes.filter((d: any) => d.description.includes(currentPartner.name))
295
- : allDisputes;
296
- const myInvoices = ENABLE_SAMPLE_DATA_CACHE
297
- ? allInvoices.filter((i: any) => i.title.includes(currentPartner.name))
298
- : allInvoices;
299
- const myActivity = ENABLE_SAMPLE_DATA_CACHE
300
- ? allActivity.filter((a: any) => a.partner === currentPartner.name)
301
- : allActivity;
302
-
303
- // Calculate partner-specific metrics
304
- const myProperties = ENABLE_SAMPLE_DATA_CACHE
305
- ? 4 // Sample data
306
- : (liveData?.partner?.Total_Properties__c?.value || liveData?.properties?.length || 0);
307
- const myReservations = ENABLE_SAMPLE_DATA_CACHE
308
- ? 10 // Sample data
309
- : (liveData?.partner?.Total_Reservations__c?.value || 0);
310
- const myOpenDisputes = myDisputes.length;
311
- const myPendingInvoices = myInvoices.length;
312
-
313
- // Custom horizontal bar chart renderer (unused for now, but kept for future use)
314
- const renderHorizontalBarChart = (svgEl: any, data: any, { width, height }: any, options: any = {}) => {
315
- const margin = options.margin || { top: 10, right: 30, bottom: 30, left: 150 };
316
- const innerWidth = width - margin.left - margin.right;
317
- const innerHeight = height - margin.top - margin.bottom;
318
-
319
- const svg = d3.select(svgEl);
320
- svg.selectAll("*").remove();
321
-
322
- const g = svg
323
- .append("g")
324
- .attr("transform", `translate(${margin.left},${margin.top})`);
325
-
326
- const x = d3
327
- .scaleLinear()
328
- .domain([0, d3.max(data, (d) => d.value) || 0])
329
- .range([0, innerWidth]);
330
-
331
- const y = d3
332
- .scaleBand()
333
- .domain(data.map((d) => d.label))
334
- .range([0, innerHeight])
335
- .padding(0.2);
336
-
337
- // Bars with gradient
338
- const gradient = svg.append("defs")
339
- .append("linearGradient")
340
- .attr("id", "barGradient")
341
- .attr("x1", "0%")
342
- .attr("x2", "100%");
343
-
344
- gradient.append("stop")
345
- .attr("offset", "0%")
346
- .attr("stop-color", "var(--color-dash-accent)");
347
-
348
- gradient.append("stop")
349
- .attr("offset", "100%")
350
- .attr("stop-color", "var(--color-dash-success)");
351
-
352
- g.selectAll(".bar")
353
- .data(data)
354
- .join("rect")
355
- .attr("class", "bar")
356
- .attr("x", 0)
357
- .attr("y", (d) => y(d.label) || 0)
358
- .attr("width", (d) => x(d.value))
359
- .attr("height", y.bandwidth())
360
- .attr("fill", "url(#barGradient)")
361
- .attr("rx", 4);
362
-
363
- // Y axis
364
- g.append("g")
365
- .call(d3.axisLeft(y))
366
- .selectAll("text")
367
- .style("font-size", "12px")
368
- .style("fill", "currentColor");
369
-
370
- // X axis
371
- g.append("g")
372
- .attr("transform", `translate(0,${innerHeight})`)
373
- .call(d3.axisBottom(x).ticks(5).tickFormat(d3.format("$~s")))
374
- .selectAll("text")
375
- .style("font-size", "11px")
376
- .style("fill", "currentColor");
377
- };
378
-
379
- // Custom donut chart renderer (unused for now, but kept for future use)
380
- const renderDonutChart = (svgEl: any, data: any, { width, height }: any) => {
381
- const svg = d3.select(svgEl);
382
- svg.selectAll("*").remove();
383
-
384
- const radius = Math.min(width, height) / 2 - 20;
385
- const g = svg
386
- .append("g")
387
- .attr("transform", `translate(${width / 2},${height / 2})`);
388
-
389
- const pie = d3.pie().value((d) => d.value);
390
- const arc = d3
391
- .arc()
392
- .innerRadius(radius * 0.6)
393
- .outerRadius(radius);
394
-
395
- const arcs = g
396
- .selectAll(".arc")
397
- .data(pie(data))
398
- .join("g")
399
- .attr("class", "arc");
400
-
401
- arcs
402
- .append("path")
403
- .attr("d", arc)
404
- .attr("fill", (d) => d.data.color)
405
- .attr("stroke", "var(--color-background)")
406
- .attr("stroke-width", 2);
407
-
408
- // Labels
409
- arcs
410
- .append("text")
411
- .attr("transform", (d) => `translate(${arc.centroid(d)})`)
412
- .attr("text-anchor", "middle")
413
- .attr("font-size", "14px")
414
- .attr("font-weight", "600")
415
- .attr("fill", "var(--color-dash-surface)")
416
- .attr("paint-order", "stroke")
417
- .attr("stroke", "rgba(0,0,0,0.3)")
418
- .attr("stroke-width", "2px")
419
- .text((d) => d.data.label);
420
-
421
- arcs
422
- .append("text")
423
- .attr("transform", (d) => {
424
- const [x, y] = arc.centroid(d);
425
- return `translate(${x},${y + 16})`;
426
- })
427
- .attr("text-anchor", "middle")
428
- .attr("font-size", "12px")
429
- .attr("fill", "var(--color-dash-surface)")
430
- .attr("paint-order", "stroke")
431
- .attr("stroke", "rgba(0,0,0,0.3)")
432
- .attr("stroke-width", "2px")
433
- .text((d) => d.data.value);
434
- };
435
-
436
- // Custom area chart renderer for invoice trend (unused for now, but kept for future use)
437
- const renderAreaChart = (svgEl: any, data: any, { width, height }: any) => {
438
- const margin = { top: 20, right: 20, bottom: 30, left: 60 };
439
- const innerWidth = width - margin.left - margin.right;
440
- const innerHeight = height - margin.top - margin.bottom;
441
-
442
- const svg = d3.select(svgEl);
443
- svg.selectAll("*").remove();
444
-
445
- const g = svg
446
- .append("g")
447
- .attr("transform", `translate(${margin.left},${margin.top})`);
448
-
449
- const x = d3
450
- .scaleBand()
451
- .domain(data.map((d) => d.month))
452
- .range([0, innerWidth])
453
- .padding(0.1);
454
-
455
- const y = d3
456
- .scaleLinear()
457
- .domain([0, d3.max(data, (d) => Math.max(d.total, d.commission)) || 0])
458
- .range([innerHeight, 0])
459
- .nice();
460
-
461
- // Area generator
462
- const areaTotal = d3
463
- .area()
464
- .x((d, i) => (x(d.month) || 0) + x.bandwidth() / 2)
465
- .y0(innerHeight)
466
- .y1((d) => y(d.total));
467
-
468
- const areaCommission = d3
469
- .area()
470
- .x((d, i) => (x(d.month) || 0) + x.bandwidth() / 2)
471
- .y0(innerHeight)
472
- .y1((d) => y(d.commission));
473
-
474
- // Total area
475
- g.append("path")
476
- .datum(data)
477
- .attr("fill", "var(--color-dash-accent)")
478
- .attr("fill-opacity", 0.3)
479
- .attr("d", areaTotal);
480
-
481
- // Commission area
482
- g.append("path")
483
- .datum(data)
484
- .attr("fill", "var(--color-dash-success)")
485
- .attr("fill-opacity", 0.5)
486
- .attr("d", areaCommission);
487
-
488
- // Total line
489
- g.append("path")
490
- .datum(data)
491
- .attr("fill", "none")
492
- .attr("stroke", "var(--color-dash-accent)")
493
- .attr("stroke-width", 2)
494
- .attr("d", d3.line()
495
- .x((d) => (x(d.month) || 0) + x.bandwidth() / 2)
496
- .y((d) => y(d.total))
497
- );
498
-
499
- // Commission line
500
- g.append("path")
501
- .datum(data)
502
- .attr("fill", "none")
503
- .attr("stroke", "var(--color-dash-success)")
504
- .attr("stroke-width", 2)
505
- .attr("d", d3.line()
506
- .x((d) => (x(d.month) || 0) + x.bandwidth() / 2)
507
- .y((d) => y(d.commission))
508
- );
509
-
510
- // X axis
511
- g.append("g")
512
- .attr("transform", `translate(0,${innerHeight})`)
513
- .call(d3.axisBottom(x))
514
- .selectAll("text")
515
- .style("font-size", "11px")
516
- .style("fill", "currentColor");
517
-
518
- // Y axis
519
- g.append("g")
520
- .call(d3.axisLeft(y).ticks(5).tickFormat(d3.format("$~s")))
521
- .selectAll("text")
522
- .style("font-size", "11px")
523
- .style("fill", "currentColor");
524
-
525
- // Legend
526
- const legend = svg
527
- .append("g")
528
- .attr("transform", `translate(${width - 180}, 10)`);
529
-
530
- legend
531
- .append("rect")
532
- .attr("width", 12)
533
- .attr("height", 12)
534
- .attr("fill", "var(--color-dash-accent)");
535
-
536
- legend
537
- .append("text")
538
- .attr("x", 18)
539
- .attr("y", 10)
540
- .attr("font-size", "12px")
541
- .attr("fill", "currentColor")
542
- .text("Total Invoiced");
543
-
544
- legend
545
- .append("rect")
546
- .attr("y", 20)
547
- .attr("width", 12)
548
- .attr("height", 12)
549
- .attr("fill", "var(--color-dash-success)");
550
-
551
- legend
552
- .append("text")
553
- .attr("x", 18)
554
- .attr("y", 30)
555
- .attr("font-size", "12px")
556
- .attr("fill", "currentColor")
557
- .text("Commission");
558
- };
559
-
560
- // Multi-line revenue trend by property renderer
561
- const renderRevenueTrendByProperty = React.useCallback((svgEl: any, data: any, { width, height }: any) => {
562
- if (!data || !data.months || !data.properties) {
563
- return;
564
- }
565
-
566
- const { months, properties } = data;
567
- const margin = { top: 20, right: 20, bottom: 40, left: 70 };
568
- const innerWidth = width - margin.left - margin.right;
569
- const innerHeight = height - margin.top - margin.bottom;
570
-
571
- if (innerWidth <= 0 || innerHeight <= 0) {
572
- return;
573
- }
574
-
575
- const svg = d3.select(svgEl);
576
- svg.selectAll("*").remove();
577
-
578
- const g = svg
579
- .append("g")
580
- .attr("transform", `translate(${margin.left},${margin.top})`);
581
-
582
- const x = d3
583
- .scalePoint()
584
- .domain(months)
585
- .range([0, innerWidth])
586
- .padding(0.1);
587
-
588
- const allValues = properties.flatMap((p) => p.values);
589
- const y = d3
590
- .scaleLinear()
591
- .domain([0, (d3.max(allValues) || 0) * 1.1])
592
- .range([innerHeight, 0])
593
- .nice();
594
-
595
- // Subtle grid lines
596
- g.append("g")
597
- .attr("class", "grid")
598
- .call(
599
- d3.axisLeft(y)
600
- .ticks(5)
601
- .tickSize(-innerWidth)
602
- .tickFormat(() => "")
603
- )
604
- .selectAll("line")
605
- .style("stroke", "currentColor")
606
- .style("stroke-opacity", "0.08");
607
- g.select(".grid .domain").remove();
608
-
609
- // Area + line per property
610
- properties.forEach((prop) => {
611
- const areaGen = d3
612
- .area<number>()
613
- .x((_d, i) => x(months[i]) ?? 0)
614
- .y0(innerHeight)
615
- .y1((d) => y(d))
616
- .curve(d3.curveMonotoneX);
617
-
618
- const lineGen = d3
619
- .line<number>()
620
- .x((_d, i) => x(months[i]) ?? 0)
621
- .y((d) => y(d))
622
- .curve(d3.curveMonotoneX);
623
-
624
- g.append("path")
625
- .datum(prop.values)
626
- .attr("fill", prop.color)
627
- .attr("fill-opacity", 0.08)
628
- .attr("d", areaGen);
629
-
630
- g.append("path")
631
- .datum(prop.values)
632
- .attr("fill", "none")
633
- .attr("stroke", prop.color)
634
- .attr("stroke-width", 2.5)
635
- .attr("d", lineGen);
636
-
637
- // Dots at last point
638
- const lastIdx = prop.values.length - 1;
639
- g.append("circle")
640
- .attr("cx", x(months[lastIdx]) ?? 0)
641
- .attr("cy", y(prop.values[lastIdx]))
642
- .attr("r", 4)
643
- .attr("fill", prop.color)
644
- .attr("stroke", "var(--color-background, #fff)")
645
- .attr("stroke-width", 2);
646
- });
647
-
648
- // X axis
649
- g.append("g")
650
- .attr("transform", `translate(0,${innerHeight})`)
651
- .call(d3.axisBottom(x))
652
- .selectAll("text")
653
- .style("font-size", "11px")
654
- .style("fill", "currentColor")
655
- .attr("dy", "1.2em");
656
- g.select(".domain").style("stroke-opacity", "0.2");
657
-
658
- // Y axis
659
- g.append("g")
660
- .call(d3.axisLeft(y).ticks(5).tickFormat(d3.format("$~s")))
661
- .selectAll("text")
662
- .style("font-size", "11px")
663
- .style("fill", "currentColor");
664
- }, []);
665
-
666
- const monthlyTotalRevenue = React.useMemo(() => {
667
- if (!revenueTrendByProperty?.months || !revenueTrendByProperty?.properties) return null;
668
- return revenueTrendByProperty.months.map((month: string, i: number) => ({
669
- month,
670
- total: revenueTrendByProperty.properties.reduce((sum: number, p: any) => sum + p.values[i], 0),
671
- }));
672
- }, [revenueTrendByProperty]);
673
-
674
- const renderTotalRevenueGrowth = React.useCallback((svgEl: any, data: any, { width, height }: any) => {
675
- if (!data || !data.length) return;
676
- const margin = { top: 20, right: 20, bottom: 40, left: 70 };
677
- const innerWidth = width - margin.left - margin.right;
678
- const innerHeight = height - margin.top - margin.bottom;
679
- if (innerWidth <= 0 || innerHeight <= 0) return;
680
-
681
- const svg = d3.select(svgEl);
682
- svg.selectAll("*").remove();
683
- const g = svg.append("g").attr("transform", `translate(${margin.left},${margin.top})`);
684
-
685
- const x = d3.scaleBand().domain(data.map((d: any) => d.month)).range([0, innerWidth]).padding(0.35);
686
- const maxVal = d3.max(data, (d: any) => d.total) || 0;
687
- const y = d3.scaleLinear().domain([0, maxVal * 1.15]).range([innerHeight, 0]).nice();
688
-
689
- g.append("g").attr("class", "grid")
690
- .call(d3.axisLeft(y).ticks(5).tickSize(-innerWidth).tickFormat(() => ""))
691
- .selectAll("line").style("stroke", "currentColor").style("stroke-opacity", "0.08");
692
- g.select(".grid .domain").remove();
693
-
694
- const accentColor = getComputedStyle(document.documentElement).getPropertyValue('--dash-accent').trim() || '#2563eb';
695
-
696
- g.selectAll(".bar").data(data).join("rect")
697
- .attr("x", (d: any) => x(d.month) ?? 0)
698
- .attr("y", (d: any) => y(d.total))
699
- .attr("width", x.bandwidth())
700
- .attr("height", (d: any) => innerHeight - y(d.total))
701
- .attr("rx", 4)
702
- .attr("fill", accentColor)
703
- .attr("fill-opacity", 0.85);
704
-
705
- g.selectAll(".label").data(data).join("text")
706
- .attr("x", (d: any) => (x(d.month) ?? 0) + x.bandwidth() / 2)
707
- .attr("y", (d: any) => y(d.total) - 8)
708
- .attr("text-anchor", "middle")
709
- .style("font-size", "11px")
710
- .style("font-weight", "600")
711
- .style("fill", "currentColor")
712
- .text((d: any) => `$${(d.total / 1000).toFixed(0)}K`);
713
-
714
- g.append("g").attr("transform", `translate(0,${innerHeight})`)
715
- .call(d3.axisBottom(x)).selectAll("text")
716
- .style("font-size", "11px").style("fill", "currentColor").attr("dy", "1.2em");
717
- g.select(".domain").style("stroke-opacity", "0.2");
718
-
719
- g.append("g").call(d3.axisLeft(y).ticks(5).tickFormat(d3.format("$~s")))
720
- .selectAll("text").style("font-size", "11px").style("fill", "currentColor");
721
- }, []);
722
-
723
- return (
724
- <div className="heroui-scope min-h-screen bg-[var(--color-dash-surface)] dark:bg-[var(--color-dash-text)] transition-colors duration-300">
725
- {/* Header - Refined Engine Brand */}
726
- <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">
727
- <div className="max-w-[1600px] mx-auto px-8 py-5">
728
- <div className="flex items-center justify-between">
729
- <div className="flex items-center gap-8">
730
- <img
731
- src={engineLogo}
732
- alt="Engine"
733
- className="h-14 w-auto dark:invert dark:brightness-0 dark:contrast-100 transition-all duration-300"
734
- />
735
- <div className="flex items-center gap-4">
736
- <div className="h-10 w-px bg-gradient-to-b from-transparent via-[var(--color-dash-label)]/40 to-transparent" />
737
- <div className="space-y-0.5">
738
- <p className="text-sm font-semibold text-[var(--color-dash-text)] dark:text-white transition-colors">
739
- {currentPartner.name}
740
- </p>
741
- <div className="flex items-center gap-2">
742
- <span className="relative flex h-2 w-2">
743
- <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-[var(--color-dash-success)] opacity-75"></span>
744
- <span className="relative inline-flex rounded-full h-2 w-2 bg-[var(--color-dash-success)]"></span>
745
- </span>
746
- <p className="text-xs font-medium text-[var(--color-dash-label)] uppercase tracking-wide">
747
- {currentPartner.tier} Partner
748
- </p>
749
- </div>
750
- </div>
751
- </div>
752
- </div>
753
- <div className="flex items-center gap-3">
754
- <button
755
- onClick={toggle}
756
- className="group p-3 rounded-xl hover:bg-[var(--color-dash-surface)] dark:hover:bg-[var(--color-dash-muted)]/20 transition-colors duration-200"
757
- aria-label="Toggle theme"
758
- >
759
- {mode === "dark" ? (
760
- <SunIcon className="h-5 w-5 text-[var(--color-dash-accent)] transition-transform group-hover:rotate-45 duration-300" />
761
- ) : (
762
- <MoonIcon className="h-5 w-5 text-[var(--color-dash-muted)] transition-transform group-hover:-rotate-12 duration-300" />
763
- )}
764
- </button>
765
-
766
- <Dropdown>
767
- <Button variant="ghost" size="sm" className="p-2">
768
- <UserCircleIcon className="h-6 w-6 text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]" />
769
- </Button>
770
- <Dropdown.Popover className="min-w-[200px]">
771
- <Dropdown.Menu
772
- className="p-2"
773
- onAction={(key) => {
774
- if (key === "settings") {
775
- toast.info("Opening settings...");
776
- } else if (key === "logout") {
777
- toast.success("Logged out successfully");
778
- }
779
- }}
780
- >
781
- <Dropdown.Item
782
- id="settings"
783
- textValue="Settings"
784
- className="px-3 py-2 rounded-lg hover:bg-[var(--color-dash-surface)] dark:hover:bg-[var(--color-dash-muted)]/20 cursor-pointer"
785
- >
786
- <div className="flex items-center gap-3">
787
- <Cog6ToothIcon className="h-5 w-5 text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]" />
788
- <span className="text-sm font-medium text-[var(--color-dash-text)] dark:text-white">Settings</span>
789
- </div>
790
- </Dropdown.Item>
791
- <Dropdown.Item
792
- id="logout"
793
- textValue="Logout"
794
- variant="danger"
795
- className="px-3 py-2 rounded-lg hover:bg-[var(--color-dash-danger)]/10 cursor-pointer"
796
- >
797
- <div className="flex items-center gap-3">
798
- <ArrowRightOnRectangleIcon className="h-5 w-5 text-[var(--color-dash-danger)]" />
799
- <span className="text-sm font-medium text-[var(--color-dash-danger)]">Logout</span>
800
- </div>
801
- </Dropdown.Item>
802
- </Dropdown.Menu>
803
- </Dropdown.Popover>
804
- </Dropdown>
805
- </div>
806
- </div>
807
- </div>
808
- </header>
809
-
810
- {/* Hero Section - Partnership Overview */}
811
- <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">
812
- {/* Subtle background pattern */}
813
- <div className="absolute inset-0 opacity-5">
814
- <div className="absolute inset-0" style={{
815
- backgroundImage: 'radial-gradient(circle at 1px 1px, white 1px, transparent 0)',
816
- backgroundSize: '40px 40px'
817
- }} />
818
- </div>
819
-
820
- {/* Gradient orbs for depth */}
821
- <div className="absolute top-0 right-0 w-96 h-96 bg-[var(--color-dash-accent)]/10 rounded-full blur-3xl" />
822
- <div className="absolute bottom-0 left-0 w-96 h-96 bg-[var(--color-dash-success)]/10 rounded-full blur-3xl" />
823
-
824
- <div className="relative max-w-[1600px] mx-auto px-8 py-12 pb-20">
825
- <div className="max-w-3xl space-y-3 animate-fade-in">
826
- <h1 className="text-3xl lg:text-4xl font-bold text-white tracking-tight leading-tight">
827
- Hey there Jamie! Here's what's happening with your properties
828
- </h1>
829
- <p className="text-lg text-white/70 leading-relaxed">
830
- Everything you need to manage your partnership with Engine — invoices, bookings, and any items that need your attention.
831
- </p>
832
- </div>
833
- </div>
834
- </div>
835
-
836
- {/* Main Content */}
837
- <div className="max-w-[1600px] mx-auto px-8 -mt-12 space-y-10">
838
- {/* Quick Stats - Uniform metrics grid */}
839
- <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-5 animate-slide-up relative z-10">
840
- {/* Revenue */}
841
- <div
842
- onClick={() => !isLoading && setIsRevenueModalOpen(true)}
843
- className={isLoading ? "" : "cursor-pointer"}
844
- >
845
- <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">
846
- {isLoading ? (
847
- <div className="space-y-3">
848
- <div className="flex items-center justify-between">
849
- <div className="bg-[var(--color-dash-label)]/20 rounded-lg h-9 w-9 animate-pulse"></div>
850
- <div className="bg-[var(--color-dash-label)]/20 rounded-full h-5 w-16 animate-pulse"></div>
851
- </div>
852
- <div className="h-3 w-24 bg-[var(--color-dash-label)]/20 rounded animate-pulse"></div>
853
- <div className="h-10 w-32 bg-[var(--color-dash-label)]/20 rounded animate-pulse"></div>
854
- <div className="h-3 w-28 bg-[var(--color-dash-label)]/20 rounded animate-pulse"></div>
855
- </div>
856
- ) : (
857
- <>
858
- <div className="flex items-center justify-between mb-3">
859
- <div className="bg-[var(--color-dash-success)]/10 rounded-lg p-2">
860
- <BanknotesIcon className="h-5 w-5 text-[var(--color-dash-success)]" />
861
- </div>
862
- <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)]">
863
- +45%
864
- </span>
865
- </div>
866
- <p className="text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] text-sm font-semibold mb-2 uppercase tracking-wider">Total Revenue</p>
867
- <p className="font-black text-[var(--color-dash-text)] dark:text-white mb-1 leading-tight" style={{ fontSize: 'var(--dash-metric-size)' }}>${(myRevenue / 1000).toFixed(0)}K</p>
868
- <p className="text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]">earned with Engine</p>
869
- </>
870
- )}
871
- </div>
872
- </div>
873
-
874
- {/* Items to Review */}
875
- <div
876
- onClick={() => !isLoading && setIsDisputesModalOpen(true)}
877
- className={isLoading ? "" : "cursor-pointer"}
878
- >
879
- <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">
880
- {isLoading ? (
881
- <CardSkeleton lines={4} />
882
- ) : (
883
- <>
884
- <div className="flex items-center justify-between mb-3">
885
- <div className="bg-[var(--color-dash-warning)]/10 rounded-lg p-2">
886
- <ExclamationTriangleIcon className="h-5 w-5 text-[var(--color-dash-warning)]" />
887
- </div>
888
- <span className="inline-flex items-center rounded-full px-2 py-0.5 text-xs font-bold bg-[var(--color-dash-warning)] text-white">
889
- REVIEW
890
- </span>
891
- </div>
892
- <p className="text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] text-sm font-semibold mb-2 uppercase tracking-wider">Things to Review</p>
893
- <p className="font-black text-[var(--color-dash-text)] dark:text-white mb-1 leading-tight" style={{ fontSize: 'var(--dash-metric-size)' }}>{myOpenDisputes}</p>
894
- <p className="text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]">items need attention</p>
895
- </>
896
- )}
897
- </div>
898
- </div>
899
-
900
- {/* Properties */}
901
- <div
902
- onClick={() => !isLoading && setIsPropertiesModalOpen(true)}
903
- className={isLoading ? "" : "cursor-pointer"}
904
- >
905
- <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">
906
- {isLoading ? (
907
- <CardSkeleton lines={4} />
908
- ) : (
909
- <>
910
- <div className="flex items-center justify-between mb-3">
911
- <div className="bg-[var(--color-dash-accent)]/10 rounded-lg p-2">
912
- <BuildingOfficeIcon className="h-5 w-5 text-[var(--color-dash-accent)]" />
913
- </div>
914
- </div>
915
- <p className="text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] text-sm font-semibold mb-2 uppercase tracking-wider">Properties</p>
916
- <p className="font-black text-[var(--color-dash-text)] dark:text-white mb-1 leading-tight" style={{ fontSize: 'var(--dash-metric-size)' }}>{myProperties}</p>
917
- <p className="text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]">active locations</p>
918
- </>
919
- )}
920
- </div>
921
- </div>
922
-
923
- {/* Reservations */}
924
- <div
925
- onClick={() => !isLoading && setIsReservationsModalOpen(true)}
926
- className={isLoading ? "" : "cursor-pointer"}
927
- >
928
- <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">
929
- {isLoading ? (
930
- <CardSkeleton lines={4} />
931
- ) : (
932
- <>
933
- <div className="flex items-center justify-between mb-3">
934
- <div className="bg-[var(--color-dash-info)]/10 rounded-lg p-2">
935
- <ClockIcon className="h-5 w-5 text-[var(--color-dash-info)]" />
936
- </div>
937
- </div>
938
- <p className="text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] text-sm font-semibold mb-2 uppercase tracking-wider">Reservations</p>
939
- <p className="font-black text-[var(--color-dash-text)] dark:text-white mb-1 leading-tight" style={{ fontSize: 'var(--dash-metric-size)' }}>{myReservations}</p>
940
- <p className="text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]">through Engine</p>
941
- </>
942
- )}
943
- </div>
944
- </div>
945
-
946
- {/* Invoices */}
947
- <div
948
- onClick={() => !isLoading && setIsInvoicesModalOpen(true)}
949
- className={isLoading ? "" : "cursor-pointer"}
950
- >
951
- <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">
952
- {isLoading ? (
953
- <CardSkeleton lines={4} />
954
- ) : (
955
- <>
956
- <div className="flex items-center justify-between mb-3">
957
- <div className="bg-[var(--color-dash-danger)]/10 rounded-lg p-2">
958
- <ShieldCheckIcon className="h-5 w-5 text-[var(--color-dash-danger)]" />
959
- </div>
960
- </div>
961
- <p className="text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] text-sm font-semibold mb-2 uppercase tracking-wider">Invoices</p>
962
- <p className="font-black text-[var(--color-dash-text)] dark:text-white mb-1 leading-tight" style={{ fontSize: 'var(--dash-metric-size)' }}>{myPendingInvoices}</p>
963
- <p className="text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]">ready to pay</p>
964
- </>
965
- )}
966
- </div>
967
- </div>
968
- </div>
969
-
970
- {/* Property Leaderboard - NEW SECTION */}
971
- {isLoading ? (
972
- <div className="bg-gradient-to-br from-white via-[var(--color-dash-surface)] to-white dark:from-[var(--color-dash-text)] dark:via-[var(--color-dash-dark)] dark:to-[var(--color-dash-text)] rounded-2xl p-8 shadow-xl border border-[var(--color-dash-label)]/20 dark:border-[var(--color-dash-muted)]/30">
973
- <div className="mb-6">
974
- <div className="h-9 w-96 bg-[var(--color-dash-label)]/20 rounded animate-pulse mb-2"></div>
975
- <div className="h-6 w-72 bg-[var(--color-dash-label)]/20 rounded animate-pulse"></div>
976
- </div>
977
- <div className="space-y-4">
978
- <CardSkeleton lines={4} />
979
- <CardSkeleton lines={4} />
980
- <CardSkeleton lines={4} />
981
- <CardSkeleton lines={4} />
982
- </div>
983
- </div>
984
- ) : (
985
- <div className="bg-gradient-to-br from-white via-[var(--color-dash-surface)] to-white dark:from-[var(--color-dash-text)] dark:via-[var(--color-dash-dark)] dark:to-[var(--color-dash-text)] rounded-2xl p-8 shadow-xl border border-[var(--color-dash-label)]/20 dark:border-[var(--color-dash-muted)]/30">
986
- <div className="mb-6">
987
- <h2 className="text-3xl font-black text-[var(--color-dash-text)] dark:text-white tracking-tight mb-2">
988
- Property Performance Leaderboard
989
- </h2>
990
- <p className="text-lg text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]">
991
- Your 4 properties ranked by total revenue (last 6 months)
992
- </p>
993
- </div>
994
-
995
- <div className="space-y-4">
996
- {propertyLeaderboard.map((property, idx) => {
997
- // Determine icon and color based on rank
998
- let RankIcon = StarIcon;
999
- let iconColor = "text-[var(--color-dash-label)]";
1000
- let bgColor = "bg-[var(--color-dash-label)]/10";
1001
-
1002
- if (idx === 0) {
1003
- RankIcon = TrophyIcon;
1004
- iconColor = "text-[var(--color-dash-warning)]";
1005
- bgColor = "bg-[var(--color-dash-warning)]/10";
1006
- } else if (idx === 1) {
1007
- RankIcon = StarIcon;
1008
- iconColor = "text-[var(--color-dash-label)]";
1009
- bgColor = "bg-[var(--color-dash-label)]/10";
1010
- } else if (idx === 2) {
1011
- RankIcon = StarIcon;
1012
- iconColor = "text-[var(--color-dash-warning)]";
1013
- bgColor = "bg-[var(--color-dash-warning)]/10";
1014
- } else {
1015
- RankIcon = RocketLaunchIcon;
1016
- iconColor = "text-[var(--color-dash-accent)]";
1017
- bgColor = "bg-[var(--color-dash-accent)]/10";
1018
- }
1019
-
1020
- return (
1021
- <div
1022
- key={idx}
1023
- className="group bg-white dark:bg-[var(--color-dash-text)] rounded-xl p-6 border-2 border-[var(--color-dash-label)]/20 dark:border-[var(--color-dash-muted)]/30 hover:border-[var(--color-dash-success)] hover:shadow-lg transition-all duration-300"
1024
- >
1025
- <div className="flex items-center justify-between gap-6">
1026
- <div className="flex items-center gap-4 flex-1">
1027
- {/* Rank Icon */}
1028
- <div className={`flex-shrink-0 ${bgColor} rounded-xl p-3`}>
1029
- <RankIcon className={`h-8 w-8 ${iconColor}`} />
1030
- </div>
1031
-
1032
- {/* Property info */}
1033
- <div className="flex-1">
1034
- <div className="flex items-center gap-3 mb-2">
1035
- <h3 className="text-xl font-bold text-[var(--color-dash-text)] dark:text-white">
1036
- {property.name}
1037
- </h3>
1038
- <span className="inline-flex items-center rounded-full px-3 py-1 text-xs font-bold bg-[var(--color-dash-success)]/20 text-[var(--color-dash-success)] border border-[var(--color-dash-success)]/30">
1039
- +{property.growth}% growth
1040
- </span>
1041
- </div>
1042
- <p className="text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] mb-1">
1043
- {property.insight}
1044
- </p>
1045
- <div className="flex items-center gap-6 text-sm">
1046
- <span className="text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]">
1047
- Total: <strong className="text-[var(--color-dash-text)] dark:text-white">${property.revenue.toLocaleString()}</strong>
1048
- </span>
1049
- <span className="text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]">
1050
- Latest: <strong className="text-[var(--color-dash-text)] dark:text-white">${property.latestRevenue.toLocaleString()}</strong>
1051
- </span>
1052
- </div>
1053
- </div>
1054
-
1055
- {/* Growth indicator */}
1056
- <div className="text-right flex-shrink-0">
1057
- <p className="font-black text-[var(--color-dash-success)] mb-1 leading-tight" style={{ fontSize: 'var(--dash-metric-size)' }}>
1058
- {property.growth}%
1059
- </p>
1060
- <p className="text-xs text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] uppercase tracking-wider">
1061
- Growth
1062
- </p>
1063
- </div>
1064
- </div>
1065
- </div>
1066
- </div>
1067
- );
1068
- })}
1069
- </div>
1070
-
1071
- {/* Summary insight */}
1072
- <div className="mt-6 bg-[var(--color-dash-info)]/10 border-l-4 border-[var(--color-dash-info)] rounded-r-lg p-5">
1073
- <div className="flex items-start gap-3">
1074
- <div className="flex-shrink-0">
1075
- <LightBulbIcon className="h-5 w-5 text-[var(--color-dash-info)]" />
1076
- </div>
1077
- <p className="text-base text-[var(--color-dash-text)] dark:text-white font-medium">
1078
- <strong>Key Insight:</strong> Austin is driving 41% of bookings through Engine and growing 60%.
1079
- SF Bay doubled in 6 months (100% growth) — strong performance across your portfolio.
1080
- </p>
1081
- </div>
1082
- </div>
1083
- </div>
1084
- )}
1085
-
1086
- {/* Penalty Calculation Issue Card */}
1087
- {!isLoading && myPenalties.filter((p: any) => p.isHero).length > 0 && (
1088
- <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">
1089
- <div className="flex items-start gap-4 mb-6">
1090
- <div className="flex-shrink-0">
1091
- <div className="bg-[var(--color-dash-warning)]/10 rounded-xl p-3">
1092
- <ExclamationTriangleIcon className="h-8 w-8 text-[var(--color-dash-warning)]" />
1093
- </div>
1094
- </div>
1095
- <div className="flex-1">
1096
- <div className="flex items-start justify-between gap-4 mb-2">
1097
- <div>
1098
- <h3 className="text-2xl font-bold text-[var(--color-dash-text)] dark:text-white mb-1">
1099
- Penalty Calculation Issue
1100
- </h3>
1101
- <p className="text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]">
1102
- ATR-00001 · Summit Austin Convention Center · TechCorp Inc. booking
1103
- </p>
1104
- </div>
1105
- <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">
1106
- NEEDS REVIEW
1107
- </span>
1108
- </div>
1109
- </div>
1110
- </div>
1111
-
1112
- <div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
1113
- <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">
1114
- <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>
1115
- <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>
1116
- <p className="text-xs text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]">You owe Engine</p>
1117
- </div>
1118
- <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">
1119
- <p className="text-xs font-semibold text-[var(--color-dash-success)] mb-2 uppercase tracking-wider">Resale Credit Missing</p>
1120
- <p className="font-black text-[var(--color-dash-success)] mb-1 leading-tight" style={{ fontSize: 'var(--dash-metric-sub)' }}>-$2,400</p>
1121
- <p className="text-xs text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]">Should reduce penalty</p>
1122
- </div>
1123
- <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">
1124
- <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>
1125
- <p className="font-black text-[var(--color-dash-accent)] mb-1 leading-tight" style={{ fontSize: 'var(--dash-metric-sub)' }}>$6,600</p>
1126
- <p className="text-xs text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]">What you should owe</p>
1127
- </div>
1128
- </div>
1129
-
1130
- <div className="bg-[var(--color-dash-warning)]/10 border-l-4 border-[var(--color-dash-warning)] rounded-r-lg p-4 mb-6">
1131
- <p className="text-sm text-[var(--color-dash-text)] dark:text-white leading-relaxed">
1132
- 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>,
1133
- 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>.
1134
- </p>
1135
- </div>
1136
-
1137
- <button
1138
- onClick={() => {
1139
- setSelectedPenalty(myPenalties.find((p: any) => p.isHero));
1140
- setIsPenaltyModalOpen(true);
1141
- }}
1142
- 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"
1143
- >
1144
- <ExclamationTriangleIcon className="h-5 w-5" />
1145
- Review This Calculation
1146
- <svg className="h-4 w-4 transition-transform group-hover:translate-x-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
1147
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
1148
- </svg>
1149
- </button>
1150
- </div>
1151
- )}
1152
-
1153
- {/* Section — Billing & Contract Details */}
1154
- <div className="pt-8 space-y-2">
1155
- <h2 className="text-3xl font-bold text-[var(--color-dash-text)] dark:text-white tracking-tight">
1156
- Billing & contract details
1157
- </h2>
1158
- <p className="text-lg text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]">
1159
- Keep track of invoices and your partnership terms
1160
- </p>
1161
- </div>
1162
-
1163
- {/* Two Charts Grid */}
1164
- {isLoading ? (
1165
- <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
1166
- <CardSkeleton lines={6} />
1167
- <CardSkeleton lines={6} />
1168
- </div>
1169
- ) : (
1170
- <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
1171
- {/* Revenue Trend by Property (multi-line) */}
1172
- <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">
1173
- <div className="p-6 border-b border-[var(--color-dash-label)]/20 dark:border-[var(--color-dash-muted)]/30">
1174
- <h3 className="text-xl font-bold text-[var(--color-dash-text)] dark:text-white mb-1">
1175
- Revenue by property
1176
- </h3>
1177
- <p className="text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]">
1178
- Monthly trend across {revenueTrendByProperty.properties?.length || 4} properties
1179
- </p>
1180
- <div className="flex flex-wrap gap-3 mt-3">
1181
- {revenueTrendByProperty.properties.map((prop) => (
1182
- <div key={prop.name} className="flex items-center gap-1.5">
1183
- <span className="inline-block h-2 w-2 rounded-full flex-shrink-0" style={{ backgroundColor: prop.color }} />
1184
- <span className="text-xs font-medium text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]">{prop.name.replace('Summit ', '')}</span>
1185
- </div>
1186
- ))}
1187
- </div>
1188
- </div>
1189
- <div className="p-4 w-full">
1190
- {revenueTrendByProperty?.months && revenueTrendByProperty?.properties ? (
1191
- <div className="w-full" style={{ minHeight: '240px' }}>
1192
- <D3Chart data={revenueTrendByProperty} renderChart={renderRevenueTrendByProperty} height={240} responsive={true} ariaLabel="Revenue trend by property" />
1193
- </div>
1194
- ) : (
1195
- <div className="h-[240px] flex items-center justify-center text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]">Loading chart data...</div>
1196
- )}
1197
- </div>
1198
- </div>
1199
-
1200
- {/* Total Monthly Revenue Growth (bar chart) */}
1201
- <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">
1202
- <div className="p-6 border-b border-[var(--color-dash-label)]/20 dark:border-[var(--color-dash-muted)]/30">
1203
- <h3 className="text-xl font-bold text-[var(--color-dash-text)] dark:text-white mb-1">
1204
- Total revenue growth
1205
- </h3>
1206
- <p className="text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]">
1207
- Combined monthly revenue — up 67% over 6 months
1208
- </p>
1209
- </div>
1210
- <div className="p-4 w-full">
1211
- {monthlyTotalRevenue ? (
1212
- <div className="w-full" style={{ minHeight: '240px' }}>
1213
- <D3Chart data={monthlyTotalRevenue} renderChart={renderTotalRevenueGrowth} height={240} responsive={true} ariaLabel="Total revenue growth" />
1214
- </div>
1215
- ) : (
1216
- <div className="h-[240px] flex items-center justify-center text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]">Loading chart data...</div>
1217
- )}
1218
- </div>
1219
- </div>
1220
- </div>
1221
- )}
1222
-
1223
- {/* Invoices & Contract */}
1224
- {isLoading ? (
1225
- <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
1226
- <CardSkeleton lines={5} />
1227
- <CardSkeleton lines={5} />
1228
- </div>
1229
- ) : (
1230
- <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
1231
- <ListCard
1232
- title="Your invoices"
1233
- subtitle="Recent statements"
1234
- items={myInvoices.map((inv) => ({
1235
- id: inv.id,
1236
- title: inv.title,
1237
- description: inv.description,
1238
- status: inv.status,
1239
- value: `$${inv.amount.toLocaleString()}`,
1240
- timestamp: `Due ${inv.due}`,
1241
- }))}
1242
- maxBodyHeight={300}
1243
- showStatus={true}
1244
- showTimestamp={true}
1245
- dense={false}
1246
- divided={true}
1247
- onItemClick={(item) => {
1248
- toast.info(`Opening invoice ${item.title.split(' — ')[0]}`);
1249
- }}
1250
- emptyMessage="No invoices right now."
1251
- loading={isLoading}
1252
- />
1253
-
1254
- <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">
1255
- <div className="p-6 border-b border-[var(--color-dash-label)]/20 dark:border-[var(--color-dash-muted)]/30">
1256
- <h3 className="text-xl font-bold text-[var(--color-dash-text)] dark:text-white mb-1">
1257
- Your Contract
1258
- </h3>
1259
- <p className="text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]">
1260
- Partnership terms
1261
- </p>
1262
- </div>
1263
- <div className="p-6 space-y-4">
1264
- <div className="flex items-center justify-between pb-4 border-b border-[var(--color-dash-label)]/20 dark:border-[var(--color-dash-muted)]/30">
1265
- <div>
1266
- <p className="text-xs text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] mb-1 uppercase tracking-wider">Contract ID</p>
1267
- <p className="text-lg font-bold text-[var(--color-dash-text)] dark:text-white">CNTR-00002</p>
1268
- </div>
1269
- <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">
1270
- Active
1271
- </span>
1272
- </div>
1273
-
1274
- <div className="grid grid-cols-2 gap-4">
1275
- <div>
1276
- <p className="text-xs text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] mb-1 uppercase tracking-wider">Commission Rate</p>
1277
- <p className="text-2xl font-bold text-[var(--color-dash-success)]">17%</p>
1278
- </div>
1279
- <div>
1280
- <p className="text-xs text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] mb-1 uppercase tracking-wider">Contract Term</p>
1281
- <p className="text-sm font-semibold text-[var(--color-dash-text)] dark:text-white">Mar 2025 - Feb 2027</p>
1282
- </div>
1283
- </div>
1284
-
1285
- <div className="bg-[var(--color-dash-surface)] dark:bg-[var(--color-dash-muted)]/10 rounded-lg p-4 space-y-3">
1286
- <div>
1287
- <p className="text-xs text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] mb-1 uppercase tracking-wider">Attrition Method</p>
1288
- <p className="text-sm font-semibold text-[var(--color-dash-text)] dark:text-white">Per Night (80% threshold)</p>
1289
- </div>
1290
- <div>
1291
- <p className="text-xs text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] mb-1 uppercase tracking-wider">Resale Credit Policy</p>
1292
- <p className="text-sm font-semibold text-[var(--color-dash-text)] dark:text-white">Partial Credit (50%)</p>
1293
- </div>
1294
- </div>
1295
-
1296
- <div className="pt-2">
1297
- <button
1298
- onClick={() => toast.info("Opening full contract PDF...")}
1299
- 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"
1300
- >
1301
- <DocumentArrowDownIcon className="h-4 w-4" />
1302
- Download Full Contract
1303
- </button>
1304
- </div>
1305
- </div>
1306
- </div>
1307
- </div>
1308
- )}
1309
-
1310
- {/* Section — Recent Activity */}
1311
- <div className="pt-8 space-y-2">
1312
- <h2 className="text-3xl font-bold text-[var(--color-dash-text)] dark:text-white tracking-tight">
1313
- Recent activity
1314
- </h2>
1315
- <p className="text-lg text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]">
1316
- Here's what's been happening with your properties
1317
- </p>
1318
- </div>
1319
-
1320
- {/* Action Items Grid */}
1321
- {isLoading ? (
1322
- <div className="grid grid-cols-1 md:grid-cols-2 gap-5">
1323
- <CardSkeleton lines={4} />
1324
- <CardSkeleton lines={4} />
1325
- </div>
1326
- ) : myDisputes.length > 0 ? (
1327
- <div className="space-y-5">
1328
- <h3 className="text-xl font-bold text-[var(--color-dash-text)] dark:text-white">
1329
- Items that need your attention
1330
- </h3>
1331
- <div className="grid grid-cols-1 md:grid-cols-2 gap-5">
1332
- {myDisputes.map((d) => (
1333
- <div
1334
- key={d.id}
1335
- onClick={() => {
1336
- toast.info(`Opening ${d.title}`);
1337
- setSelectedDispute(d);
1338
- }}
1339
- 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"
1340
- >
1341
- <div className="flex items-start justify-between gap-3 mb-3">
1342
- <h4 className="font-semibold text-[var(--color-dash-text)] dark:text-white flex-1">
1343
- {d.title}
1344
- </h4>
1345
- <span
1346
- className={`inline-flex items-center rounded-full px-2 py-1 text-xs font-medium flex-shrink-0 ${
1347
- d.status === "critical"
1348
- ? "bg-[var(--color-dash-danger)]/10 text-[var(--color-dash-danger)] border border-[var(--color-dash-danger)]/30"
1349
- : d.status === "warning"
1350
- ? "bg-[var(--color-dash-warning)]/10 text-[var(--color-dash-warning)] border border-[var(--color-dash-warning)]/30"
1351
- : "bg-[var(--color-dash-info)]/10 text-[var(--color-dash-info)] border border-[var(--color-dash-info)]/30"
1352
- }`}
1353
- >
1354
- {d.badge}
1355
- </span>
1356
- </div>
1357
- <p className="text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] mb-4">
1358
- {d.description}
1359
- </p>
1360
- <div className="flex items-center justify-between">
1361
- <span className="text-lg font-bold text-[var(--color-dash-text)] dark:text-white">
1362
- ${d.amount.toLocaleString()}
1363
- </span>
1364
- {d.agentHandled && (
1365
- <span className="text-xs text-[var(--color-dash-accent)] flex items-center gap-1">
1366
- <svg className="h-3 w-3" fill="currentColor" viewBox="0 0 20 20">
1367
- <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" />
1368
- </svg>
1369
- Agent reviewed
1370
- </span>
1371
- )}
1372
- </div>
1373
- </div>
1374
- ))}
1375
- </div>
1376
- </div>
1377
- ) : null}
1378
-
1379
- {/* All Penalties - Enhanced Table */}
1380
- {isLoading ? (
1381
- <CardSkeleton lines={8} />
1382
- ) : (
1383
- <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 hover:shadow-lg transition-shadow duration-300">
1384
- <div className="p-8 border-b border-[var(--color-dash-label)]/20 dark:border-[var(--color-dash-muted)]/30">
1385
- <h3 className="text-2xl font-bold text-[var(--color-dash-text)] dark:text-white mb-2">
1386
- All attrition penalties
1387
- </h3>
1388
- <p className="text-base text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]">
1389
- {myPenalties.length} {myPenalties.length === 1 ? 'penalty' : 'penalties'} · Showing recent calculations and adjustments
1390
- </p>
1391
- </div>
1392
- <div className="overflow-x-auto">
1393
- <table className="w-full">
1394
- <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">
1395
- <tr>
1396
- <th className="px-6 py-3 text-left text-xs font-semibold text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] uppercase tracking-wider">
1397
- Penalty ID
1398
- </th>
1399
- <th className="px-6 py-3 text-left text-xs font-semibold text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] uppercase tracking-wider">
1400
- Property
1401
- </th>
1402
- <th className="px-6 py-3 text-left text-xs font-semibold text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] uppercase tracking-wider">
1403
- Customer
1404
- </th>
1405
- <th className="px-6 py-3 text-left text-xs font-semibold text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] uppercase tracking-wider">
1406
- Method
1407
- </th>
1408
- <th className="px-6 py-3 text-right text-xs font-semibold text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] uppercase tracking-wider">
1409
- Penalty
1410
- </th>
1411
- <th className="px-6 py-3 text-right text-xs font-semibold text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] uppercase tracking-wider">
1412
- Credit
1413
- </th>
1414
- <th className="px-6 py-3 text-center text-xs font-semibold text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] uppercase tracking-wider">
1415
- Status
1416
- </th>
1417
- </tr>
1418
- </thead>
1419
- <tbody className="divide-y divide-[var(--color-dash-label)]/20 dark:divide-[var(--color-dash-muted)]/30">
1420
- {myPenalties.map((penalty) => (
1421
- <tr
1422
- key={penalty.id}
1423
- onClick={() => {
1424
- setSelectedPenalty(penalty);
1425
- setIsPenaltyModalOpen(true);
1426
- }}
1427
- className={`hover:bg-[var(--color-dash-surface)]/50 dark:hover:bg-[var(--color-dash-muted)]/5 transition-colors cursor-pointer ${
1428
- penalty.isHero ? "bg-[var(--color-dash-warning)]/5" : ""
1429
- }`}
1430
- >
1431
- <td className="px-6 py-4 whitespace-nowrap">
1432
- <div className="flex items-center gap-2">
1433
- <span className="text-sm font-medium text-[var(--color-dash-text)] dark:text-white">
1434
- {penalty.name}
1435
- </span>
1436
- {penalty.isHero && (
1437
- <span className="inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium bg-[var(--color-dash-warning)]/20 text-[var(--color-dash-warning)] border border-[var(--color-dash-warning)]/30">
1438
- Review
1439
- </span>
1440
- )}
1441
- </div>
1442
- </td>
1443
- <td className="px-6 py-4 text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]">
1444
- {penalty.property}
1445
- </td>
1446
- <td className="px-6 py-4 text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]">
1447
- {penalty.customer}
1448
- </td>
1449
- <td className="px-6 py-4 text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]">
1450
- {penalty.method}
1451
- </td>
1452
- <td className="px-6 py-4 text-sm font-semibold text-right text-[var(--color-dash-text)] dark:text-white">
1453
- ${penalty.penalty.toLocaleString()}
1454
- </td>
1455
- <td className="px-6 py-4 text-sm font-semibold text-right text-[var(--color-dash-success)]">
1456
- ${penalty.credit.toLocaleString()}
1457
- </td>
1458
- <td className="px-6 py-4 text-center">
1459
- <span
1460
- className={`inline-flex items-center rounded-full px-2.5 py-1 text-xs font-medium ${
1461
- penalty.status === "Approved"
1462
- ? "bg-[var(--color-dash-success)]/10 text-[var(--color-dash-success)] border border-[var(--color-dash-success)]/30"
1463
- : penalty.status === "Reviewed"
1464
- ? "bg-[var(--color-dash-info)]/10 text-[var(--color-dash-info)] border border-[var(--color-dash-info)]/30"
1465
- : penalty.status === "Calculated"
1466
- ? "bg-[var(--color-dash-label)]/10 text-[var(--color-dash-muted)] border border-[var(--color-dash-label)]/30"
1467
- : "bg-[var(--color-dash-danger)]/10 text-[var(--color-dash-danger)] border border-[var(--color-dash-danger)]/30"
1468
- }`}
1469
- >
1470
- {penalty.status}
1471
- </span>
1472
- </td>
1473
- </tr>
1474
- ))}
1475
- </tbody>
1476
- </table>
1477
- </div>
1478
- </div>
1479
- )}
1480
-
1481
- {/* Section */}
1482
- <div className="pt-8 space-y-2">
1483
- <h2 className="text-3xl font-bold text-[var(--color-dash-text)] dark:text-white tracking-tight">
1484
- What's been happening
1485
- </h2>
1486
- <p className="text-lg text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]">
1487
- A quick timeline of recent updates
1488
- </p>
1489
- </div>
1490
-
1491
- {/* Activity Feed */}
1492
- {isLoading ? (
1493
- <CardSkeleton lines={6} />
1494
- ) : (
1495
- <ActivityCard
1496
- title="Recent updates"
1497
- actions={myActivity.map((a) => ({
1498
- id: a.id,
1499
- status:
1500
- a.status === "alert"
1501
- ? "error"
1502
- : a.status === "warning"
1503
- ? "pending"
1504
- : a.status === "success"
1505
- ? "complete"
1506
- : "working",
1507
- title: a.title,
1508
- subtitle: a.description,
1509
- timestamp: a.timestamp,
1510
- }))}
1511
- />
1512
- )}
1513
-
1514
- {/* Help Section - Enhanced */}
1515
- <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">
1516
- <div className="text-center max-w-2xl mx-auto space-y-6">
1517
- <h3 className="text-3xl font-bold text-[var(--color-dash-text)] dark:text-white tracking-tight">
1518
- Questions? We're here to help
1519
- </h3>
1520
- <p className="text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] text-xl leading-relaxed">
1521
- Our partner team is standing by if you need anything — from billing questions to contract details.
1522
- </p>
1523
- <div className="flex gap-4 justify-center flex-wrap pt-2">
1524
- <Modal>
1525
- <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">
1526
- Get in touch
1527
- </button>
1528
- <Modal.Backdrop>
1529
- <Modal.Container>
1530
- <Modal.Dialog className="max-w-2xl">
1531
- <Modal.CloseTrigger />
1532
- <Modal.Header>
1533
- <Modal.Heading className="text-2xl font-bold text-[var(--color-dash-text)] dark:text-white">
1534
- Contact Support
1535
- </Modal.Heading>
1536
- </Modal.Header>
1537
- <Modal.Body className="space-y-6">
1538
- <p className="text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]">
1539
- Our partner support team is here to help with billing questions, contract details, or any other issues you may have.
1540
- </p>
1541
-
1542
- {/* Contact Options */}
1543
- <div className="space-y-4">
1544
- <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">
1545
- <div className="flex items-start gap-4">
1546
- <div className="bg-[var(--color-dash-accent)]/20 rounded-lg p-3">
1547
- <svg className="h-6 w-6 text-[var(--color-dash-accent)]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
1548
- <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" />
1549
- </svg>
1550
- </div>
1551
- <div>
1552
- <h4 className="font-semibold text-[var(--color-dash-text)] dark:text-white mb-1">Email Support</h4>
1553
- <p className="text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] mb-2">
1554
- Get help via email - we typically respond within 2-4 hours
1555
- </p>
1556
- <a href="mailto:partners@engine.com" className="text-sm font-medium text-[var(--color-dash-accent)] hover:underline">
1557
- partners@engine.com
1558
- </a>
1559
- </div>
1560
- </div>
1561
- </div>
1562
-
1563
- <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">
1564
- <div className="flex items-start gap-4">
1565
- <div className="bg-[var(--color-dash-success)]/20 rounded-lg p-3">
1566
- <svg className="h-6 w-6 text-[var(--color-dash-success)]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
1567
- <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" />
1568
- </svg>
1569
- </div>
1570
- <div>
1571
- <h4 className="font-semibold text-[var(--color-dash-text)] dark:text-white mb-1">Phone Support</h4>
1572
- <p className="text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] mb-2">
1573
- Speak with a partner specialist directly
1574
- </p>
1575
- <a href="tel:+18005551234" className="text-sm font-medium text-[var(--color-dash-success)] hover:underline">
1576
- 1-800-555-1234
1577
- </a>
1578
- <p className="text-xs text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] mt-1">
1579
- Mon-Fri, 8am-6pm EST
1580
- </p>
1581
- </div>
1582
- </div>
1583
- </div>
1584
- </div>
1585
- </Modal.Body>
1586
- <Modal.Footer>
1587
- <div className="flex gap-3 justify-end">
1588
- <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">
1589
- Close
1590
- </Modal.CloseTrigger>
1591
- </div>
1592
- </Modal.Footer>
1593
- </Modal.Dialog>
1594
- </Modal.Container>
1595
- </Modal.Backdrop>
1596
- </Modal>
1597
-
1598
- <button
1599
- onClick={() => toast.info("Opening help documentation...")}
1600
- 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"
1601
- >
1602
- Browse help docs
1603
- </button>
1604
- </div>
1605
- </div>
1606
- </div>
1607
-
1608
- {/* Footer */}
1609
- <div className="text-center py-8 text-sm text-[var(--color-dash-label)]">
1610
- <p>© 2026 Engine · Welcome to modern travel management</p>
1611
- </div>
1612
- </div>
1613
-
1614
- {/* Properties Modal */}
1615
- <Modal isOpen={isPropertiesModalOpen} onOpenChange={setIsPropertiesModalOpen}>
1616
- <Modal.Backdrop>
1617
- <Modal.Container>
1618
- <Modal.Dialog className="max-w-4xl">
1619
- <Modal.CloseTrigger />
1620
- <Modal.Header>
1621
- <Modal.Heading className="text-2xl font-bold text-[var(--color-dash-text)] dark:text-white">
1622
- Your Properties
1623
- </Modal.Heading>
1624
- </Modal.Header>
1625
- <Modal.Body className="space-y-4">
1626
- <p className="text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] mb-4">
1627
- {myProperties} active properties in your portfolio
1628
- </p>
1629
- <div className="space-y-3">
1630
- {revenueTrendByProperty.properties?.map((prop, idx) => (
1631
- <div
1632
- key={idx}
1633
- 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"
1634
- >
1635
- <div className="flex items-start justify-between gap-4">
1636
- <div className="flex items-start gap-3 flex-1">
1637
- <div className="mt-1">
1638
- <BuildingOfficeIcon className="h-5 w-5 text-[var(--color-dash-accent)]" />
1639
- </div>
1640
- <div>
1641
- <h4 className="font-semibold text-[var(--color-dash-text)] dark:text-white mb-1">
1642
- {prop.name}
1643
- </h4>
1644
- <p className="text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]">
1645
- Latest revenue: ${prop.values[prop.values.length - 1].toLocaleString()}
1646
- </p>
1647
- </div>
1648
- </div>
1649
- <span
1650
- className="inline-block h-3 w-3 rounded-full flex-shrink-0 mt-2"
1651
- style={{ backgroundColor: prop.color }}
1652
- />
1653
- </div>
1654
- </div>
1655
- ))}
1656
- </div>
1657
- </Modal.Body>
1658
- <Modal.Footer>
1659
- <Modal.CloseTrigger asChild>
1660
- <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">
1661
- Close
1662
- </button>
1663
- </Modal.CloseTrigger>
1664
- </Modal.Footer>
1665
- </Modal.Dialog>
1666
- </Modal.Container>
1667
- </Modal.Backdrop>
1668
- </Modal>
1669
-
1670
- {/* Revenue Modal */}
1671
- <Modal isOpen={isRevenueModalOpen} onOpenChange={setIsRevenueModalOpen}>
1672
- <Modal.Backdrop>
1673
- <Modal.Container>
1674
- <Modal.Dialog className="max-w-4xl">
1675
- <Modal.CloseTrigger />
1676
- <Modal.Header>
1677
- <Modal.Heading className="text-2xl font-bold text-[var(--color-dash-text)] dark:text-white">
1678
- Revenue Breakdown
1679
- </Modal.Heading>
1680
- </Modal.Header>
1681
- <Modal.Body className="space-y-4">
1682
- <div className="bg-gradient-to-br from-[var(--color-dash-success)]/10 to-[var(--color-dash-accent)]/10 rounded-xl p-6 mb-4">
1683
- <p className="text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] mb-2">Total Revenue</p>
1684
- <p className="text-4xl font-bold text-[var(--color-dash-text)] dark:text-white">
1685
- ${myRevenue.toLocaleString()}
1686
- </p>
1687
- </div>
1688
- <h3 className="font-semibold text-[var(--color-dash-text)] dark:text-white mb-3">By Property</h3>
1689
- <div className="space-y-3">
1690
- {revenueTrendByProperty.properties?.map((prop, idx) => {
1691
- const totalRevenue = prop.values.reduce((sum, val) => sum + val, 0);
1692
- const percentage = ((totalRevenue / myRevenue) * 100).toFixed(1);
1693
- return (
1694
- <div
1695
- key={idx}
1696
- 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"
1697
- >
1698
- <div className="flex items-center justify-between mb-3">
1699
- <div className="flex items-center gap-3">
1700
- <span
1701
- className="inline-block h-3 w-3 rounded-full"
1702
- style={{ backgroundColor: prop.color }}
1703
- />
1704
- <span className="font-semibold text-[var(--color-dash-text)] dark:text-white">
1705
- {prop.name}
1706
- </span>
1707
- </div>
1708
- <span className="text-lg font-bold text-[var(--color-dash-text)] dark:text-white">
1709
- ${totalRevenue.toLocaleString()}
1710
- </span>
1711
- </div>
1712
- <div className="flex items-center gap-3">
1713
- <div className="flex-1 bg-[var(--color-dash-label)]/20 rounded-full h-2">
1714
- <div
1715
- className="h-2 rounded-full"
1716
- style={{
1717
- width: `${percentage}%`,
1718
- backgroundColor: prop.color,
1719
- }}
1720
- />
1721
- </div>
1722
- <span className="text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] w-12 text-right">
1723
- {percentage}%
1724
- </span>
1725
- </div>
1726
- </div>
1727
- );
1728
- })}
1729
- </div>
1730
- </Modal.Body>
1731
- <Modal.Footer>
1732
- <button
1733
- onClick={() => setIsRevenueModalOpen(false)}
1734
- 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"
1735
- >
1736
- Close
1737
- </button>
1738
- </Modal.Footer>
1739
- </Modal.Dialog>
1740
- </Modal.Container>
1741
- </Modal.Backdrop>
1742
- </Modal>
1743
-
1744
- {/* Reservations Modal */}
1745
- <Modal isOpen={isReservationsModalOpen} onOpenChange={setIsReservationsModalOpen}>
1746
- <Modal.Backdrop>
1747
- <Modal.Container>
1748
- <Modal.Dialog className="max-w-4xl">
1749
- <Modal.CloseTrigger />
1750
- <Modal.Header>
1751
- <Modal.Heading className="text-2xl font-bold text-[var(--color-dash-text)] dark:text-white">
1752
- Reservations
1753
- </Modal.Heading>
1754
- </Modal.Header>
1755
- <Modal.Body>
1756
- <p className="text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] mb-4">
1757
- {myReservations} total reservations through Engine
1758
- </p>
1759
- <div className="bg-[var(--color-dash-surface)] dark:bg-[var(--color-dash-muted)]/10 rounded-xl p-8 text-center">
1760
- <ClockIcon className="h-12 w-12 text-[var(--color-dash-info)] mx-auto mb-3" />
1761
- <p className="text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]">
1762
- Detailed reservation data is available in your full property management dashboard
1763
- </p>
1764
- </div>
1765
- </Modal.Body>
1766
- <Modal.Footer>
1767
- <button
1768
- onClick={() => setIsReservationsModalOpen(false)}
1769
- 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"
1770
- >
1771
- Close
1772
- </button>
1773
- </Modal.Footer>
1774
- </Modal.Dialog>
1775
- </Modal.Container>
1776
- </Modal.Backdrop>
1777
- </Modal>
1778
-
1779
- {/* Disputes Modal */}
1780
- <Modal isOpen={isDisputesModalOpen} onOpenChange={setIsDisputesModalOpen}>
1781
- <Modal.Backdrop>
1782
- <Modal.Container>
1783
- <Modal.Dialog className="max-w-4xl">
1784
- <Modal.CloseTrigger />
1785
- <Modal.Header>
1786
- <Modal.Heading className="text-2xl font-bold text-[var(--color-dash-text)] dark:text-white">
1787
- Items to Review
1788
- </Modal.Heading>
1789
- </Modal.Header>
1790
- <Modal.Body className="space-y-4">
1791
- <p className="text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] mb-4">
1792
- {myOpenDisputes} {myOpenDisputes === 1 ? 'item needs' : 'items need'} your attention
1793
- </p>
1794
- {myDisputes.length > 0 ? (
1795
- <div className="space-y-3">
1796
- {myDisputes.map((d) => (
1797
- <div
1798
- key={d.id}
1799
- 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"
1800
- >
1801
- <div className="flex items-start justify-between gap-3 mb-2">
1802
- <h4 className="font-semibold text-[var(--color-dash-text)] dark:text-white flex-1">
1803
- {d.title}
1804
- </h4>
1805
- <span
1806
- className={`inline-flex items-center rounded-full px-2 py-1 text-xs font-medium flex-shrink-0 ${
1807
- d.status === "critical"
1808
- ? "bg-[var(--color-dash-danger)]/10 text-[var(--color-dash-danger)] border border-[var(--color-dash-danger)]/30"
1809
- : d.status === "warning"
1810
- ? "bg-[var(--color-dash-warning)]/10 text-[var(--color-dash-warning)] border border-[var(--color-dash-warning)]/30"
1811
- : "bg-[var(--color-dash-info)]/10 text-[var(--color-dash-info)] border border-[var(--color-dash-info)]/30"
1812
- }`}
1813
- >
1814
- {d.badge}
1815
- </span>
1816
- </div>
1817
- <p className="text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] mb-3">
1818
- {d.description}
1819
- </p>
1820
- <div className="flex items-center justify-between">
1821
- <span className="text-lg font-bold text-[var(--color-dash-text)] dark:text-white">
1822
- ${d.amount.toLocaleString()}
1823
- </span>
1824
- {d.agentHandled && (
1825
- <span className="text-xs text-[var(--color-dash-accent)] flex items-center gap-1">
1826
- <svg className="h-3 w-3" fill="currentColor" viewBox="0 0 20 20">
1827
- <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" />
1828
- </svg>
1829
- Agent reviewed
1830
- </span>
1831
- )}
1832
- </div>
1833
- </div>
1834
- ))}
1835
- </div>
1836
- ) : (
1837
- <div className="bg-[var(--color-dash-surface)] dark:bg-[var(--color-dash-muted)]/10 rounded-xl p-8 text-center">
1838
- <p className="text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]">No items need attention</p>
1839
- </div>
1840
- )}
1841
- </Modal.Body>
1842
- <Modal.Footer>
1843
- <button
1844
- onClick={() => setIsDisputesModalOpen(false)}
1845
- 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"
1846
- >
1847
- Close
1848
- </button>
1849
- </Modal.Footer>
1850
- </Modal.Dialog>
1851
- </Modal.Container>
1852
- </Modal.Backdrop>
1853
- </Modal>
1854
-
1855
- {/* Penalty Modal */}
1856
- <Modal isOpen={isPenaltyModalOpen} onOpenChange={setIsPenaltyModalOpen}>
1857
- <Modal.Backdrop>
1858
- <Modal.Container>
1859
- <Modal.Dialog className="w-[95vw] max-w-4xl max-h-[90vh] overflow-y-auto">
1860
- <Modal.CloseTrigger />
1861
- <Modal.Header className="p-8 pb-4">
1862
- <Modal.Heading className="text-2xl font-bold text-[var(--color-dash-text)] dark:text-white pr-8">
1863
- Penalty Review: {selectedPenalty?.name}
1864
- </Modal.Heading>
1865
- </Modal.Header>
1866
- <Modal.Body className="space-y-6 px-8 py-4">
1867
- {selectedPenalty && (
1868
- <>
1869
- {/* Property & Customer Info */}
1870
- <div className="grid grid-cols-2 gap-4">
1871
- <div>
1872
- <p className="text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] mb-1">Property</p>
1873
- <p className="font-semibold text-[var(--color-dash-text)] dark:text-white">{selectedPenalty.property}</p>
1874
- </div>
1875
- <div>
1876
- <p className="text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] mb-1">Customer</p>
1877
- <p className="font-semibold text-[var(--color-dash-text)] dark:text-white">{selectedPenalty.customer}</p>
1878
- </div>
1879
- </div>
1880
-
1881
- {/* Booking Details */}
1882
- <div className="bg-[var(--color-dash-surface)] dark:bg-[var(--color-dash-muted)]/10 rounded-xl p-6">
1883
- <h4 className="font-semibold text-[var(--color-dash-text)] dark:text-white mb-4">Booking Details</h4>
1884
- <div className="grid grid-cols-3 gap-6">
1885
- <div>
1886
- <p className="text-xs text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] mb-2 uppercase tracking-wider">Original Block</p>
1887
- <p className="text-xl font-bold text-[var(--color-dash-text)] dark:text-white">{selectedPenalty.originalRoomBlock} rooms</p>
1888
- </div>
1889
- <div>
1890
- <p className="text-xs text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] mb-2 uppercase tracking-wider">Rooms Used</p>
1891
- <p className="text-xl font-bold text-[var(--color-dash-success)]">{selectedPenalty.actualRoomsUsed} rooms</p>
1892
- </div>
1893
- <div>
1894
- <p className="text-xs text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] mb-2 uppercase tracking-wider">Unused</p>
1895
- <p className="text-xl font-bold text-[var(--color-dash-danger)]">{selectedPenalty.unusedRooms} rooms</p>
1896
- </div>
1897
- </div>
1898
- <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">
1899
- <div>
1900
- <p className="text-xs text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] mb-2 uppercase tracking-wider">Room Rate</p>
1901
- <p className="text-xl font-bold text-[var(--color-dash-text)] dark:text-white">${selectedPenalty.roomRate}/night</p>
1902
- </div>
1903
- <div>
1904
- <p className="text-xs text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] mb-2 uppercase tracking-wider">Number of Nights</p>
1905
- <p className="text-xl font-bold text-[var(--color-dash-text)] dark:text-white">{selectedPenalty.numberOfNights}</p>
1906
- </div>
1907
- <div>
1908
- <p className="text-xs text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] mb-2 uppercase tracking-wider">Rooms Resold</p>
1909
- <p className="text-xl font-bold text-[var(--color-dash-accent)]">{selectedPenalty.roomsResold} rooms</p>
1910
- </div>
1911
- </div>
1912
- </div>
1913
-
1914
- {/* Calculation Method & Policy */}
1915
- <div className="grid grid-cols-2 gap-6">
1916
- <div>
1917
- <p className="text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] mb-2">Calculation Method</p>
1918
- <p className="text-lg font-semibold text-[var(--color-dash-text)] dark:text-white">{selectedPenalty.method}</p>
1919
- </div>
1920
- <div>
1921
- <p className="text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] mb-2">Resale Policy</p>
1922
- <p className="text-lg font-semibold text-[var(--color-dash-text)] dark:text-white">{selectedPenalty.resalePolicy}</p>
1923
- </div>
1924
- </div>
1925
-
1926
- {/* Financial Details */}
1927
- <div>
1928
- <h4 className="font-semibold text-[var(--color-dash-text)] dark:text-white mb-4">Financial Summary</h4>
1929
- <div className="grid grid-cols-3 gap-6">
1930
- <div className="bg-[var(--color-dash-surface)] dark:bg-[var(--color-dash-muted)]/10 rounded-lg p-5">
1931
- <p className="text-xs text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] mb-2 uppercase tracking-wider">Calculated Penalty</p>
1932
- <p className="text-2xl font-bold text-[var(--color-dash-text)] dark:text-white">${selectedPenalty.penaltyCalculated.toLocaleString()}</p>
1933
- </div>
1934
- <div className="bg-[var(--color-dash-surface)] dark:bg-[var(--color-dash-muted)]/10 rounded-lg p-5">
1935
- <p className="text-xs text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] mb-2 uppercase tracking-wider">Credit Applied</p>
1936
- <p className="text-2xl font-bold text-[var(--color-dash-success)]">${selectedPenalty.credit.toLocaleString()}</p>
1937
- </div>
1938
- <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'}`}>
1939
- <p className="text-xs text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] mb-2 uppercase tracking-wider">Final Penalty</p>
1940
- <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>
1941
- </div>
1942
- </div>
1943
- </div>
1944
-
1945
- {/* Issue Description */}
1946
- {selectedPenalty.isHero && (
1947
- <div className="bg-[var(--color-dash-warning)]/10 border-l-4 border-[var(--color-dash-warning)] rounded-lg p-5">
1948
- <p className="font-semibold text-[var(--color-dash-text)] dark:text-white mb-3 flex items-center gap-2">
1949
- <ExclamationTriangleIcon className="h-5 w-5 text-[var(--color-dash-warning)]" />
1950
- Why this needs review:
1951
- </p>
1952
- <p className="text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] leading-relaxed">
1953
- 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'},
1954
- we'd expect to see a ${(selectedPenalty.roomsResold * selectedPenalty.roomRate * selectedPenalty.numberOfNights * 0.5).toLocaleString()} credit that doesn't appear to be fully applied.
1955
- </p>
1956
- </div>
1957
- )}
1958
-
1959
- {/* Status */}
1960
- <div>
1961
- <p className="text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] mb-2">Status</p>
1962
- <span
1963
- className={`inline-flex items-center rounded-full px-3 py-1 text-sm font-medium ${
1964
- selectedPenalty.status === "Approved"
1965
- ? "bg-[var(--color-dash-success)]/10 text-[var(--color-dash-success)] border border-[var(--color-dash-success)]/30"
1966
- : selectedPenalty.status === "Reviewed"
1967
- ? "bg-[var(--color-dash-info)]/10 text-[var(--color-dash-info)] border border-[var(--color-dash-info)]/30"
1968
- : selectedPenalty.status === "Calculated"
1969
- ? "bg-[var(--color-dash-label)]/10 text-[var(--color-dash-muted)] border border-[var(--color-dash-label)]/30"
1970
- : "bg-[var(--color-dash-warning)]/10 text-[var(--color-dash-warning)] border border-[var(--color-dash-warning)]/30"
1971
- }`}
1972
- >
1973
- {selectedPenalty.status}
1974
- </span>
1975
- </div>
1976
- </>
1977
- )}
1978
- </Modal.Body>
1979
- <Modal.Footer className="p-8 pt-4">
1980
- <div className="flex gap-4 justify-end flex-wrap">
1981
- <button
1982
- onClick={() => setIsPenaltyModalOpen(false)}
1983
- 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"
1984
- >
1985
- Close
1986
- </button>
1987
- <button
1988
- onClick={() => {
1989
- toast.success("Dispute has been filed. Support team will follow up.");
1990
- setIsPenaltyModalOpen(false);
1991
- }}
1992
- 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"
1993
- >
1994
- File Dispute
1995
- </button>
1996
- </div>
1997
- </Modal.Footer>
1998
- </Modal.Dialog>
1999
- </Modal.Container>
2000
- </Modal.Backdrop>
2001
- </Modal>
2002
-
2003
- {/* Invoices Modal */}
2004
- <Modal isOpen={isInvoicesModalOpen} onOpenChange={setIsInvoicesModalOpen}>
2005
- <Modal.Backdrop>
2006
- <Modal.Container>
2007
- <Modal.Dialog className="max-w-4xl">
2008
- <Modal.CloseTrigger />
2009
- <Modal.Header>
2010
- <Modal.Heading className="text-2xl font-bold text-[var(--color-dash-text)] dark:text-white">
2011
- Your Invoices
2012
- </Modal.Heading>
2013
- </Modal.Header>
2014
- <Modal.Body className="space-y-4">
2015
- <p className="text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] mb-4">
2016
- {myPendingInvoices} {myPendingInvoices === 1 ? 'invoice' : 'invoices'} ready to pay
2017
- </p>
2018
- {myInvoices.length > 0 ? (
2019
- <div className="space-y-3">
2020
- {myInvoices.map((inv) => (
2021
- <div
2022
- key={inv.id}
2023
- 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"
2024
- >
2025
- <div className="flex items-start justify-between gap-3 mb-2">
2026
- <div>
2027
- <h4 className="font-semibold text-[var(--color-dash-text)] dark:text-white">
2028
- {inv.title}
2029
- </h4>
2030
- <p className="text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] mt-1">
2031
- {inv.description}
2032
- </p>
2033
- </div>
2034
- <span
2035
- className={`inline-flex items-center rounded-full px-2 py-1 text-xs font-medium flex-shrink-0 ${
2036
- inv.status === "critical"
2037
- ? "bg-[var(--color-dash-danger)]/10 text-[var(--color-dash-danger)] border border-[var(--color-dash-danger)]/30"
2038
- : "bg-[var(--color-dash-info)]/10 text-[var(--color-dash-info)] border border-[var(--color-dash-info)]/30"
2039
- }`}
2040
- >
2041
- {inv.badge}
2042
- </span>
2043
- </div>
2044
- <div className="flex items-center justify-between mt-3">
2045
- <span className="text-lg font-bold text-[var(--color-dash-text)] dark:text-white">
2046
- ${inv.amount.toLocaleString()}
2047
- </span>
2048
- <span className="text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]">
2049
- Due {inv.due}
2050
- </span>
2051
- </div>
2052
- </div>
2053
- ))}
2054
- </div>
2055
- ) : (
2056
- <div className="bg-[var(--color-dash-surface)] dark:bg-[var(--color-dash-muted)]/10 rounded-xl p-8 text-center">
2057
- <p className="text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]">No pending invoices</p>
2058
- </div>
2059
- )}
2060
- </Modal.Body>
2061
- <Modal.Footer>
2062
- <button
2063
- onClick={() => setIsInvoicesModalOpen(false)}
2064
- 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"
2065
- >
2066
- Close
2067
- </button>
2068
- </Modal.Footer>
2069
- </Modal.Dialog>
2070
- </Modal.Container>
2071
- </Modal.Backdrop>
2072
- </Modal>
2073
-
2074
- <AgentPanel />
2075
- </div>
2076
- );
2077
- }