@schandlergarcia/sf-web-components 2.3.15 → 2.3.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/brands/engine/app/components/AgentPanel.tsx +247 -108
- package/brands/engine/app/components/Data360Widget.tsx +301 -0
- package/brands/engine/app/data/partner-hub-sample-data.js +297 -0
- package/brands/engine/app/pages/PartnerHubDashboard.tsx +1036 -353
- package/brands/engine/app/styles/global.css +1 -1
- package/brands/engine/global.css +1 -1
- package/dist/styles/global.css +60 -49
- package/package.json +1 -1
- package/src/styles/global.css +60 -49
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ListCard, ActivityCard, D3Chart, Dropdown, Button, Modal, CardSkeleton } from "@/components/library";
|
|
1
|
+
import { ListCard, ActivityCard, D3Chart, Dropdown, Button, Modal, CardSkeleton, Tabs } from "@/components/library";
|
|
2
2
|
import useDataSource from "@/components/library/data/useDataSource";
|
|
3
3
|
import { useThemeMode } from "@/components/library/theme/AppThemeProvider";
|
|
4
4
|
import { toast } from "sonner";
|
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
RocketLaunchIcon,
|
|
31
31
|
LightBulbIcon,
|
|
32
32
|
DocumentArrowDownIcon,
|
|
33
|
+
SparklesIcon,
|
|
33
34
|
} from "@heroicons/react/24/outline";
|
|
34
35
|
import * as d3 from "d3";
|
|
35
36
|
import engineLogo from "@/assets/images/engine_logo.png";
|
|
@@ -45,8 +46,24 @@ import engineLogo from "@/assets/images/engine_logo.png";
|
|
|
45
46
|
* - Their contract details
|
|
46
47
|
* - Communication with Engine
|
|
47
48
|
*/
|
|
49
|
+
type TabId = "overview" | "cases" | "properties" | "analytics";
|
|
50
|
+
|
|
48
51
|
export default function PartnerHubDashboard() {
|
|
49
52
|
const { mode, toggle } = useThemeMode();
|
|
53
|
+
const [activeTab, setActiveTab] = React.useState<TabId>("overview");
|
|
54
|
+
const [tabLoading, setTabLoading] = React.useState(false);
|
|
55
|
+
const tabLoadRef = React.useRef<ReturnType<typeof setTimeout>>();
|
|
56
|
+
|
|
57
|
+
const switchTab = React.useCallback((id: TabId) => {
|
|
58
|
+
if (id === activeTab) return;
|
|
59
|
+
setTabLoading(true);
|
|
60
|
+
setActiveTab(id);
|
|
61
|
+
clearTimeout(tabLoadRef.current);
|
|
62
|
+
tabLoadRef.current = setTimeout(() => setTabLoading(false), 700);
|
|
63
|
+
}, [activeTab]);
|
|
64
|
+
|
|
65
|
+
React.useEffect(() => () => clearTimeout(tabLoadRef.current), []);
|
|
66
|
+
|
|
50
67
|
const [selectedPenalty, setSelectedPenalty] = React.useState(null);
|
|
51
68
|
const [isPenaltyModalOpen, setIsPenaltyModalOpen] = React.useState(false);
|
|
52
69
|
const [isPropertiesModalOpen, setIsPropertiesModalOpen] = React.useState(false);
|
|
@@ -69,6 +86,7 @@ export default function PartnerHubDashboard() {
|
|
|
69
86
|
|
|
70
87
|
// Determine if we're loading (only in live mode)
|
|
71
88
|
const isLoading = !ENABLE_SAMPLE_DATA_CACHE && liveLoading;
|
|
89
|
+
const showSkeleton = isLoading || tabLoading;
|
|
72
90
|
|
|
73
91
|
// Show error toast if live data fails
|
|
74
92
|
React.useEffect(() => {
|
|
@@ -130,7 +148,7 @@ export default function PartnerHubDashboard() {
|
|
|
130
148
|
return status !== "Paid";
|
|
131
149
|
}).map((i: any) => ({
|
|
132
150
|
id: i.Id,
|
|
133
|
-
title: `${i.Name?.value || i.Name}
|
|
151
|
+
title: `${i.Name?.value || i.Name} - ${currentPartner.name}`,
|
|
134
152
|
description: `${i.Invoice_Period_Start__c?.value || ""} to ${i.Invoice_Period_End__c?.value || ""}`,
|
|
135
153
|
status: (i.Invoice_Status__c?.value || i.Invoice_Status__c) === "Overdue" ? "critical" : "default",
|
|
136
154
|
badge: i.Invoice_Status__c?.value || i.Invoice_Status__c || "Draft",
|
|
@@ -243,7 +261,7 @@ export default function PartnerHubDashboard() {
|
|
|
243
261
|
// Calculate total revenue first (needed by leaderboard)
|
|
244
262
|
const myRevenue = React.useMemo(() => {
|
|
245
263
|
if (ENABLE_SAMPLE_DATA_CACHE) {
|
|
246
|
-
return
|
|
264
|
+
return 283000;
|
|
247
265
|
}
|
|
248
266
|
return (liveData?.invoices || []).reduce((sum: number, inv: any) => sum + (inv.Invoice_Total__c?.value || 0), 0);
|
|
249
267
|
}, [liveData?.invoices]);
|
|
@@ -279,8 +297,8 @@ export default function PartnerHubDashboard() {
|
|
|
279
297
|
revenue: propRevenue,
|
|
280
298
|
latestRevenue: latestRevenue,
|
|
281
299
|
growth: growth,
|
|
282
|
-
insight: idx === 0 ? `Highest booking volume through Engine
|
|
283
|
-
idx === properties.length - 1 ? `Bookings doubled since October
|
|
300
|
+
insight: idx === 0 ? `Highest booking volume through Engine, ${Math.round(weight * 100)}% of total` :
|
|
301
|
+
idx === properties.length - 1 ? `Bookings doubled since October, ${growth}% growth` :
|
|
284
302
|
`Strong booking growth in Q1 2026`
|
|
285
303
|
};
|
|
286
304
|
}).sort((a, b) => b.revenue - a.revenue);
|
|
@@ -827,7 +845,7 @@ export default function PartnerHubDashboard() {
|
|
|
827
845
|
Hey there Jamie! Here's what's happening with your properties
|
|
828
846
|
</h1>
|
|
829
847
|
<p className="text-lg text-white/70 leading-relaxed">
|
|
830
|
-
|
|
848
|
+
Track service cases, monitor guest satisfaction, and manage your partnership with Engine, all in one place.
|
|
831
849
|
</p>
|
|
832
850
|
</div>
|
|
833
851
|
</div>
|
|
@@ -837,12 +855,12 @@ export default function PartnerHubDashboard() {
|
|
|
837
855
|
<div className="max-w-[1600px] mx-auto px-8 -mt-12 space-y-10">
|
|
838
856
|
{/* Quick Stats - Uniform metrics grid */}
|
|
839
857
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-5 animate-slide-up relative z-10">
|
|
840
|
-
{/*
|
|
858
|
+
{/* Open Cases */}
|
|
841
859
|
<div
|
|
842
|
-
onClick={() => !isLoading &&
|
|
860
|
+
onClick={() => !isLoading && setIsDisputesModalOpen(true)}
|
|
843
861
|
className={isLoading ? "" : "cursor-pointer"}
|
|
844
862
|
>
|
|
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-
|
|
863
|
+
<div className="bg-white dark:bg-[var(--color-dash-text)] rounded-xl p-6 shadow-sm hover:shadow-lg transition-all duration-300 border border-[var(--color-dash-warning)]/50 dark:border-[var(--color-dash-warning)]/30 h-full">
|
|
846
864
|
{isLoading ? (
|
|
847
865
|
<div className="space-y-3">
|
|
848
866
|
<div className="flex items-center justify-between">
|
|
@@ -856,42 +874,39 @@ export default function PartnerHubDashboard() {
|
|
|
856
874
|
) : (
|
|
857
875
|
<>
|
|
858
876
|
<div className="flex items-center justify-between mb-3">
|
|
859
|
-
<div className="bg-[var(--color-dash-
|
|
860
|
-
<
|
|
877
|
+
<div className="bg-[var(--color-dash-warning)]/10 rounded-lg p-2">
|
|
878
|
+
<ExclamationTriangleIcon className="h-5 w-5 text-[var(--color-dash-warning)]" />
|
|
861
879
|
</div>
|
|
862
|
-
<span className="inline-flex items-center rounded-full px-2 py-0.5 text-xs font-bold bg-[var(--color-dash-
|
|
863
|
-
|
|
880
|
+
<span className="inline-flex items-center rounded-full px-2 py-0.5 text-xs font-bold bg-[var(--color-dash-danger)] text-white">
|
|
881
|
+
3 URGENT
|
|
864
882
|
</span>
|
|
865
883
|
</div>
|
|
866
|
-
<p className="text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] text-sm font-semibold mb-2 uppercase tracking-wider">
|
|
867
|
-
<p className="font-black text-[var(--color-dash-text)] dark:text-white mb-1 leading-tight" style={{ fontSize: 'var(--dash-metric-size)' }}
|
|
868
|
-
<p className="text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]">
|
|
884
|
+
<p className="text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] text-sm font-semibold mb-2 uppercase tracking-wider">Open Cases</p>
|
|
885
|
+
<p className="font-black text-[var(--color-dash-text)] dark:text-white mb-1 leading-tight" style={{ fontSize: 'var(--dash-metric-size)' }}>7</p>
|
|
886
|
+
<p className="text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]">across your properties</p>
|
|
869
887
|
</>
|
|
870
888
|
)}
|
|
871
889
|
</div>
|
|
872
890
|
</div>
|
|
873
891
|
|
|
874
|
-
{/*
|
|
875
|
-
<div
|
|
876
|
-
|
|
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">
|
|
892
|
+
{/* Guest Satisfaction */}
|
|
893
|
+
<div className="cursor-pointer">
|
|
894
|
+
<div className="bg-white dark:bg-[var(--color-dash-text)] rounded-xl p-6 shadow-sm hover:shadow-lg transition-all duration-300 border border-[var(--color-dash-label)]/20 dark:border-[var(--color-dash-muted)]/30 h-full">
|
|
880
895
|
{isLoading ? (
|
|
881
896
|
<CardSkeleton lines={4} />
|
|
882
897
|
) : (
|
|
883
898
|
<>
|
|
884
899
|
<div className="flex items-center justify-between mb-3">
|
|
885
|
-
<div className="bg-[var(--color-dash-
|
|
886
|
-
<
|
|
900
|
+
<div className="bg-[var(--color-dash-success)]/10 rounded-lg p-2">
|
|
901
|
+
<StarIcon className="h-5 w-5 text-[var(--color-dash-success)]" />
|
|
887
902
|
</div>
|
|
888
|
-
<span className="inline-flex items-center rounded-full px-2 py-0.5 text-xs font-bold bg-[var(--color-dash-
|
|
889
|
-
|
|
903
|
+
<span className="inline-flex items-center rounded-full px-2 py-0.5 text-xs font-bold bg-[var(--color-dash-success)]/10 text-[var(--color-dash-success)]">
|
|
904
|
+
+0.3
|
|
890
905
|
</span>
|
|
891
906
|
</div>
|
|
892
|
-
<p className="text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] text-sm font-semibold mb-2 uppercase tracking-wider">
|
|
893
|
-
<p className="font-black text-[var(--color-dash-text)] dark:text-white mb-1 leading-tight" style={{ fontSize: 'var(--dash-metric-size)' }}>
|
|
894
|
-
<p className="text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]">
|
|
907
|
+
<p className="text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] text-sm font-semibold mb-2 uppercase tracking-wider">Guest Satisfaction</p>
|
|
908
|
+
<p className="font-black text-[var(--color-dash-text)] dark:text-white mb-1 leading-tight" style={{ fontSize: 'var(--dash-metric-size)' }}>4.6</p>
|
|
909
|
+
<p className="text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]">out of 5.0 average</p>
|
|
895
910
|
</>
|
|
896
911
|
)}
|
|
897
912
|
</div>
|
|
@@ -920,11 +935,8 @@ export default function PartnerHubDashboard() {
|
|
|
920
935
|
</div>
|
|
921
936
|
</div>
|
|
922
937
|
|
|
923
|
-
{/*
|
|
924
|
-
<div
|
|
925
|
-
onClick={() => !isLoading && setIsReservationsModalOpen(true)}
|
|
926
|
-
className={isLoading ? "" : "cursor-pointer"}
|
|
927
|
-
>
|
|
938
|
+
{/* Avg Response Time */}
|
|
939
|
+
<div className="cursor-pointer">
|
|
928
940
|
<div className="bg-white dark:bg-[var(--color-dash-text)] rounded-xl p-6 shadow-sm hover:shadow-lg transition-all duration-300 border border-[var(--color-dash-label)]/20 dark:border-[var(--color-dash-muted)]/30 h-full">
|
|
929
941
|
{isLoading ? (
|
|
930
942
|
<CardSkeleton lines={4} />
|
|
@@ -934,155 +946,521 @@ export default function PartnerHubDashboard() {
|
|
|
934
946
|
<div className="bg-[var(--color-dash-info)]/10 rounded-lg p-2">
|
|
935
947
|
<ClockIcon className="h-5 w-5 text-[var(--color-dash-info)]" />
|
|
936
948
|
</div>
|
|
949
|
+
<span className="inline-flex items-center rounded-full px-2 py-0.5 text-xs font-bold bg-[var(--color-dash-success)]/10 text-[var(--color-dash-success)]">
|
|
950
|
+
-18%
|
|
951
|
+
</span>
|
|
937
952
|
</div>
|
|
938
|
-
<p className="text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] text-sm font-semibold mb-2 uppercase tracking-wider">
|
|
939
|
-
<p className="font-black text-[var(--color-dash-text)] dark:text-white mb-1 leading-tight" style={{ fontSize: 'var(--dash-metric-size)' }}>
|
|
940
|
-
<p className="text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]">
|
|
953
|
+
<p className="text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] text-sm font-semibold mb-2 uppercase tracking-wider">Avg Response Time</p>
|
|
954
|
+
<p className="font-black text-[var(--color-dash-text)] dark:text-white mb-1 leading-tight" style={{ fontSize: 'var(--dash-metric-size)' }}>2.4h</p>
|
|
955
|
+
<p className="text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]">across all cases</p>
|
|
941
956
|
</>
|
|
942
957
|
)}
|
|
943
958
|
</div>
|
|
944
959
|
</div>
|
|
945
960
|
|
|
946
|
-
{/*
|
|
947
|
-
<div
|
|
948
|
-
onClick={() => !isLoading && setIsInvoicesModalOpen(true)}
|
|
949
|
-
className={isLoading ? "" : "cursor-pointer"}
|
|
950
|
-
>
|
|
961
|
+
{/* SLA Compliance */}
|
|
962
|
+
<div className="cursor-pointer">
|
|
951
963
|
<div className="bg-white dark:bg-[var(--color-dash-text)] rounded-xl p-6 shadow-sm hover:shadow-lg transition-all duration-300 border border-[var(--color-dash-label)]/20 dark:border-[var(--color-dash-muted)]/30 h-full">
|
|
952
964
|
{isLoading ? (
|
|
953
965
|
<CardSkeleton lines={4} />
|
|
954
966
|
) : (
|
|
955
967
|
<>
|
|
956
968
|
<div className="flex items-center justify-between mb-3">
|
|
957
|
-
<div className="bg-[var(--color-dash-
|
|
958
|
-
<ShieldCheckIcon className="h-5 w-5 text-[var(--color-dash-
|
|
969
|
+
<div className="bg-[var(--color-dash-success)]/10 rounded-lg p-2">
|
|
970
|
+
<ShieldCheckIcon className="h-5 w-5 text-[var(--color-dash-success)]" />
|
|
959
971
|
</div>
|
|
972
|
+
<span className="inline-flex items-center rounded-full px-2 py-0.5 text-xs font-bold bg-[var(--color-dash-success)]/10 text-[var(--color-dash-success)]">
|
|
973
|
+
ON TRACK
|
|
974
|
+
</span>
|
|
960
975
|
</div>
|
|
961
|
-
<p className="text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] text-sm font-semibold mb-2 uppercase tracking-wider">
|
|
962
|
-
<p className="font-black text-[var(--color-dash-text)] dark:text-white mb-1 leading-tight" style={{ fontSize: 'var(--dash-metric-size)' }}>
|
|
963
|
-
<p className="text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]">
|
|
976
|
+
<p className="text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] text-sm font-semibold mb-2 uppercase tracking-wider">SLA Compliance</p>
|
|
977
|
+
<p className="font-black text-[var(--color-dash-text)] dark:text-white mb-1 leading-tight" style={{ fontSize: 'var(--dash-metric-size)' }}>94%</p>
|
|
978
|
+
<p className="text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]">cases resolved on time</p>
|
|
964
979
|
</>
|
|
965
980
|
)}
|
|
966
981
|
</div>
|
|
967
982
|
</div>
|
|
968
983
|
</div>
|
|
969
984
|
|
|
970
|
-
{/*
|
|
971
|
-
|
|
972
|
-
<
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
985
|
+
{/* Tab Navigation */}
|
|
986
|
+
<div className="border-b border-[var(--color-dash-label)]/20 dark:border-[var(--color-dash-muted)]/30">
|
|
987
|
+
<nav className="flex items-center gap-8">
|
|
988
|
+
{([
|
|
989
|
+
{ id: "overview" as TabId, label: "Overview" },
|
|
990
|
+
{ id: "cases" as TabId, label: "Cases", count: 7 },
|
|
991
|
+
{ id: "properties" as TabId, label: "Properties" },
|
|
992
|
+
{ id: "analytics" as TabId, label: "Analytics" },
|
|
993
|
+
]).map((tab) => (
|
|
994
|
+
<button
|
|
995
|
+
key={tab.id}
|
|
996
|
+
onClick={() => switchTab(tab.id)}
|
|
997
|
+
className={`relative pb-3 text-sm font-medium transition-colors whitespace-nowrap ${
|
|
998
|
+
activeTab === tab.id
|
|
999
|
+
? "text-[var(--color-dash-text)] dark:text-white"
|
|
1000
|
+
: "text-[var(--color-dash-label)] hover:text-[var(--color-dash-muted)] dark:hover:text-white/70"
|
|
1001
|
+
}`}
|
|
1002
|
+
>
|
|
1003
|
+
{tab.label}
|
|
1004
|
+
{tab.count ? (
|
|
1005
|
+
<span className="ml-1.5 inline-flex items-center justify-center h-5 min-w-[20px] px-1.5 rounded-full text-[10px] font-bold bg-[var(--color-dash-danger)]/10 text-[var(--color-dash-danger)]">
|
|
1006
|
+
{tab.count}
|
|
1007
|
+
</span>
|
|
1008
|
+
) : null}
|
|
1009
|
+
{activeTab === tab.id && (
|
|
1010
|
+
<span className="absolute bottom-0 left-0 right-0 h-0.5 bg-[var(--color-dash-text)] dark:bg-white rounded-full" />
|
|
1011
|
+
)}
|
|
1012
|
+
</button>
|
|
1013
|
+
))}
|
|
1014
|
+
</nav>
|
|
1015
|
+
</div>
|
|
1016
|
+
|
|
1017
|
+
{/* Tab loading skeletons */}
|
|
1018
|
+
{tabLoading && (
|
|
1019
|
+
<div className="space-y-6 animate-pulse">
|
|
1020
|
+
{activeTab === "overview" && <>
|
|
1021
|
+
<div className="bg-white dark:bg-[var(--color-dash-text)] rounded-xl border border-[var(--color-dash-label)]/20 dark:border-[var(--color-dash-muted)]/30 p-6 space-y-4">
|
|
1022
|
+
<div className="flex items-center gap-2">
|
|
1023
|
+
<div className="h-4 w-4 rounded bg-[var(--color-dash-accent)]/20" />
|
|
1024
|
+
<div className="h-4 w-20 rounded bg-[var(--color-dash-label)]/20" />
|
|
1025
|
+
<div className="h-4 w-16 rounded bg-[var(--color-dash-accent)]/10" />
|
|
1026
|
+
</div>
|
|
1027
|
+
<div className="space-y-4">
|
|
1028
|
+
{[1, 2, 3].map(i => (
|
|
1029
|
+
<div key={i} className="flex gap-4 items-start">
|
|
1030
|
+
<div className="h-8 w-8 rounded-lg bg-[var(--color-dash-label)]/15" />
|
|
1031
|
+
<div className="flex-1 space-y-2">
|
|
1032
|
+
<div className="h-4 w-3/4 rounded bg-[var(--color-dash-label)]/20" />
|
|
1033
|
+
<div className="h-3 w-full rounded bg-[var(--color-dash-label)]/10" />
|
|
1034
|
+
</div>
|
|
1035
|
+
</div>
|
|
1036
|
+
))}
|
|
1037
|
+
</div>
|
|
1038
|
+
</div>
|
|
1039
|
+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
1040
|
+
{[1, 2].map(i => (
|
|
1041
|
+
<div key={i} className="bg-white dark:bg-[var(--color-dash-text)] rounded-2xl p-8 border border-[var(--color-dash-label)]/20 dark:border-[var(--color-dash-muted)]/30 space-y-4">
|
|
1042
|
+
<div className="h-6 w-48 rounded bg-[var(--color-dash-label)]/20" />
|
|
1043
|
+
<div className="h-4 w-64 rounded bg-[var(--color-dash-label)]/10" />
|
|
1044
|
+
{[1, 2, 3, 4].map(j => (
|
|
1045
|
+
<div key={j} className="flex items-center gap-3 p-3 rounded-lg bg-[var(--color-dash-surface)]/50 dark:bg-[var(--color-dash-dark)]/50">
|
|
1046
|
+
<div className="h-6 w-6 rounded-full bg-[var(--color-dash-label)]/20" />
|
|
1047
|
+
<div className="flex-1 space-y-1.5">
|
|
1048
|
+
<div className="h-4 w-40 rounded bg-[var(--color-dash-label)]/20" />
|
|
1049
|
+
<div className="h-3 w-28 rounded bg-[var(--color-dash-label)]/10" />
|
|
1050
|
+
</div>
|
|
1051
|
+
<div className="h-5 w-12 rounded-full bg-[var(--color-dash-label)]/15" />
|
|
1052
|
+
</div>
|
|
1053
|
+
))}
|
|
1054
|
+
</div>
|
|
1055
|
+
))}
|
|
1056
|
+
</div>
|
|
1057
|
+
</>}
|
|
1058
|
+
|
|
1059
|
+
{activeTab === "cases" && <>
|
|
1060
|
+
<div className="bg-gradient-to-r from-[var(--color-dash-dark)] to-[var(--color-dash-dark)]/90 rounded-xl p-5 flex gap-4">
|
|
1061
|
+
<div className="h-9 w-9 rounded-lg bg-white/10" />
|
|
1062
|
+
<div className="flex-1 space-y-2">
|
|
1063
|
+
<div className="h-4 w-2/3 rounded bg-white/15" />
|
|
1064
|
+
<div className="h-3 w-full rounded bg-white/8" />
|
|
1065
|
+
</div>
|
|
1066
|
+
</div>
|
|
1067
|
+
<div className="grid grid-cols-4 gap-4">
|
|
1068
|
+
{[1, 2, 3, 4].map(i => (
|
|
1069
|
+
<div key={i} className="bg-white dark:bg-[var(--color-dash-text)] rounded-xl p-5 border border-[var(--color-dash-label)]/20 dark:border-[var(--color-dash-muted)]/30 space-y-2">
|
|
1070
|
+
<div className="h-3 w-16 rounded bg-[var(--color-dash-label)]/20" />
|
|
1071
|
+
<div className="h-8 w-10 rounded bg-[var(--color-dash-label)]/15" />
|
|
1072
|
+
</div>
|
|
1073
|
+
))}
|
|
1074
|
+
</div>
|
|
1075
|
+
<div className="bg-white dark:bg-[var(--color-dash-text)] rounded-xl border border-[var(--color-dash-label)]/20 dark:border-[var(--color-dash-muted)]/30 overflow-hidden">
|
|
1076
|
+
<div className="p-4 space-y-3 bg-[var(--color-dash-surface)]/50 dark:bg-[var(--color-dash-muted)]/10">
|
|
1077
|
+
<div className="flex gap-4">
|
|
1078
|
+
{[1, 2, 3, 4, 5, 6, 7, 8, 9].map(i => (
|
|
1079
|
+
<div key={i} className="h-3 w-16 rounded bg-[var(--color-dash-label)]/20" />
|
|
1080
|
+
))}
|
|
1081
|
+
</div>
|
|
1082
|
+
</div>
|
|
1083
|
+
{[1, 2, 3, 4, 5, 6].map(i => (
|
|
1084
|
+
<div key={i} className="px-4 py-3 flex gap-4 border-t border-[var(--color-dash-label)]/10 dark:border-[var(--color-dash-muted)]/20">
|
|
1085
|
+
<div className="h-4 w-14 rounded bg-[var(--color-dash-accent)]/15" />
|
|
1086
|
+
<div className="h-4 w-48 rounded bg-[var(--color-dash-label)]/15" />
|
|
1087
|
+
<div className="h-4 w-32 rounded bg-[var(--color-dash-label)]/10" />
|
|
1088
|
+
<div className="h-4 w-20 rounded bg-[var(--color-dash-label)]/10" />
|
|
1089
|
+
<div className="h-4 w-12 rounded bg-[var(--color-dash-label)]/10" />
|
|
1090
|
+
<div className="h-4 w-16 rounded bg-[var(--color-dash-label)]/10" />
|
|
1091
|
+
</div>
|
|
1092
|
+
))}
|
|
1093
|
+
</div>
|
|
1094
|
+
</>}
|
|
1095
|
+
|
|
1096
|
+
{activeTab === "properties" && <>
|
|
1097
|
+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
1098
|
+
{[1, 2, 3, 4].map(i => (
|
|
1099
|
+
<div key={i} className="bg-white dark:bg-[var(--color-dash-text)] rounded-xl border border-[var(--color-dash-label)]/20 dark:border-[var(--color-dash-muted)]/30 p-6 space-y-4">
|
|
1100
|
+
<div className="flex items-start justify-between">
|
|
1101
|
+
<div className="space-y-2">
|
|
1102
|
+
<div className="h-5 w-48 rounded bg-[var(--color-dash-label)]/20" />
|
|
1103
|
+
<div className="h-3 w-32 rounded bg-[var(--color-dash-label)]/10" />
|
|
1104
|
+
</div>
|
|
1105
|
+
<div className="h-6 w-20 rounded-full bg-[var(--color-dash-label)]/15" />
|
|
1106
|
+
</div>
|
|
1107
|
+
<div className="grid grid-cols-3 gap-4">
|
|
1108
|
+
{[1, 2, 3].map(j => (
|
|
1109
|
+
<div key={j} className="space-y-1.5">
|
|
1110
|
+
<div className="h-3 w-16 rounded bg-[var(--color-dash-label)]/15" />
|
|
1111
|
+
<div className="h-6 w-10 rounded bg-[var(--color-dash-label)]/20" />
|
|
1112
|
+
</div>
|
|
1113
|
+
))}
|
|
1114
|
+
</div>
|
|
1115
|
+
<div className="pt-4 border-t border-[var(--color-dash-label)]/10 dark:border-[var(--color-dash-muted)]/20 flex items-center justify-between">
|
|
1116
|
+
<div className="space-y-1.5">
|
|
1117
|
+
<div className="h-3 w-20 rounded bg-[var(--color-dash-label)]/10" />
|
|
1118
|
+
<div className="h-5 w-16 rounded bg-[var(--color-dash-label)]/20" />
|
|
1119
|
+
</div>
|
|
1120
|
+
<div className="h-5 w-12 rounded-full bg-[var(--color-dash-success)]/15" />
|
|
1121
|
+
</div>
|
|
1122
|
+
<div className="bg-[var(--color-dash-surface)]/70 dark:bg-[var(--color-dash-dark)]/50 rounded-lg p-3 flex gap-2">
|
|
1123
|
+
<div className="h-3.5 w-3.5 rounded bg-[var(--color-dash-accent)]/20 flex-shrink-0" />
|
|
1124
|
+
<div className="flex-1 space-y-1.5">
|
|
1125
|
+
<div className="h-3 w-full rounded bg-[var(--color-dash-label)]/10" />
|
|
1126
|
+
<div className="h-3 w-2/3 rounded bg-[var(--color-dash-label)]/8" />
|
|
1127
|
+
</div>
|
|
1128
|
+
</div>
|
|
1129
|
+
</div>
|
|
1130
|
+
))}
|
|
1131
|
+
</div>
|
|
1132
|
+
</>}
|
|
1133
|
+
|
|
1134
|
+
{activeTab === "analytics" && <>
|
|
1135
|
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
1136
|
+
{[1, 2, 3].map(i => (
|
|
1137
|
+
<div key={i} className="bg-white dark:bg-[var(--color-dash-text)] rounded-xl border border-[var(--color-dash-label)]/20 dark:border-[var(--color-dash-muted)]/30 p-5 space-y-3">
|
|
1138
|
+
<div className="h-3 w-24 rounded bg-[var(--color-dash-label)]/15" />
|
|
1139
|
+
<div className="h-8 w-20 rounded bg-[var(--color-dash-label)]/20" />
|
|
1140
|
+
<div className="h-3 w-16 rounded bg-[var(--color-dash-label)]/10" />
|
|
1141
|
+
</div>
|
|
1142
|
+
))}
|
|
1143
|
+
</div>
|
|
1144
|
+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
1145
|
+
{[1, 2].map(i => (
|
|
1146
|
+
<div key={i} className="bg-white dark:bg-[var(--color-dash-text)] rounded-2xl border border-[var(--color-dash-label)]/20 dark:border-[var(--color-dash-muted)]/30 p-8 space-y-4">
|
|
1147
|
+
<div className="h-6 w-48 rounded bg-[var(--color-dash-label)]/20" />
|
|
1148
|
+
<div className="h-4 w-32 rounded bg-[var(--color-dash-label)]/10" />
|
|
1149
|
+
<div className="h-48 w-full rounded-lg bg-[var(--color-dash-surface)]/70 dark:bg-[var(--color-dash-dark)]/40" />
|
|
1150
|
+
</div>
|
|
1151
|
+
))}
|
|
1152
|
+
</div>
|
|
1153
|
+
<div className="bg-white dark:bg-[var(--color-dash-text)] rounded-2xl border border-[var(--color-dash-label)]/20 dark:border-[var(--color-dash-muted)]/30 p-8 space-y-4">
|
|
1154
|
+
<div className="h-6 w-56 rounded bg-[var(--color-dash-label)]/20" />
|
|
1155
|
+
<div className="h-64 w-full rounded-lg bg-[var(--color-dash-surface)]/70 dark:bg-[var(--color-dash-dark)]/40" />
|
|
1156
|
+
</div>
|
|
1157
|
+
</>}
|
|
1158
|
+
|
|
1159
|
+
{/* data360 skeleton placeholder: added by headless-data-360 skill during demo */}
|
|
1160
|
+
</div>
|
|
1161
|
+
)}
|
|
1162
|
+
|
|
1163
|
+
{/* ============== OVERVIEW TAB ============== */}
|
|
1164
|
+
{activeTab === "overview" && !tabLoading && <>
|
|
1165
|
+
|
|
1166
|
+
{/* AI Insights */}
|
|
1167
|
+
{!isLoading && (
|
|
1168
|
+
<div className="bg-white dark:bg-[var(--color-dash-text)] rounded-xl border border-[var(--color-dash-label)]/20 dark:border-[var(--color-dash-muted)]/30 shadow-sm overflow-hidden">
|
|
1169
|
+
<div className="px-6 py-4 border-b border-[var(--color-dash-label)]/10 dark:border-[var(--color-dash-muted)]/20 flex items-center gap-2">
|
|
1170
|
+
<SparklesIcon className="h-4 w-4 text-[var(--color-dash-accent)]" />
|
|
1171
|
+
<h3 className="text-sm font-semibold text-[var(--color-dash-text)] dark:text-white">AI Insights</h3>
|
|
1172
|
+
<span className="text-[10px] font-medium px-1.5 py-0.5 rounded bg-[var(--color-dash-accent)]/10 text-[var(--color-dash-accent)]">Agentforce</span>
|
|
976
1173
|
</div>
|
|
977
|
-
<div className="
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
1174
|
+
<div className="divide-y divide-[var(--color-dash-label)]/10 dark:divide-[var(--color-dash-muted)]/20">
|
|
1175
|
+
{[
|
|
1176
|
+
{
|
|
1177
|
+
action: "Launch promotional rate for 40 open rooms at Austin Convention Center",
|
|
1178
|
+
reason: "TechCorp canceled their May 19-21 block. Based on Engine network demand patterns, a $159/night promo could fill 70% of rooms and recover $12,287 in revenue.",
|
|
1179
|
+
type: "revenue",
|
|
1180
|
+
priority: "high",
|
|
1181
|
+
},
|
|
1182
|
+
{
|
|
1183
|
+
action: "Escalate SF Bay response times before SLA breach",
|
|
1184
|
+
reason: "San Francisco Bay is averaging 3.2h response time, approaching the 4h SLA limit. Two cases are at risk. Reassigning to a dedicated agent could reduce this by 40%.",
|
|
1185
|
+
type: "service",
|
|
1186
|
+
priority: "high",
|
|
1187
|
+
},
|
|
1188
|
+
{
|
|
1189
|
+
action: "Review $2,400 resale credit on ATR-00001",
|
|
1190
|
+
reason: "Your contract specifies a 50% resale credit for resold rooms. 8 rooms were resold but the credit was not applied. This could reduce your penalty from $9,000 to $6,600.",
|
|
1191
|
+
type: "billing",
|
|
1192
|
+
priority: "medium",
|
|
1193
|
+
},
|
|
1194
|
+
].map((insight, i) => (
|
|
1195
|
+
<div key={i} className="px-6 py-4 flex items-start gap-4 hover:bg-[var(--color-dash-surface)]/50 dark:hover:bg-[var(--color-dash-dark)]/30 transition-colors cursor-pointer group">
|
|
1196
|
+
<div className={`flex-shrink-0 mt-0.5 h-8 w-8 rounded-lg flex items-center justify-center ${
|
|
1197
|
+
insight.priority === "high" ? "bg-[var(--color-dash-danger)]/10" : "bg-[var(--color-dash-warning)]/10"
|
|
1198
|
+
}`}>
|
|
1199
|
+
<SparklesIcon className={`h-4 w-4 ${
|
|
1200
|
+
insight.priority === "high" ? "text-[var(--color-dash-danger)]" : "text-[var(--color-dash-warning)]"
|
|
1201
|
+
}`} />
|
|
1202
|
+
</div>
|
|
1203
|
+
<div className="flex-1 min-w-0">
|
|
1204
|
+
<p className="text-sm font-semibold text-[var(--color-dash-text)] dark:text-white mb-1">{insight.action}</p>
|
|
1205
|
+
<p className="text-xs text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] leading-relaxed">{insight.reason}</p>
|
|
1206
|
+
</div>
|
|
1207
|
+
<button className="flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity text-xs font-semibold text-[var(--color-dash-accent)] hover:underline whitespace-nowrap mt-1">
|
|
1208
|
+
Take action
|
|
1209
|
+
</button>
|
|
1210
|
+
</div>
|
|
1211
|
+
))}
|
|
982
1212
|
</div>
|
|
983
1213
|
</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>
|
|
1214
|
+
)}
|
|
1031
1215
|
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1216
|
+
{/* ===== PROPERTY LEADERBOARD + SERVICE PERFORMANCE (side by side) ===== */}
|
|
1217
|
+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 animate-slide-up">
|
|
1218
|
+
{/* Property Leaderboard (compact) */}
|
|
1219
|
+
<div className="bg-white dark:bg-[var(--color-dash-text)] rounded-2xl p-8 shadow-sm border border-[var(--color-dash-label)]/20 dark:border-[var(--color-dash-muted)]/30">
|
|
1220
|
+
{isLoading ? (
|
|
1221
|
+
<div className="space-y-4">
|
|
1222
|
+
<div className="h-8 w-64 bg-[var(--color-dash-label)]/20 rounded animate-pulse"></div>
|
|
1223
|
+
<CardSkeleton lines={5} />
|
|
1224
|
+
</div>
|
|
1225
|
+
) : (
|
|
1226
|
+
<>
|
|
1227
|
+
<div className="flex items-center justify-between mb-6">
|
|
1228
|
+
<div>
|
|
1229
|
+
<h3 className="text-xl font-bold text-[var(--color-dash-text)] dark:text-white">Property Leaderboard</h3>
|
|
1230
|
+
<p className="text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] mt-1">Ranked by service quality & revenue (6 months)</p>
|
|
1231
|
+
</div>
|
|
1232
|
+
</div>
|
|
1233
|
+
<div className="space-y-3">
|
|
1234
|
+
{propertyLeaderboard.map((property, idx) => {
|
|
1235
|
+
const rankLabels = ["1", "2", "3", "4"];
|
|
1236
|
+
return (
|
|
1237
|
+
<div key={idx} className="flex items-center gap-3 p-3 rounded-lg bg-[var(--color-dash-surface)]/50 dark:bg-[var(--color-dash-dark)]/50 hover:bg-[var(--color-dash-surface)] dark:hover:bg-[var(--color-dash-dark)] transition-colors cursor-pointer">
|
|
1238
|
+
<span className="text-sm font-bold flex-shrink-0 w-6 h-6 rounded-full bg-[var(--color-dash-dark)] text-white flex items-center justify-center">{rankLabels[idx]}</span>
|
|
1239
|
+
<div className="flex-1 min-w-0">
|
|
1240
|
+
<p className="font-semibold text-[var(--color-dash-text)] dark:text-white text-sm truncate">{property.name.replace('Summit ', '')}</p>
|
|
1241
|
+
<div className="flex items-center gap-3 text-xs text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] mt-0.5">
|
|
1242
|
+
<span className="inline-flex items-center gap-0.5">
|
|
1243
|
+
<StarIcon className="h-3 w-3 text-[var(--color-dash-accent)]" />
|
|
1244
|
+
<strong className="text-[var(--color-dash-text)] dark:text-white">{property.satisfaction || '4.5'}</strong>
|
|
1051
1245
|
</span>
|
|
1246
|
+
<span>{property.responseTime || '2.4h'} avg</span>
|
|
1247
|
+
<span>${(property.revenue / 1000).toFixed(0)}K rev</span>
|
|
1052
1248
|
</div>
|
|
1053
1249
|
</div>
|
|
1250
|
+
<span className="inline-flex items-center rounded-full px-2 py-0.5 text-xs font-bold bg-[var(--color-dash-success)]/10 text-[var(--color-dash-success)] flex-shrink-0">
|
|
1251
|
+
+{property.growth}%
|
|
1252
|
+
</span>
|
|
1253
|
+
</div>
|
|
1254
|
+
);
|
|
1255
|
+
})}
|
|
1256
|
+
</div>
|
|
1257
|
+
<div className="mt-4 p-3 rounded-lg bg-[var(--color-dash-info)]/5 border border-[var(--color-dash-info)]/20">
|
|
1258
|
+
<p className="text-xs text-[var(--color-dash-text)] dark:text-white">
|
|
1259
|
+
<LightBulbIcon className="h-3.5 w-3.5 inline mr-1 text-[var(--color-dash-info)]" />
|
|
1260
|
+
<strong>Austin</strong> leads with 4.8 satisfaction and fastest response time (1.8h).
|
|
1261
|
+
</p>
|
|
1262
|
+
</div>
|
|
1263
|
+
</>
|
|
1264
|
+
)}
|
|
1265
|
+
</div>
|
|
1054
1266
|
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1267
|
+
{/* Service Performance by Property */}
|
|
1268
|
+
<div className="bg-white dark:bg-[var(--color-dash-text)] rounded-2xl p-8 shadow-sm border border-[var(--color-dash-label)]/20 dark:border-[var(--color-dash-muted)]/30">
|
|
1269
|
+
{isLoading ? (
|
|
1270
|
+
<div className="space-y-4">
|
|
1271
|
+
<div className="h-8 w-64 bg-[var(--color-dash-label)]/20 rounded animate-pulse"></div>
|
|
1272
|
+
<CardSkeleton lines={5} />
|
|
1273
|
+
</div>
|
|
1274
|
+
) : (
|
|
1275
|
+
<>
|
|
1276
|
+
<div className="flex items-center justify-between mb-6">
|
|
1277
|
+
<div>
|
|
1278
|
+
<h3 className="text-xl font-bold text-[var(--color-dash-text)] dark:text-white">Service Performance</h3>
|
|
1279
|
+
<p className="text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] mt-1">Guest satisfaction & response times by property</p>
|
|
1280
|
+
</div>
|
|
1281
|
+
</div>
|
|
1282
|
+
<div className="space-y-4">
|
|
1283
|
+
{[
|
|
1284
|
+
{ name: "Summit Austin Convention Center", satisfaction: 4.8, responseTime: "1.8h", cases: 2, trend: "+0.4" },
|
|
1285
|
+
{ name: "Summit Midtown NYC", satisfaction: 4.7, responseTime: "2.1h", cases: 1, trend: "+0.3" },
|
|
1286
|
+
{ name: "Summit Chicago Downtown", satisfaction: 4.5, responseTime: "2.6h", cases: 1, trend: "+0.2" },
|
|
1287
|
+
{ name: "Summit San Francisco Bay", satisfaction: 4.3, responseTime: "3.2h", cases: 3, trend: "+0.5" },
|
|
1288
|
+
].map((prop) => (
|
|
1289
|
+
<div key={prop.name} className="flex items-center justify-between p-3 rounded-lg bg-[var(--color-dash-surface)]/50 dark:bg-[var(--color-dash-dark)]/50 hover:bg-[var(--color-dash-surface)] dark:hover:bg-[var(--color-dash-dark)] transition-colors cursor-pointer">
|
|
1290
|
+
<div className="flex-1 min-w-0">
|
|
1291
|
+
<p className="font-semibold text-[var(--color-dash-text)] dark:text-white text-sm truncate">{prop.name}</p>
|
|
1292
|
+
<p className="text-xs text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]">{prop.cases} open cases · Avg {prop.responseTime}</p>
|
|
1293
|
+
</div>
|
|
1294
|
+
<div className="flex items-center gap-4 ml-4">
|
|
1295
|
+
<div className="text-right">
|
|
1296
|
+
<div className="flex items-center gap-1">
|
|
1297
|
+
<StarIcon className="h-4 w-4 text-[var(--color-dash-accent)]" />
|
|
1298
|
+
<span className="font-bold text-[var(--color-dash-text)] dark:text-white text-sm">{prop.satisfaction}</span>
|
|
1299
|
+
</div>
|
|
1300
|
+
<span className="text-xs text-[var(--color-dash-success)] font-semibold">{prop.trend}</span>
|
|
1063
1301
|
</div>
|
|
1064
1302
|
</div>
|
|
1065
1303
|
</div>
|
|
1304
|
+
))}
|
|
1305
|
+
</div>
|
|
1306
|
+
<div className="mt-5 p-3 rounded-lg bg-[var(--color-dash-accent)]/5 border border-[var(--color-dash-accent)]/20">
|
|
1307
|
+
<div className="flex items-start gap-2">
|
|
1308
|
+
<LightBulbIcon className="h-5 w-5 text-[var(--color-dash-accent)] flex-shrink-0 mt-0.5" />
|
|
1309
|
+
<p className="text-sm text-[var(--color-dash-text)] dark:text-white">
|
|
1310
|
+
<span className="font-semibold">Key Insight:</span> Austin leads in guest satisfaction at 4.8 with the fastest response time (1.8h). SF Bay improved the most (+0.5) but still has room to reduce response times.
|
|
1311
|
+
</p>
|
|
1066
1312
|
</div>
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1313
|
+
</div>
|
|
1314
|
+
</>
|
|
1315
|
+
)}
|
|
1316
|
+
</div>
|
|
1317
|
+
</div>
|
|
1070
1318
|
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1319
|
+
{/* Open Cases by Category */}
|
|
1320
|
+
<div className="bg-white dark:bg-[var(--color-dash-text)] rounded-2xl p-8 shadow-sm border border-[var(--color-dash-label)]/20 dark:border-[var(--color-dash-muted)]/30 animate-slide-up">
|
|
1321
|
+
{isLoading ? (
|
|
1322
|
+
<div className="space-y-4">
|
|
1323
|
+
<div className="h-8 w-64 bg-[var(--color-dash-label)]/20 rounded animate-pulse"></div>
|
|
1324
|
+
<CardSkeleton lines={5} />
|
|
1325
|
+
</div>
|
|
1326
|
+
) : (
|
|
1327
|
+
<>
|
|
1328
|
+
<div className="flex items-center justify-between mb-6">
|
|
1329
|
+
<div>
|
|
1330
|
+
<h3 className="text-xl font-bold text-[var(--color-dash-text)] dark:text-white">Open Cases by Category</h3>
|
|
1331
|
+
<p className="text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] mt-1">7 open cases across your properties</p>
|
|
1076
1332
|
</div>
|
|
1077
|
-
<
|
|
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>
|
|
1333
|
+
<button className="text-sm font-semibold text-[var(--color-dash-accent)] hover:underline">View All</button>
|
|
1081
1334
|
</div>
|
|
1335
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
1336
|
+
{[
|
|
1337
|
+
{ category: "Billing Disputes", count: 3, color: "var(--color-dash-warning)", urgent: true },
|
|
1338
|
+
{ category: "Guest Complaints", count: 2, color: "var(--color-dash-danger)", urgent: true },
|
|
1339
|
+
{ category: "Maintenance Requests", count: 1, color: "var(--color-dash-info)", urgent: false },
|
|
1340
|
+
{ category: "Booking Modifications", count: 1, color: "var(--color-dash-accent)", urgent: false },
|
|
1341
|
+
].map((item) => (
|
|
1342
|
+
<div key={item.category} className="flex items-center gap-3 p-4 rounded-xl bg-[var(--color-dash-surface)]/50 dark:bg-[var(--color-dash-dark)]/50 hover:bg-[var(--color-dash-surface)] dark:hover:bg-[var(--color-dash-dark)] transition-colors cursor-pointer border border-[var(--color-dash-label)]/10 dark:border-[var(--color-dash-muted)]/20">
|
|
1343
|
+
<div className="w-1.5 h-10 rounded-full flex-shrink-0" style={{ backgroundColor: item.color }} />
|
|
1344
|
+
<div className="flex-1">
|
|
1345
|
+
<p className="font-semibold text-[var(--color-dash-text)] dark:text-white text-sm">{item.category}</p>
|
|
1346
|
+
<div className="flex items-center gap-2 mt-0.5">
|
|
1347
|
+
<span className="text-lg font-bold text-[var(--color-dash-text)] dark:text-white">{item.count}</span>
|
|
1348
|
+
{item.urgent && (
|
|
1349
|
+
<span className="text-[10px] font-bold px-1.5 py-0.5 rounded-full bg-[var(--color-dash-danger)]/10 text-[var(--color-dash-danger)]">URGENT</span>
|
|
1350
|
+
)}
|
|
1351
|
+
</div>
|
|
1352
|
+
</div>
|
|
1353
|
+
</div>
|
|
1354
|
+
))}
|
|
1355
|
+
</div>
|
|
1356
|
+
</>
|
|
1357
|
+
)}
|
|
1358
|
+
</div>
|
|
1359
|
+
|
|
1360
|
+
{/* ===== SERVICE ACTIVITY (moved up for service-first layout) ===== */}
|
|
1361
|
+
<div className="pt-4 space-y-2">
|
|
1362
|
+
<h2 className="text-3xl font-bold text-[var(--color-dash-text)] dark:text-white tracking-tight">
|
|
1363
|
+
Service activity
|
|
1364
|
+
</h2>
|
|
1365
|
+
<p className="text-lg text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]">
|
|
1366
|
+
Recent cases, guest feedback, and service updates across your properties
|
|
1367
|
+
</p>
|
|
1368
|
+
</div>
|
|
1369
|
+
|
|
1370
|
+
{/* Action Items Grid (moved up) */}
|
|
1371
|
+
{isLoading ? (
|
|
1372
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
|
|
1373
|
+
<CardSkeleton lines={4} />
|
|
1374
|
+
<CardSkeleton lines={4} />
|
|
1375
|
+
</div>
|
|
1376
|
+
) : myDisputes.length > 0 ? (
|
|
1377
|
+
<div className="space-y-5">
|
|
1378
|
+
<h3 className="text-xl font-bold text-[var(--color-dash-text)] dark:text-white">
|
|
1379
|
+
Open cases & items needing attention
|
|
1380
|
+
</h3>
|
|
1381
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
|
|
1382
|
+
{myDisputes.map((d) => (
|
|
1383
|
+
<div
|
|
1384
|
+
key={d.id}
|
|
1385
|
+
onClick={() => {
|
|
1386
|
+
toast.info(`Opening ${d.title}`);
|
|
1387
|
+
setSelectedDispute(d);
|
|
1388
|
+
}}
|
|
1389
|
+
className="group bg-white dark:bg-[var(--color-dash-text)] border border-[var(--color-dash-label)]/20 dark:border-[var(--color-dash-muted)]/30 rounded-xl p-6 hover:border-[var(--color-dash-accent)] dark:hover:border-[var(--color-dash-accent)] transition-all duration-300 hover:shadow-lg cursor-pointer"
|
|
1390
|
+
>
|
|
1391
|
+
<div className="flex items-start justify-between gap-3 mb-3">
|
|
1392
|
+
<h4 className="font-semibold text-[var(--color-dash-text)] dark:text-white flex-1">
|
|
1393
|
+
{d.title}
|
|
1394
|
+
</h4>
|
|
1395
|
+
<span
|
|
1396
|
+
className={`inline-flex items-center rounded-full px-2 py-1 text-xs font-medium flex-shrink-0 ${
|
|
1397
|
+
d.status === "critical"
|
|
1398
|
+
? "bg-[var(--color-dash-danger)]/10 text-[var(--color-dash-danger)] border border-[var(--color-dash-danger)]/30"
|
|
1399
|
+
: d.status === "warning"
|
|
1400
|
+
? "bg-[var(--color-dash-warning)]/10 text-[var(--color-dash-warning)] border border-[var(--color-dash-warning)]/30"
|
|
1401
|
+
: "bg-[var(--color-dash-info)]/10 text-[var(--color-dash-info)] border border-[var(--color-dash-info)]/30"
|
|
1402
|
+
}`}
|
|
1403
|
+
>
|
|
1404
|
+
{d.badge}
|
|
1405
|
+
</span>
|
|
1406
|
+
</div>
|
|
1407
|
+
<p className="text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] mb-4">
|
|
1408
|
+
{d.description}
|
|
1409
|
+
</p>
|
|
1410
|
+
<div className="flex items-center justify-between">
|
|
1411
|
+
<span className="text-lg font-bold text-[var(--color-dash-text)] dark:text-white">
|
|
1412
|
+
${d.amount.toLocaleString()}
|
|
1413
|
+
</span>
|
|
1414
|
+
{d.agentHandled && (
|
|
1415
|
+
<span className="text-xs text-[var(--color-dash-accent)] flex items-center gap-1">
|
|
1416
|
+
<svg className="h-3 w-3" fill="currentColor" viewBox="0 0 20 20">
|
|
1417
|
+
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
|
1418
|
+
</svg>
|
|
1419
|
+
Agent reviewed
|
|
1420
|
+
</span>
|
|
1421
|
+
)}
|
|
1422
|
+
</div>
|
|
1423
|
+
</div>
|
|
1424
|
+
))}
|
|
1082
1425
|
</div>
|
|
1083
1426
|
</div>
|
|
1427
|
+
) : null}
|
|
1428
|
+
|
|
1429
|
+
{/* Service Timeline (moved up) */}
|
|
1430
|
+
<div className="pt-4 space-y-2">
|
|
1431
|
+
<h3 className="text-xl font-bold text-[var(--color-dash-text)] dark:text-white">
|
|
1432
|
+
Service timeline
|
|
1433
|
+
</h3>
|
|
1434
|
+
<p className="text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]">
|
|
1435
|
+
Cases, guest feedback, and updates across your properties
|
|
1436
|
+
</p>
|
|
1437
|
+
</div>
|
|
1438
|
+
|
|
1439
|
+
{/* Activity Feed (moved up) */}
|
|
1440
|
+
{isLoading ? (
|
|
1441
|
+
<CardSkeleton lines={6} />
|
|
1442
|
+
) : (
|
|
1443
|
+
<ActivityCard
|
|
1444
|
+
title="Recent updates"
|
|
1445
|
+
actions={myActivity.map((a) => ({
|
|
1446
|
+
id: a.id,
|
|
1447
|
+
status:
|
|
1448
|
+
a.status === "alert"
|
|
1449
|
+
? "error"
|
|
1450
|
+
: a.status === "warning"
|
|
1451
|
+
? "pending"
|
|
1452
|
+
: a.status === "success"
|
|
1453
|
+
? "complete"
|
|
1454
|
+
: "working",
|
|
1455
|
+
title: a.title,
|
|
1456
|
+
subtitle: a.description,
|
|
1457
|
+
timestamp: a.timestamp,
|
|
1458
|
+
}))}
|
|
1459
|
+
/>
|
|
1084
1460
|
)}
|
|
1085
1461
|
|
|
1462
|
+
{/* ===== BILLING & OPERATIONS (moved down) ===== */}
|
|
1463
|
+
|
|
1086
1464
|
{/* Penalty Calculation Issue Card */}
|
|
1087
1465
|
{!isLoading && myPenalties.filter((p: any) => p.isHero).length > 0 && (
|
|
1088
1466
|
<div className="bg-white dark:bg-[var(--color-dash-text)] border-2 border-[var(--color-dash-warning)]/50 rounded-2xl p-8 shadow-lg hover:shadow-xl transition-shadow duration-300">
|
|
@@ -1099,7 +1477,7 @@ export default function PartnerHubDashboard() {
|
|
|
1099
1477
|
Penalty Calculation Issue
|
|
1100
1478
|
</h3>
|
|
1101
1479
|
<p className="text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]">
|
|
1102
|
-
ATR-00001
|
|
1480
|
+
ATR-00001 / Summit Austin Convention Center / TechCorp Inc. booking
|
|
1103
1481
|
</p>
|
|
1104
1482
|
</div>
|
|
1105
1483
|
<span className="inline-flex items-center rounded-full px-3 py-1 text-xs font-bold bg-[var(--color-dash-warning)] text-white flex-shrink-0">
|
|
@@ -1150,7 +1528,7 @@ export default function PartnerHubDashboard() {
|
|
|
1150
1528
|
</div>
|
|
1151
1529
|
)}
|
|
1152
1530
|
|
|
1153
|
-
{/* Section
|
|
1531
|
+
{/* Section - Billing & Contract Details */}
|
|
1154
1532
|
<div className="pt-8 space-y-2">
|
|
1155
1533
|
<h2 className="text-3xl font-bold text-[var(--color-dash-text)] dark:text-white tracking-tight">
|
|
1156
1534
|
Billing & contract details
|
|
@@ -1204,7 +1582,7 @@ export default function PartnerHubDashboard() {
|
|
|
1204
1582
|
Total revenue growth
|
|
1205
1583
|
</h3>
|
|
1206
1584
|
<p className="text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]">
|
|
1207
|
-
Combined monthly revenue
|
|
1585
|
+
Combined monthly revenue, up 73% over 6 months
|
|
1208
1586
|
</p>
|
|
1209
1587
|
</div>
|
|
1210
1588
|
<div className="p-4 w-full">
|
|
@@ -1245,7 +1623,7 @@ export default function PartnerHubDashboard() {
|
|
|
1245
1623
|
dense={false}
|
|
1246
1624
|
divided={true}
|
|
1247
1625
|
onItemClick={(item) => {
|
|
1248
|
-
toast.info(`Opening invoice ${item.title.split('
|
|
1626
|
+
toast.info(`Opening invoice ${item.title.split(' - ')[0]}`);
|
|
1249
1627
|
}}
|
|
1250
1628
|
emptyMessage="No invoices right now."
|
|
1251
1629
|
loading={isLoading}
|
|
@@ -1307,210 +1685,6 @@ export default function PartnerHubDashboard() {
|
|
|
1307
1685
|
</div>
|
|
1308
1686
|
)}
|
|
1309
1687
|
|
|
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
1688
|
{/* Help Section - Enhanced */}
|
|
1515
1689
|
<div className="bg-gradient-to-br from-white to-[var(--color-dash-surface)] dark:from-[var(--color-dash-text)] dark:to-[var(--color-dash-dark)] rounded-2xl p-12 border border-[var(--color-dash-label)]/20 dark:border-[var(--color-dash-muted)]/30 shadow-lg">
|
|
1516
1690
|
<div className="text-center max-w-2xl mx-auto space-y-6">
|
|
@@ -1518,7 +1692,7 @@ export default function PartnerHubDashboard() {
|
|
|
1518
1692
|
Questions? We're here to help
|
|
1519
1693
|
</h3>
|
|
1520
1694
|
<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
|
|
1695
|
+
Our partner team is standing by if you need anything, from service cases to billing questions.
|
|
1522
1696
|
</p>
|
|
1523
1697
|
<div className="flex gap-4 justify-center flex-wrap pt-2">
|
|
1524
1698
|
<Modal>
|
|
@@ -1605,6 +1779,515 @@ export default function PartnerHubDashboard() {
|
|
|
1605
1779
|
</div>
|
|
1606
1780
|
</div>
|
|
1607
1781
|
|
|
1782
|
+
</>}
|
|
1783
|
+
|
|
1784
|
+
{/* ============== CASES TAB ============== */}
|
|
1785
|
+
{activeTab === "cases" && !tabLoading && (
|
|
1786
|
+
<div className="space-y-6 animate-fade-in">
|
|
1787
|
+
<div className="flex items-center justify-between">
|
|
1788
|
+
<div>
|
|
1789
|
+
<h2 className="text-2xl font-bold text-[var(--color-dash-text)] dark:text-white">Service Cases</h2>
|
|
1790
|
+
<p className="text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] mt-1">All open and recent cases across your properties</p>
|
|
1791
|
+
</div>
|
|
1792
|
+
<div className="flex items-center gap-3">
|
|
1793
|
+
<select className="bg-[var(--color-dash-surface)] dark:bg-[var(--color-dash-muted)]/10 border border-[var(--color-dash-label)]/20 dark:border-[var(--color-dash-muted)]/30 rounded-lg px-3 py-2 text-sm text-[var(--color-dash-text)] dark:text-white">
|
|
1794
|
+
<option>All Properties</option>
|
|
1795
|
+
<option>Austin Convention Center</option>
|
|
1796
|
+
<option>Midtown NYC</option>
|
|
1797
|
+
<option>Chicago Downtown</option>
|
|
1798
|
+
<option>San Francisco Bay</option>
|
|
1799
|
+
</select>
|
|
1800
|
+
<select className="bg-[var(--color-dash-surface)] dark:bg-[var(--color-dash-muted)]/10 border border-[var(--color-dash-label)]/20 dark:border-[var(--color-dash-muted)]/30 rounded-lg px-3 py-2 text-sm text-[var(--color-dash-text)] dark:text-white">
|
|
1801
|
+
<option>All Statuses</option>
|
|
1802
|
+
<option>Open</option>
|
|
1803
|
+
<option>In Progress</option>
|
|
1804
|
+
<option>Escalated</option>
|
|
1805
|
+
<option>Resolved</option>
|
|
1806
|
+
</select>
|
|
1807
|
+
</div>
|
|
1808
|
+
</div>
|
|
1809
|
+
|
|
1810
|
+
{/* Cases Summary Cards */}
|
|
1811
|
+
<div className="grid grid-cols-1 sm:grid-cols-4 gap-4">
|
|
1812
|
+
{[
|
|
1813
|
+
{ label: "Open", count: 3, color: "var(--color-dash-warning)", bg: "var(--color-dash-warning)" },
|
|
1814
|
+
{ label: "In Progress", count: 2, color: "var(--color-dash-info)", bg: "var(--color-dash-info)" },
|
|
1815
|
+
{ label: "Escalated", count: 2, color: "var(--color-dash-danger)", bg: "var(--color-dash-danger)" },
|
|
1816
|
+
{ label: "Resolved (30d)", count: 22, color: "var(--color-dash-success)", bg: "var(--color-dash-success)" },
|
|
1817
|
+
].map((s) => (
|
|
1818
|
+
<div key={s.label} className="bg-white dark:bg-[var(--color-dash-text)] rounded-xl p-5 border border-[var(--color-dash-label)]/20 dark:border-[var(--color-dash-muted)]/30 shadow-sm">
|
|
1819
|
+
<p className="text-xs font-semibold uppercase tracking-wider mb-1" style={{ color: s.color }}>{s.label}</p>
|
|
1820
|
+
<p className="text-3xl font-black text-[var(--color-dash-text)] dark:text-white">{s.count}</p>
|
|
1821
|
+
</div>
|
|
1822
|
+
))}
|
|
1823
|
+
</div>
|
|
1824
|
+
|
|
1825
|
+
{/* Cases Data Table */}
|
|
1826
|
+
<div className="bg-white dark:bg-[var(--color-dash-text)] border border-[var(--color-dash-label)]/20 dark:border-[var(--color-dash-muted)]/30 rounded-xl overflow-hidden shadow-sm">
|
|
1827
|
+
<div className="overflow-x-auto">
|
|
1828
|
+
<table className="w-full table-fixed">
|
|
1829
|
+
<thead className="bg-[var(--color-dash-surface)] dark:bg-[var(--color-dash-muted)]/10 border-b border-[var(--color-dash-label)]/20 dark:border-[var(--color-dash-muted)]/30">
|
|
1830
|
+
<tr>
|
|
1831
|
+
<th className="w-[88px] px-4 py-3 text-left text-xs font-semibold text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] uppercase tracking-wider">Case #</th>
|
|
1832
|
+
<th className="px-4 py-3 text-left text-xs font-semibold text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] uppercase tracking-wider">Subject</th>
|
|
1833
|
+
<th className="w-[160px] px-4 py-3 text-left text-xs font-semibold text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] uppercase tracking-wider">Property</th>
|
|
1834
|
+
<th className="w-[120px] px-4 py-3 text-left text-xs font-semibold text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] uppercase tracking-wider">Category</th>
|
|
1835
|
+
<th className="w-[80px] px-4 py-3 text-left text-xs font-semibold text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] uppercase tracking-wider">Priority</th>
|
|
1836
|
+
<th className="w-[100px] px-4 py-3 text-left text-xs font-semibold text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] uppercase tracking-wider">Status</th>
|
|
1837
|
+
<th className="w-[110px] px-4 py-3 text-left text-xs font-semibold text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] uppercase tracking-wider">Assigned To</th>
|
|
1838
|
+
<th className="w-[72px] px-4 py-3 text-left text-xs font-semibold text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] uppercase tracking-wider">SLA</th>
|
|
1839
|
+
</tr>
|
|
1840
|
+
</thead>
|
|
1841
|
+
<tbody className="divide-y divide-[var(--color-dash-label)]/10 dark:divide-[var(--color-dash-muted)]/20">
|
|
1842
|
+
{[
|
|
1843
|
+
{ id: "CS-1042", subject: "Noise complaint, room 412 adjacent to event space", property: "Austin Conv. Ctr", category: "Guest Complaint", priority: "High", status: "Resolved", assignedTo: "Sarah Chen", sla: "Met" },
|
|
1844
|
+
{ id: "CS-1041", subject: "HVAC unit failure in conference room B", property: "SF Bay", category: "Maintenance", priority: "Medium", status: "In Progress", assignedTo: "Mike Torres", sla: "On Track" },
|
|
1845
|
+
{ id: "CS-1040", subject: "Billing dispute, missing resale credit ATR-00001", property: "Austin Conv. Ctr", category: "Billing", priority: "High", status: "Open", assignedTo: "Agent", sla: "At Risk" },
|
|
1846
|
+
{ id: "CS-1039", subject: "Room cleanliness, repeat complaint, 3rd occurrence", property: "Chicago Dtwn", category: "Guest Complaint", priority: "High", status: "Escalated", assignedTo: "Regional Mgr", sla: "Breached" },
|
|
1847
|
+
{ id: "CS-1038", subject: "Late checkout request for VIP guest, corporate account", property: "SF Bay", category: "Guest Request", priority: "Low", status: "Open", assignedTo: "Front Desk", sla: "On Track" },
|
|
1848
|
+
{ id: "CS-1037", subject: "Commission rate discrepancy on March invoice", property: "Midtown NYC", category: "Billing", priority: "Medium", status: "Open", assignedTo: "Agent", sla: "On Track" },
|
|
1849
|
+
{ id: "CS-1036", subject: "Group booking modification, TechCorp adding 12 rooms", property: "Austin Conv. Ctr", category: "Booking", priority: "Medium", status: "Resolved", assignedTo: "Reservations", sla: "Met" },
|
|
1850
|
+
{ id: "CS-1035", subject: "Pool area maintenance, heater malfunction", property: "SF Bay", category: "Maintenance", priority: "Low", status: "In Progress", assignedTo: "Facilities", sla: "On Track" },
|
|
1851
|
+
{ id: "CS-1034", subject: "Penalty calculation review, revenue-based method request", property: "Austin Conv. Ctr", category: "Billing", priority: "High", status: "Escalated", assignedTo: "Partner Mgr", sla: "Breached" },
|
|
1852
|
+
{ id: "CS-1033", subject: "Positive guest feedback, front desk team recognition", property: "Midtown NYC", category: "Guest Feedback", priority: "Low", status: "Resolved", assignedTo: "HR", sla: "Met" },
|
|
1853
|
+
].map((c) => (
|
|
1854
|
+
<tr key={c.id} className="hover:bg-[var(--color-dash-surface)]/50 dark:hover:bg-[var(--color-dash-muted)]/5 transition-colors cursor-pointer">
|
|
1855
|
+
<td className="px-4 py-3 text-sm font-medium text-[var(--color-dash-accent)] whitespace-nowrap">{c.id}</td>
|
|
1856
|
+
<td className="px-4 py-3 text-sm text-[var(--color-dash-text)] dark:text-white leading-snug">{c.subject}</td>
|
|
1857
|
+
<td className="px-4 py-3 text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]">{c.property}</td>
|
|
1858
|
+
<td className="px-4 py-3">
|
|
1859
|
+
<span className={`inline-flex whitespace-nowrap text-xs font-medium px-2 py-0.5 rounded-full ${
|
|
1860
|
+
c.category === "Guest Complaint" ? "bg-[var(--color-dash-danger)]/10 text-[var(--color-dash-danger)]" :
|
|
1861
|
+
c.category === "Billing" ? "bg-[var(--color-dash-warning)]/10 text-[var(--color-dash-warning)]" :
|
|
1862
|
+
c.category === "Maintenance" ? "bg-[var(--color-dash-info)]/10 text-[var(--color-dash-info)]" :
|
|
1863
|
+
"bg-[var(--color-dash-label)]/10 text-[var(--color-dash-muted)]"
|
|
1864
|
+
}`}>{c.category}</span>
|
|
1865
|
+
</td>
|
|
1866
|
+
<td className="px-4 py-3">
|
|
1867
|
+
<span className={`text-xs font-bold whitespace-nowrap ${
|
|
1868
|
+
c.priority === "High" ? "text-[var(--color-dash-danger)]" :
|
|
1869
|
+
c.priority === "Medium" ? "text-[var(--color-dash-warning)]" :
|
|
1870
|
+
"text-[var(--color-dash-label)]"
|
|
1871
|
+
}`}>{c.priority}</span>
|
|
1872
|
+
</td>
|
|
1873
|
+
<td className="px-4 py-3">
|
|
1874
|
+
<span className={`inline-flex whitespace-nowrap items-center rounded-full px-2 py-0.5 text-xs font-medium ${
|
|
1875
|
+
c.status === "Open" ? "bg-[var(--color-dash-warning)]/10 text-[var(--color-dash-warning)] border border-[var(--color-dash-warning)]/30" :
|
|
1876
|
+
c.status === "In Progress" ? "bg-[var(--color-dash-info)]/10 text-[var(--color-dash-info)] border border-[var(--color-dash-info)]/30" :
|
|
1877
|
+
c.status === "Escalated" ? "bg-[var(--color-dash-danger)]/10 text-[var(--color-dash-danger)] border border-[var(--color-dash-danger)]/30" :
|
|
1878
|
+
"bg-[var(--color-dash-success)]/10 text-[var(--color-dash-success)] border border-[var(--color-dash-success)]/30"
|
|
1879
|
+
}`}>{c.status}</span>
|
|
1880
|
+
</td>
|
|
1881
|
+
<td className="px-4 py-3 text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] whitespace-nowrap">{c.assignedTo}</td>
|
|
1882
|
+
<td className="px-4 py-3">
|
|
1883
|
+
<span className={`text-xs font-semibold whitespace-nowrap ${
|
|
1884
|
+
c.sla === "Met" ? "text-[var(--color-dash-success)]" :
|
|
1885
|
+
c.sla === "On Track" ? "text-[var(--color-dash-info)]" :
|
|
1886
|
+
c.sla === "At Risk" ? "text-[var(--color-dash-warning)]" :
|
|
1887
|
+
"text-[var(--color-dash-danger)]"
|
|
1888
|
+
}`}>{c.sla}</span>
|
|
1889
|
+
</td>
|
|
1890
|
+
</tr>
|
|
1891
|
+
))}
|
|
1892
|
+
</tbody>
|
|
1893
|
+
</table>
|
|
1894
|
+
</div>
|
|
1895
|
+
<div className="px-6 py-4 border-t border-[var(--color-dash-label)]/10 dark:border-[var(--color-dash-muted)]/20 flex items-center justify-between">
|
|
1896
|
+
<p className="text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]">Showing 10 of 30 cases</p>
|
|
1897
|
+
<div className="flex items-center gap-2">
|
|
1898
|
+
<button className="px-3 py-1.5 text-sm rounded-lg border border-[var(--color-dash-label)]/20 dark:border-[var(--color-dash-muted)]/30 text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] hover:bg-[var(--color-dash-surface)] dark:hover:bg-[var(--color-dash-muted)]/10 transition-colors">Previous</button>
|
|
1899
|
+
<span className="px-3 py-1.5 text-sm rounded-lg bg-[var(--color-dash-dark)] text-white font-semibold">1</span>
|
|
1900
|
+
<button className="px-3 py-1.5 text-sm rounded-lg border border-[var(--color-dash-label)]/20 dark:border-[var(--color-dash-muted)]/30 text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] hover:bg-[var(--color-dash-surface)] dark:hover:bg-[var(--color-dash-muted)]/10 transition-colors">2</button>
|
|
1901
|
+
<button className="px-3 py-1.5 text-sm rounded-lg border border-[var(--color-dash-label)]/20 dark:border-[var(--color-dash-muted)]/30 text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] hover:bg-[var(--color-dash-surface)] dark:hover:bg-[var(--color-dash-muted)]/10 transition-colors">Next</button>
|
|
1902
|
+
</div>
|
|
1903
|
+
</div>
|
|
1904
|
+
</div>
|
|
1905
|
+
</div>
|
|
1906
|
+
)}
|
|
1907
|
+
|
|
1908
|
+
{/* ============== PROPERTIES TAB ============== */}
|
|
1909
|
+
{activeTab === "properties" && !tabLoading && (
|
|
1910
|
+
<div className="space-y-6 animate-fade-in">
|
|
1911
|
+
<div className="flex items-center justify-between">
|
|
1912
|
+
<div>
|
|
1913
|
+
<h2 className="text-2xl font-bold text-[var(--color-dash-text)] dark:text-white">Your Properties</h2>
|
|
1914
|
+
<p className="text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] mt-1">Manage and monitor all your properties in the Engine network</p>
|
|
1915
|
+
</div>
|
|
1916
|
+
</div>
|
|
1917
|
+
|
|
1918
|
+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
1919
|
+
{[
|
|
1920
|
+
{ name: "Summit Austin Convention Center", city: "Austin, TX", rooms: 320, satisfaction: 4.8, responseTime: "1.8h", openCases: 2, revenue: "$97,600", growth: "+60%", status: "Excellent", aiRec: "40-room cancellation creates promo opportunity. Launch Engine network promotion to recover up to $12,287 in revenue." },
|
|
1921
|
+
{ name: "Summit Midtown NYC", city: "New York, NY", rooms: 280, satisfaction: 4.7, responseTime: "2.1h", openCases: 1, revenue: "$73,100", growth: "+55%", status: "Good", aiRec: "Strong performance. Consider requesting a rate increase for Q3 based on 55% growth trajectory and high guest satisfaction." },
|
|
1922
|
+
{ name: "Summit Chicago Downtown", city: "Chicago, IL", rooms: 240, satisfaction: 4.5, responseTime: "2.6h", openCases: 1, revenue: "$59,000", growth: "+75%", status: "Good", aiRec: "Satisfaction trending down (4.7 to 4.5). Housekeeping audit recommended based on repeat cleanliness complaint." },
|
|
1923
|
+
{ name: "Summit San Francisco Bay", city: "San Francisco, CA", rooms: 200, satisfaction: 4.3, responseTime: "3.2h", openCases: 3, revenue: "$53,300", growth: "+100%", status: "Needs Attention", aiRec: "Response time (3.2h) approaching SLA limit. Assign dedicated service agent to prevent breaches and protect 100% growth momentum." },
|
|
1924
|
+
].map((prop) => (
|
|
1925
|
+
<div key={prop.name} className="bg-white dark:bg-[var(--color-dash-text)] rounded-xl border border-[var(--color-dash-label)]/20 dark:border-[var(--color-dash-muted)]/30 shadow-sm hover:shadow-lg transition-all duration-300 overflow-hidden cursor-pointer">
|
|
1926
|
+
<div className="p-6">
|
|
1927
|
+
<div className="flex items-start justify-between mb-4">
|
|
1928
|
+
<div>
|
|
1929
|
+
<h3 className="text-lg font-bold text-[var(--color-dash-text)] dark:text-white">{prop.name}</h3>
|
|
1930
|
+
<p className="text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]">{prop.city} / {prop.rooms} rooms</p>
|
|
1931
|
+
</div>
|
|
1932
|
+
<span className={`inline-flex items-center rounded-full px-2.5 py-1 text-xs font-medium ${
|
|
1933
|
+
prop.status === "Excellent" ? "bg-[var(--color-dash-success)]/10 text-[var(--color-dash-success)] border border-[var(--color-dash-success)]/30" :
|
|
1934
|
+
prop.status === "Good" ? "bg-[var(--color-dash-info)]/10 text-[var(--color-dash-info)] border border-[var(--color-dash-info)]/30" :
|
|
1935
|
+
"bg-[var(--color-dash-warning)]/10 text-[var(--color-dash-warning)] border border-[var(--color-dash-warning)]/30"
|
|
1936
|
+
}`}>{prop.status}</span>
|
|
1937
|
+
</div>
|
|
1938
|
+
|
|
1939
|
+
<div className="grid grid-cols-3 gap-4 mb-4">
|
|
1940
|
+
<div>
|
|
1941
|
+
<p className="text-xs text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] uppercase tracking-wider mb-1">Satisfaction</p>
|
|
1942
|
+
<div className="flex items-center gap-1">
|
|
1943
|
+
<StarIcon className="h-4 w-4 text-[var(--color-dash-accent)]" />
|
|
1944
|
+
<span className="text-xl font-bold text-[var(--color-dash-text)] dark:text-white">{prop.satisfaction}</span>
|
|
1945
|
+
</div>
|
|
1946
|
+
</div>
|
|
1947
|
+
<div>
|
|
1948
|
+
<p className="text-xs text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] uppercase tracking-wider mb-1">Avg Response</p>
|
|
1949
|
+
<span className="text-xl font-bold text-[var(--color-dash-text)] dark:text-white">{prop.responseTime}</span>
|
|
1950
|
+
</div>
|
|
1951
|
+
<div>
|
|
1952
|
+
<p className="text-xs text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] uppercase tracking-wider mb-1">Open Cases</p>
|
|
1953
|
+
<span className="text-xl font-bold text-[var(--color-dash-text)] dark:text-white">{prop.openCases}</span>
|
|
1954
|
+
</div>
|
|
1955
|
+
</div>
|
|
1956
|
+
|
|
1957
|
+
<div className="flex items-center justify-between pt-4 border-t border-[var(--color-dash-label)]/10 dark:border-[var(--color-dash-muted)]/20 mb-4">
|
|
1958
|
+
<div>
|
|
1959
|
+
<p className="text-xs text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]">Revenue (6 mo)</p>
|
|
1960
|
+
<p className="text-lg font-bold text-[var(--color-dash-text)] dark:text-white">{prop.revenue}</p>
|
|
1961
|
+
</div>
|
|
1962
|
+
<span className="inline-flex items-center rounded-full px-2 py-0.5 text-xs font-bold bg-[var(--color-dash-success)]/10 text-[var(--color-dash-success)]">
|
|
1963
|
+
{prop.growth}
|
|
1964
|
+
</span>
|
|
1965
|
+
</div>
|
|
1966
|
+
|
|
1967
|
+
{/* AI Recommendation */}
|
|
1968
|
+
<div className="bg-[var(--color-dash-surface)]/70 dark:bg-[var(--color-dash-dark)]/50 rounded-lg p-3 flex items-start gap-2.5">
|
|
1969
|
+
<SparklesIcon className="h-3.5 w-3.5 text-[var(--color-dash-accent)] flex-shrink-0 mt-0.5" />
|
|
1970
|
+
<p className="text-xs text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] leading-relaxed">{prop.aiRec}</p>
|
|
1971
|
+
</div>
|
|
1972
|
+
</div>
|
|
1973
|
+
</div>
|
|
1974
|
+
))}
|
|
1975
|
+
</div>
|
|
1976
|
+
</div>
|
|
1977
|
+
)}
|
|
1978
|
+
|
|
1979
|
+
{/* ============== ANALYTICS TAB ============== */}
|
|
1980
|
+
{activeTab === "analytics" && !tabLoading && (
|
|
1981
|
+
<div className="space-y-6 animate-fade-in">
|
|
1982
|
+
<div className="flex items-center justify-between">
|
|
1983
|
+
<div>
|
|
1984
|
+
<h2 className="text-2xl font-bold text-[var(--color-dash-text)] dark:text-white">Analytics</h2>
|
|
1985
|
+
<p className="text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] mt-1">
|
|
1986
|
+
Performance analytics powered by Tableau
|
|
1987
|
+
</p>
|
|
1988
|
+
</div>
|
|
1989
|
+
<div className="flex items-center gap-2 text-xs text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]">
|
|
1990
|
+
<span className="inline-flex items-center gap-1 px-2 py-1 rounded bg-[var(--color-dash-surface)] dark:bg-[var(--color-dash-muted)]/10 border border-[var(--color-dash-label)]/20 dark:border-[var(--color-dash-muted)]/30">
|
|
1991
|
+
<span className="h-1.5 w-1.5 rounded-full bg-[var(--color-dash-success)] animate-pulse" />
|
|
1992
|
+
Live from Tableau
|
|
1993
|
+
</span>
|
|
1994
|
+
</div>
|
|
1995
|
+
</div>
|
|
1996
|
+
|
|
1997
|
+
{/* Key Metrics Row */}
|
|
1998
|
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
1999
|
+
{[
|
|
2000
|
+
{ label: "Total Revenue (6mo)", value: "$283,000", delta: "+73%", deltaColor: "var(--color-dash-success)" },
|
|
2001
|
+
{ label: "Avg Occupancy Rate", value: "77.8%", delta: "+5.2%", deltaColor: "var(--color-dash-success)" },
|
|
2002
|
+
{ label: "RevPAR", value: "$149.25", delta: "+12%", deltaColor: "var(--color-dash-success)" },
|
|
2003
|
+
].map((m) => (
|
|
2004
|
+
<div key={m.label} className="bg-white dark:bg-[var(--color-dash-text)] rounded-xl p-5 border border-[var(--color-dash-label)]/20 dark:border-[var(--color-dash-muted)]/30 shadow-sm">
|
|
2005
|
+
<p className="text-xs font-semibold text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] uppercase tracking-wider mb-1">{m.label}</p>
|
|
2006
|
+
<div className="flex items-baseline gap-2">
|
|
2007
|
+
<p className="text-2xl font-black text-[var(--color-dash-text)] dark:text-white">{m.value}</p>
|
|
2008
|
+
<span className="text-xs font-bold" style={{ color: m.deltaColor }}>{m.delta}</span>
|
|
2009
|
+
</div>
|
|
2010
|
+
</div>
|
|
2011
|
+
))}
|
|
2012
|
+
</div>
|
|
2013
|
+
|
|
2014
|
+
{/* Charts Row 1: Revenue Trend + Booking Pipeline */}
|
|
2015
|
+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
2016
|
+
{/* Revenue Trend (multi-line) */}
|
|
2017
|
+
<div className="bg-white dark:bg-[var(--color-dash-text)] rounded-2xl p-8 border border-[var(--color-dash-label)]/20 dark:border-[var(--color-dash-muted)]/30 shadow-sm">
|
|
2018
|
+
<div className="flex items-center justify-between mb-2">
|
|
2019
|
+
<div>
|
|
2020
|
+
<h3 className="text-lg font-bold text-[var(--color-dash-text)] dark:text-white">Revenue Trend</h3>
|
|
2021
|
+
<p className="text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] mt-1">Monthly revenue by property</p>
|
|
2022
|
+
</div>
|
|
2023
|
+
</div>
|
|
2024
|
+
<div className="flex gap-4 mb-4 text-xs">
|
|
2025
|
+
{[{ label: "Austin", color: "#5BC8C8" }, { label: "NYC", color: "#6366f1" }, { label: "Chicago", color: "#f59e0b" }, { label: "SF Bay", color: "#ef4444" }].map(l => (
|
|
2026
|
+
<span key={l.label} className="flex items-center gap-1.5 text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]">
|
|
2027
|
+
<span className="h-2 w-2 rounded-full" style={{ backgroundColor: l.color }} />
|
|
2028
|
+
{l.label}
|
|
2029
|
+
</span>
|
|
2030
|
+
))}
|
|
2031
|
+
</div>
|
|
2032
|
+
<D3Chart
|
|
2033
|
+
data={[
|
|
2034
|
+
{ x: 0, austin: 13800, nyc: 9800, chicago: 7200, sf: 5400 },
|
|
2035
|
+
{ x: 1, austin: 13800, nyc: 10200, chicago: 8100, sf: 6200 },
|
|
2036
|
+
{ x: 2, austin: 15200, nyc: 11400, chicago: 9600, sf: 7100 },
|
|
2037
|
+
{ x: 3, austin: 16900, nyc: 12600, chicago: 10400, sf: 8800 },
|
|
2038
|
+
{ x: 4, austin: 18100, nyc: 13900, chicago: 11100, sf: 10800 },
|
|
2039
|
+
{ x: 5, austin: 19800, nyc: 15200, chicago: 12600, sf: 15000 },
|
|
2040
|
+
]}
|
|
2041
|
+
renderChart={(svg, data, dims) => {
|
|
2042
|
+
const sel = d3.select(svg);
|
|
2043
|
+
sel.selectAll("*").remove();
|
|
2044
|
+
const m = { top: 12, right: 16, bottom: 28, left: 48 };
|
|
2045
|
+
const w = dims.width, h = dims.height;
|
|
2046
|
+
const iw = w - m.left - m.right, ih = h - m.top - m.bottom;
|
|
2047
|
+
sel.attr("viewBox", `0 0 ${w} ${h}`);
|
|
2048
|
+
const g = sel.append("g").attr("transform", `translate(${m.left},${m.top})`);
|
|
2049
|
+
const months = ["Oct", "Nov", "Dec", "Jan", "Feb", "Mar"];
|
|
2050
|
+
const x = d3.scaleLinear().domain([0, 5]).range([0, iw]);
|
|
2051
|
+
const allVals = data.flatMap(d => [d.austin, d.nyc, d.chicago, d.sf]);
|
|
2052
|
+
const y = d3.scaleLinear().domain([d3.min(allVals) * 0.9, d3.max(allVals) * 1.05]).range([ih, 0]);
|
|
2053
|
+
g.append("g").call(d3.axisLeft(y).ticks(5).tickFormat(d => `$${(d/1000).toFixed(0)}K`).tickSize(-iw))
|
|
2054
|
+
.call(a => a.selectAll("text").attr("fill", "#94a3b8").attr("font-size", "10px"))
|
|
2055
|
+
.call(a => a.selectAll(".tick line").attr("stroke", "#e2e8f0").attr("stroke-dasharray", "2,2"))
|
|
2056
|
+
.call(a => a.select(".domain").remove());
|
|
2057
|
+
g.append("g").attr("transform", `translate(0,${ih})`).call(d3.axisBottom(x).ticks(6).tickFormat((_, i) => months[i]).tickSize(0))
|
|
2058
|
+
.call(a => a.selectAll("text").attr("fill", "#94a3b8").attr("font-size", "11px"))
|
|
2059
|
+
.call(a => a.select(".domain").remove());
|
|
2060
|
+
const series = [{ key: "austin", color: "#5BC8C8" }, { key: "nyc", color: "#6366f1" }, { key: "chicago", color: "#f59e0b" }, { key: "sf", color: "#ef4444" }];
|
|
2061
|
+
series.forEach(s => {
|
|
2062
|
+
const area = d3.area().x(d => x(d.x)).y0(ih).y1(d => y(d[s.key])).curve(d3.curveMonotoneX);
|
|
2063
|
+
g.append("path").datum(data).attr("fill", s.color).attr("opacity", 0.08).attr("d", area);
|
|
2064
|
+
const line = d3.line().x(d => x(d.x)).y(d => y(d[s.key])).curve(d3.curveMonotoneX);
|
|
2065
|
+
g.append("path").datum(data).attr("fill", "none").attr("stroke", s.color).attr("stroke-width", 2.5).attr("d", line);
|
|
2066
|
+
});
|
|
2067
|
+
}}
|
|
2068
|
+
height={240}
|
|
2069
|
+
responsive
|
|
2070
|
+
/>
|
|
2071
|
+
</div>
|
|
2072
|
+
|
|
2073
|
+
{/* Booking Pipeline (grouped bar) */}
|
|
2074
|
+
<div className="bg-white dark:bg-[var(--color-dash-text)] rounded-2xl p-8 border border-[var(--color-dash-label)]/20 dark:border-[var(--color-dash-muted)]/30 shadow-sm">
|
|
2075
|
+
<div className="flex items-center justify-between mb-2">
|
|
2076
|
+
<div>
|
|
2077
|
+
<h3 className="text-lg font-bold text-[var(--color-dash-text)] dark:text-white">Booking Pipeline</h3>
|
|
2078
|
+
<p className="text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] mt-1">Upcoming reservations by category</p>
|
|
2079
|
+
</div>
|
|
2080
|
+
</div>
|
|
2081
|
+
<div className="flex gap-4 mb-4 text-xs">
|
|
2082
|
+
<span className="flex items-center gap-1.5 text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]"><span className="h-2 w-2 rounded-full bg-[#5BC8C8]" />Confirmed</span>
|
|
2083
|
+
<span className="flex items-center gap-1.5 text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]"><span className="h-2 w-2 rounded-full bg-[#f59e0b]" />Tentative</span>
|
|
2084
|
+
</div>
|
|
2085
|
+
<D3Chart
|
|
2086
|
+
data={[
|
|
2087
|
+
{ x: "Corporate", confirmed: 142, tentative: 38 },
|
|
2088
|
+
{ x: "Leisure", confirmed: 89, tentative: 22 },
|
|
2089
|
+
{ x: "Group", confirmed: 34, tentative: 12 },
|
|
2090
|
+
{ x: "Events", confirmed: 18, tentative: 8 },
|
|
2091
|
+
{ x: "Last-Min", confirmed: 56, tentative: 14 },
|
|
2092
|
+
]}
|
|
2093
|
+
renderChart={(svg, data, dims) => {
|
|
2094
|
+
const sel = d3.select(svg);
|
|
2095
|
+
sel.selectAll("*").remove();
|
|
2096
|
+
const m = { top: 12, right: 16, bottom: 32, left: 48 };
|
|
2097
|
+
const w = dims.width, h = dims.height;
|
|
2098
|
+
const iw = w - m.left - m.right, ih = h - m.top - m.bottom;
|
|
2099
|
+
sel.attr("viewBox", `0 0 ${w} ${h}`);
|
|
2100
|
+
const g = sel.append("g").attr("transform", `translate(${m.left},${m.top})`);
|
|
2101
|
+
const keys = ["confirmed", "tentative"];
|
|
2102
|
+
const colors = ["#5BC8C8", "#f59e0b"];
|
|
2103
|
+
const x0 = d3.scaleBand().domain(data.map(d => d.x)).range([0, iw]).padding(0.3);
|
|
2104
|
+
const x1 = d3.scaleBand().domain(keys).range([0, x0.bandwidth()]).padding(0.05);
|
|
2105
|
+
const yMax = d3.max(data, d => d3.max(keys, k => d[k])) * 1.15;
|
|
2106
|
+
const y = d3.scaleLinear().domain([0, yMax]).range([ih, 0]);
|
|
2107
|
+
g.append("g").call(d3.axisLeft(y).ticks(5).tickFormat(d3.format(",.0f")).tickSize(-iw))
|
|
2108
|
+
.call(a => a.selectAll("text").attr("fill", "#94a3b8").attr("font-size", "10px"))
|
|
2109
|
+
.call(a => a.selectAll(".tick line").attr("stroke", "#e2e8f0").attr("stroke-dasharray", "2,2"))
|
|
2110
|
+
.call(a => a.select(".domain").remove());
|
|
2111
|
+
g.append("g").attr("transform", `translate(0,${ih})`).call(d3.axisBottom(x0).tickSize(0))
|
|
2112
|
+
.call(a => a.selectAll("text").attr("fill", "#94a3b8").attr("font-size", "11px"))
|
|
2113
|
+
.call(a => a.select(".domain").remove());
|
|
2114
|
+
const rows = g.selectAll(".bar-group").data(data).join("g").attr("transform", d => `translate(${x0(d.x)},0)`);
|
|
2115
|
+
keys.forEach((key, i) => {
|
|
2116
|
+
rows.append("rect").attr("x", x1(key)).attr("y", d => y(d[key])).attr("width", x1.bandwidth()).attr("height", d => ih - y(d[key])).attr("rx", 4).attr("fill", colors[i]);
|
|
2117
|
+
});
|
|
2118
|
+
}}
|
|
2119
|
+
height={240}
|
|
2120
|
+
responsive
|
|
2121
|
+
/>
|
|
2122
|
+
</div>
|
|
2123
|
+
</div>
|
|
2124
|
+
|
|
2125
|
+
{/* Charts Row 2: Guest Satisfaction Trend + Occupancy */}
|
|
2126
|
+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
2127
|
+
{/* Guest Satisfaction */}
|
|
2128
|
+
<div className="bg-white dark:bg-[var(--color-dash-text)] rounded-2xl p-8 border border-[var(--color-dash-label)]/20 dark:border-[var(--color-dash-muted)]/30 shadow-sm">
|
|
2129
|
+
<div className="flex items-center justify-between mb-2">
|
|
2130
|
+
<div>
|
|
2131
|
+
<h3 className="text-lg font-bold text-[var(--color-dash-text)] dark:text-white">Guest Satisfaction Trend</h3>
|
|
2132
|
+
<p className="text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] mt-1">Average rating over the last 6 months</p>
|
|
2133
|
+
</div>
|
|
2134
|
+
<div className="flex items-center gap-1.5 text-sm text-[var(--color-dash-success)] font-semibold">
|
|
2135
|
+
<SparklesIcon className="h-4 w-4" />
|
|
2136
|
+
Trending up
|
|
2137
|
+
</div>
|
|
2138
|
+
</div>
|
|
2139
|
+
<div className="flex gap-4 mb-4 text-xs">
|
|
2140
|
+
{[{ label: "Austin", color: "#5BC8C8" }, { label: "NYC", color: "#6366f1" }, { label: "Chicago", color: "#f59e0b" }, { label: "SF Bay", color: "#ef4444" }].map(l => (
|
|
2141
|
+
<span key={l.label} className="flex items-center gap-1.5 text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]">
|
|
2142
|
+
<span className="h-2 w-2 rounded-full" style={{ backgroundColor: l.color }} />
|
|
2143
|
+
{l.label}
|
|
2144
|
+
</span>
|
|
2145
|
+
))}
|
|
2146
|
+
</div>
|
|
2147
|
+
<D3Chart
|
|
2148
|
+
data={[
|
|
2149
|
+
{ x: 0, austin: 4.5, nyc: 4.3, chicago: 4.6, sf: 3.8 },
|
|
2150
|
+
{ x: 1, austin: 4.6, nyc: 4.3, chicago: 4.6, sf: 3.9 },
|
|
2151
|
+
{ x: 2, austin: 4.6, nyc: 4.4, chicago: 4.5, sf: 4.0 },
|
|
2152
|
+
{ x: 3, austin: 4.7, nyc: 4.5, chicago: 4.5, sf: 4.1 },
|
|
2153
|
+
{ x: 4, austin: 4.7, nyc: 4.6, chicago: 4.5, sf: 4.2 },
|
|
2154
|
+
{ x: 5, austin: 4.8, nyc: 4.7, chicago: 4.5, sf: 4.3 },
|
|
2155
|
+
]}
|
|
2156
|
+
renderChart={(svg, data, dims) => {
|
|
2157
|
+
const sel = d3.select(svg);
|
|
2158
|
+
sel.selectAll("*").remove();
|
|
2159
|
+
const m = { top: 12, right: 16, bottom: 28, left: 36 };
|
|
2160
|
+
const w = dims.width, h = dims.height;
|
|
2161
|
+
const iw = w - m.left - m.right, ih = h - m.top - m.bottom;
|
|
2162
|
+
sel.attr("viewBox", `0 0 ${w} ${h}`);
|
|
2163
|
+
const g = sel.append("g").attr("transform", `translate(${m.left},${m.top})`);
|
|
2164
|
+
const months = ["Oct", "Nov", "Dec", "Jan", "Feb", "Mar"];
|
|
2165
|
+
const x = d3.scaleLinear().domain([0, 5]).range([0, iw]);
|
|
2166
|
+
const y = d3.scaleLinear().domain([3.5, 5.0]).range([ih, 0]);
|
|
2167
|
+
g.append("g").call(d3.axisLeft(y).ticks(4).tickSize(-iw))
|
|
2168
|
+
.call(a => a.selectAll("text").attr("fill", "#94a3b8").attr("font-size", "10px"))
|
|
2169
|
+
.call(a => a.selectAll(".tick line").attr("stroke", "#e2e8f0").attr("stroke-dasharray", "2,2"))
|
|
2170
|
+
.call(a => a.select(".domain").remove());
|
|
2171
|
+
g.append("g").attr("transform", `translate(0,${ih})`).call(d3.axisBottom(x).ticks(6).tickFormat((_, i) => months[i]).tickSize(0))
|
|
2172
|
+
.call(a => a.selectAll("text").attr("fill", "#94a3b8").attr("font-size", "11px"))
|
|
2173
|
+
.call(a => a.select(".domain").remove());
|
|
2174
|
+
const series = [{ key: "austin", color: "#5BC8C8" }, { key: "nyc", color: "#6366f1" }, { key: "chicago", color: "#f59e0b" }, { key: "sf", color: "#ef4444" }];
|
|
2175
|
+
series.forEach(s => {
|
|
2176
|
+
const line = d3.line().x(d => x(d.x)).y(d => y(d[s.key])).curve(d3.curveMonotoneX);
|
|
2177
|
+
g.append("path").datum(data).attr("fill", "none").attr("stroke", s.color).attr("stroke-width", 2.5).attr("d", line);
|
|
2178
|
+
g.selectAll(`.dot-${s.key}`).data(data).join("circle").attr("cx", d => x(d.x)).attr("cy", d => y(d[s.key])).attr("r", 3).attr("fill", s.color);
|
|
2179
|
+
});
|
|
2180
|
+
}}
|
|
2181
|
+
height={240}
|
|
2182
|
+
responsive
|
|
2183
|
+
/>
|
|
2184
|
+
</div>
|
|
2185
|
+
|
|
2186
|
+
{/* Occupancy by Day of Week */}
|
|
2187
|
+
<div className="bg-white dark:bg-[var(--color-dash-text)] rounded-2xl p-8 border border-[var(--color-dash-label)]/20 dark:border-[var(--color-dash-muted)]/30 shadow-sm">
|
|
2188
|
+
<div className="flex items-center justify-between mb-6">
|
|
2189
|
+
<div>
|
|
2190
|
+
<h3 className="text-lg font-bold text-[var(--color-dash-text)] dark:text-white">Occupancy by Day</h3>
|
|
2191
|
+
<p className="text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] mt-1">Average occupancy rate by day of the week</p>
|
|
2192
|
+
</div>
|
|
2193
|
+
</div>
|
|
2194
|
+
<D3Chart
|
|
2195
|
+
data={[
|
|
2196
|
+
{ x: "Mon", value: 68 },
|
|
2197
|
+
{ x: "Tue", value: 72 },
|
|
2198
|
+
{ x: "Wed", value: 76 },
|
|
2199
|
+
{ x: "Thu", value: 82 },
|
|
2200
|
+
{ x: "Fri", value: 91 },
|
|
2201
|
+
{ x: "Sat", value: 94 },
|
|
2202
|
+
{ x: "Sun", value: 85 },
|
|
2203
|
+
]}
|
|
2204
|
+
renderChart={(svg, data, dims) => {
|
|
2205
|
+
const sel = d3.select(svg);
|
|
2206
|
+
sel.selectAll("*").remove();
|
|
2207
|
+
const m = { top: 12, right: 16, bottom: 32, left: 40 };
|
|
2208
|
+
const w = dims.width, h = dims.height;
|
|
2209
|
+
const iw = w - m.left - m.right, ih = h - m.top - m.bottom;
|
|
2210
|
+
sel.attr("viewBox", `0 0 ${w} ${h}`);
|
|
2211
|
+
const g = sel.append("g").attr("transform", `translate(${m.left},${m.top})`);
|
|
2212
|
+
const x = d3.scaleBand().domain(data.map(d => d.x)).range([0, iw]).padding(0.35);
|
|
2213
|
+
const y = d3.scaleLinear().domain([0, 100]).range([ih, 0]);
|
|
2214
|
+
g.append("g").call(d3.axisLeft(y).ticks(5).tickFormat(d => d + "%").tickSize(-iw))
|
|
2215
|
+
.call(a => a.selectAll("text").attr("fill", "#94a3b8").attr("font-size", "10px"))
|
|
2216
|
+
.call(a => a.selectAll(".tick line").attr("stroke", "#e2e8f0").attr("stroke-dasharray", "2,2"))
|
|
2217
|
+
.call(a => a.select(".domain").remove());
|
|
2218
|
+
g.append("g").attr("transform", `translate(0,${ih})`).call(d3.axisBottom(x).tickSize(0))
|
|
2219
|
+
.call(a => a.selectAll("text").attr("fill", "#94a3b8").attr("font-size", "11px"))
|
|
2220
|
+
.call(a => a.select(".domain").remove());
|
|
2221
|
+
g.selectAll("rect").data(data).join("rect")
|
|
2222
|
+
.attr("x", d => x(d.x)).attr("y", d => y(d.value))
|
|
2223
|
+
.attr("width", x.bandwidth()).attr("height", d => ih - y(d.value))
|
|
2224
|
+
.attr("rx", 4).attr("fill", d => d.value >= 90 ? "#5BC8C8" : d.value >= 80 ? "#5BC8C8cc" : "#5BC8C880");
|
|
2225
|
+
g.selectAll(".val-label").data(data).join("text")
|
|
2226
|
+
.attr("x", d => x(d.x) + x.bandwidth() / 2).attr("y", d => y(d.value) - 6)
|
|
2227
|
+
.attr("text-anchor", "middle").attr("fill", "#64748b").attr("font-size", "10px").attr("font-weight", "600")
|
|
2228
|
+
.text(d => d.value + "%");
|
|
2229
|
+
}}
|
|
2230
|
+
height={240}
|
|
2231
|
+
responsive
|
|
2232
|
+
/>
|
|
2233
|
+
</div>
|
|
2234
|
+
</div>
|
|
2235
|
+
|
|
2236
|
+
{/* Revenue Breakdown Table */}
|
|
2237
|
+
<div className="bg-white dark:bg-[var(--color-dash-text)] rounded-2xl p-8 border border-[var(--color-dash-label)]/20 dark:border-[var(--color-dash-muted)]/30 shadow-sm">
|
|
2238
|
+
<div className="flex items-center justify-between mb-6">
|
|
2239
|
+
<div>
|
|
2240
|
+
<h3 className="text-lg font-bold text-[var(--color-dash-text)] dark:text-white">Revenue Breakdown by Property</h3>
|
|
2241
|
+
<p className="text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] mt-1">6-month performance comparison across all properties</p>
|
|
2242
|
+
</div>
|
|
2243
|
+
</div>
|
|
2244
|
+
<div className="overflow-x-auto">
|
|
2245
|
+
<table className="w-full">
|
|
2246
|
+
<thead className="border-b border-[var(--color-dash-label)]/20 dark:border-[var(--color-dash-muted)]/30">
|
|
2247
|
+
<tr>
|
|
2248
|
+
{["Property", "Revenue", "ADR", "Occupancy", "RevPAR", "Bookings", "Cancellations", "Net Growth"].map((h) => (
|
|
2249
|
+
<th key={h} className="px-4 py-3 text-left text-xs font-semibold text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)] uppercase tracking-wider">{h}</th>
|
|
2250
|
+
))}
|
|
2251
|
+
</tr>
|
|
2252
|
+
</thead>
|
|
2253
|
+
<tbody className="divide-y divide-[var(--color-dash-label)]/10 dark:divide-[var(--color-dash-muted)]/20">
|
|
2254
|
+
{[
|
|
2255
|
+
{ name: "Austin Convention Center", revenue: "$97,600", adr: "$189", occupancy: "82%", revpar: "$155", bookings: 516, cancellations: 12, growth: "+60%" },
|
|
2256
|
+
{ name: "Midtown NYC", revenue: "$73,100", adr: "$215", occupancy: "79%", revpar: "$170", bookings: 340, cancellations: 8, growth: "+55%" },
|
|
2257
|
+
{ name: "Chicago Downtown", revenue: "$59,000", adr: "$165", occupancy: "76%", revpar: "$125", bookings: 358, cancellations: 6, growth: "+75%" },
|
|
2258
|
+
{ name: "San Francisco Bay", revenue: "$53,300", adr: "$198", occupancy: "74%", revpar: "$147", bookings: 269, cancellations: 15, growth: "+100%" },
|
|
2259
|
+
].map((r) => (
|
|
2260
|
+
<tr key={r.name} className="hover:bg-[var(--color-dash-surface)]/50 dark:hover:bg-[var(--color-dash-muted)]/5 transition-colors">
|
|
2261
|
+
<td className="px-4 py-3 text-sm font-semibold text-[var(--color-dash-text)] dark:text-white">{r.name}</td>
|
|
2262
|
+
<td className="px-4 py-3 text-sm font-bold text-[var(--color-dash-text)] dark:text-white">{r.revenue}</td>
|
|
2263
|
+
<td className="px-4 py-3 text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]">{r.adr}</td>
|
|
2264
|
+
<td className="px-4 py-3 text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]">{r.occupancy}</td>
|
|
2265
|
+
<td className="px-4 py-3 text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]">{r.revpar}</td>
|
|
2266
|
+
<td className="px-4 py-3 text-sm text-[var(--color-dash-text)] dark:text-white">{r.bookings}</td>
|
|
2267
|
+
<td className="px-4 py-3 text-sm text-[var(--color-dash-danger)]">{r.cancellations}</td>
|
|
2268
|
+
<td className="px-4 py-3 text-sm font-bold text-[var(--color-dash-success)]">{r.growth}</td>
|
|
2269
|
+
</tr>
|
|
2270
|
+
))}
|
|
2271
|
+
</tbody>
|
|
2272
|
+
</table>
|
|
2273
|
+
</div>
|
|
2274
|
+
</div>
|
|
2275
|
+
|
|
2276
|
+
{/* AI Analytics Insight */}
|
|
2277
|
+
<div className="bg-gradient-to-r from-[var(--color-dash-dark)] to-[var(--color-dash-dark)]/90 rounded-xl p-5 flex items-start gap-4">
|
|
2278
|
+
<div className="flex-shrink-0 h-9 w-9 rounded-lg bg-white/10 flex items-center justify-center">
|
|
2279
|
+
<SparklesIcon className="h-5 w-5 text-[var(--color-dash-accent)]" />
|
|
2280
|
+
</div>
|
|
2281
|
+
<div className="flex-1">
|
|
2282
|
+
<p className="text-sm font-semibold text-white mb-1">Revenue Acceleration Opportunity</p>
|
|
2283
|
+
<p className="text-xs text-white/60 leading-relaxed">Based on Tableau data, San Francisco Bay has the highest growth rate (+100%) but lowest absolute revenue. Increasing its occupancy from 74% to 82% (matching Austin) would generate an additional $14,200/month. The main constraint is response time (3.2h), which correlates with lower rebooking rates.</p>
|
|
2284
|
+
</div>
|
|
2285
|
+
</div>
|
|
2286
|
+
</div>
|
|
2287
|
+
)}
|
|
2288
|
+
|
|
2289
|
+
{/* DATA 360 TAB: added by headless-data-360 skill during demo */}
|
|
2290
|
+
|
|
1608
2291
|
{/* Footer */}
|
|
1609
2292
|
<div className="text-center py-8 text-sm text-[var(--color-dash-label)]">
|
|
1610
2293
|
<p>© 2026 Engine · Welcome to modern travel management</p>
|