diff --git a/package-lock.json b/package-lock.json
index 6884ce5..792a620 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12,6 +12,7 @@
"@dnd-kit/modifiers": "^9.0.0",
"@google-analytics/data": "^5.2.0",
"@hookform/resolvers": "^5.2.1",
+ "@icons-pack/react-simple-icons": "^13.8.0",
"@next/third-parties": "^15.5.2",
"@radix-ui/react-accordion": "^1.2.11",
"@radix-ui/react-alert-dialog": "^1.1.14",
@@ -478,6 +479,15 @@
"url": "https://github.com/sponsors/nzakas"
}
},
+ "node_modules/@icons-pack/react-simple-icons": {
+ "version": "13.8.0",
+ "resolved": "https://registry.npmjs.org/@icons-pack/react-simple-icons/-/react-simple-icons-13.8.0.tgz",
+ "integrity": "sha512-iZrhL1fSklfCCVn68IYHaAoKfcby3RakUTn2tRPyHBkhr2tkYqeQbjJWf+NizIYBzKBn2IarDJXmTdXd6CuEfw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^16.13 || ^17 || ^18 || ^19"
+ }
+ },
"node_modules/@img/colour": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz",
diff --git a/package.json b/package.json
index a57c3d7..9f1a93e 100644
--- a/package.json
+++ b/package.json
@@ -14,6 +14,7 @@
"@dnd-kit/modifiers": "^9.0.0",
"@google-analytics/data": "^5.2.0",
"@hookform/resolvers": "^5.2.1",
+ "@icons-pack/react-simple-icons": "^13.8.0",
"@next/third-parties": "^15.5.2",
"@radix-ui/react-accordion": "^1.2.11",
"@radix-ui/react-alert-dialog": "^1.1.14",
diff --git a/src/app/(home-app)/events/[event_id]/_components/Sessions.tsx b/src/app/(home-app)/events/[event_id]/_components/Sessions.tsx
index 42378ba..e3a729a 100644
--- a/src/app/(home-app)/events/[event_id]/_components/Sessions.tsx
+++ b/src/app/(home-app)/events/[event_id]/_components/Sessions.tsx
@@ -1,49 +1,13 @@
'use client';
-import {PaginatedResponse} from '@/types/paginatedResponse';
import React, {useEffect, useState} from 'react';
import {SessionInfoBasicDTO} from "@/types/event";
-import {getEventSessions, getEventSessionsInRange} from '@/lib/actions/public/eventActions';
+import {getEventSessionsInRange} from '@/lib/actions/public/eventActions';
import {CalendarIcon} from "lucide-react";
import {Button} from "@/components/ui/button";
import {SessionItem} from "@/app/(home-app)/events/[event_id]/_components/SessionItem";
import {DatePicker} from "@/components/ui/datepicker";
-const Sessions = ({eventId}: { eventId: string }) => {
- const [page, setPage] = useState(0);
- const [sessionsData, setSessionsData] = useState | null>(null);
- const [isLoading, setIsLoading] = useState(true);
-
- useEffect(() => {
- const fetchSessions = async () => {
- setIsLoading(true);
- const data = await getEventSessions({eventId, page});
- setSessionsData(data);
- setIsLoading(false);
- };
- fetchSessions();
- }, [eventId, page]);
-
- if (isLoading) return Loading sessions...
;
- if (!sessionsData || sessionsData.empty) return No sessions found.
;
-
- return (
-
-
- Sessions
-
-
- {sessionsData.content.map(session => )}
-
-
-
- Page {sessionsData.number + 1} of {sessionsData.totalPages}
-
-
-
- );
-};
-
const SessionsNoPagination = ({eventId}: { eventId: string }) => {
// Default dates: one week ago to three months ahead
const now = new Date();
@@ -118,4 +82,4 @@ const SessionsNoPagination = ({eventId}: { eventId: string }) => {
);
};
-export default SessionsNoPagination;
+export default SessionsNoPagination;
\ No newline at end of file
diff --git a/src/app/(home-app)/events/[event_id]/layout.tsx b/src/app/(home-app)/events/[event_id]/layout.tsx
index b384a3f..60626c5 100644
--- a/src/app/(home-app)/events/[event_id]/layout.tsx
+++ b/src/app/(home-app)/events/[event_id]/layout.tsx
@@ -1,16 +1,16 @@
-import React, {Suspense} from 'react';
-import {EventHero, ReviewEventHeroSkeleton} from "@/app/(home-app)/events/[event_id]/_components/EventHero";
-import {Separator} from "@/components/ui/separator";
-import {getEventSummery, getEventTotalViews} from "@/lib/actions/public/server/eventActions";
-import {EventTracker} from "@/app/(home-app)/events/[event_id]/_components/EventTracker";
-import {EventOverview} from "@/app/manage/_components/review/EventOverview";
+import React, { Suspense } from 'react';
+import { EventHero, ReviewEventHeroSkeleton } from "@/app/(home-app)/events/[event_id]/_components/EventHero";
+import { Separator } from "@/components/ui/separator";
+import { getEventSummery, getEventTotalViews } from "@/lib/actions/public/server/eventActions";
+import { EventOverview } from "@/app/manage/_components/review/EventOverview";
+import { EventTracker } from './_components/EventTracker';
-export default async function Layout({params, children}: {
+export default async function Layout({ params, children }: {
params: Promise<{ event_id: string }>
children: React.ReactNode;
}) {
- const {event_id} = await params;
+ const { event_id } = await params;
const eventSummery = await getEventSummery(event_id);
const viewsData = await getEventTotalViews(event_id);
@@ -18,18 +18,18 @@ export default async function Layout({params, children}: {
-
- }>
+ }>
+
-
-
+ viewCount={viewsData.success ? viewsData.viewCount : undefined} />
+
+
-
+
{children}
diff --git a/src/app/(home-app)/page.tsx b/src/app/(home-app)/page.tsx
index 0ea5c2f..3e0d499 100644
--- a/src/app/(home-app)/page.tsx
+++ b/src/app/(home-app)/page.tsx
@@ -5,71 +5,69 @@ import CategorySection from "@/app/(home-app)/_components/CategorySection";
import {EventThumbnailDTO} from "@/types/event";
import {sriLankaLocations} from "@/app/(home-app)/_utils/locations";
import {LocationCard} from "@/app/(home-app)/_components/LocationCard";
-import {ArrowRight, Calendar, MapPin, Users} from "lucide-react";
+import {ArrowRight, Calendar, MapPin, Ticket} from "lucide-react";
import Link from 'next/link';
import {useAuth} from '@/providers/AuthProvider';
-
-const trendingEvents: EventThumbnailDTO[] = [
- {
- id: '1',
- title: 'Colombo Music Fest 2025',
- coverPhotoUrl: 'https://images.unsplash.com/photo-1516450360452-9312f5e86fc7?q=80&w=2070&auto=format=fit=crop',
- organizationName: 'Vibe Events',
- categoryName: 'Music',
- earliestSession: {
- startTime: '2025-09-15T18:00:00Z',
- venueName: 'Galle Face Green',
- city: 'Colombo'
- },
- startingPrice: 25.00,
- discounts: null
- },
- {
- id: '2',
- title: 'Kandy Esala Perahera Viewing',
- coverPhotoUrl: 'https://images.unsplash.com/photo-1566766188646-5d0310191714?q=80&w=2070&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
- organizationName: 'Cultural SL',
- categoryName: 'Culture',
- earliestSession: {
- startTime: '2025-08-28T19:00:00Z',
- venueName: 'Temple of the Tooth',
- city: 'Kandy'
- },
- startingPrice: 15.00,
- discounts: null
- },
- {
- id: '3',
- title: 'Galle Literary Festival',
- coverPhotoUrl: 'https://images.unsplash.com/photo-1455390582262-044cdead277a?q=80&w=1973&auto=format=fit=crop',
- organizationName: 'Sri Lanka Arts',
- categoryName: 'Arts & Literature',
- earliestSession: {
- startTime: '2025-10-10T10:00:00Z',
- venueName: 'Galle Fort',
- city: 'Galle'
- },
- startingPrice: null,
- discounts: null
- },
- {
- id: '4',
- title: 'Startup Summit Sri Lanka',
- coverPhotoUrl: 'https://images.unsplash.com/photo-1542744173-8e7e53415bb0?q=80&w=2070&auto=format=fit=crop',
- organizationName: 'TechHub LK',
- categoryName: 'Tech',
- earliestSession: {
- startTime: '2025-11-05T09:00:00Z',
- venueName: 'BMICH',
- city: 'Colombo'
- },
- startingPrice: 50.00,
- discounts: null
- },
-];
+import { useEffect, useState } from 'react';
+import {getTrendingEvents, getTotalSessionsCount, getTotalTicketsSold} from '@/lib/actions/public/eventActions';
+import { Skeleton } from '@/components/ui/skeleton';
+import CounterAnimation from "@/components/ui/counter-animation";
export default function HomePage() {
const {isAuthenticated, keycloak} = useAuth();
+ const [trendingEvents, setTrendingEvents] = useState
([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const [totalSessionsCount, setTotalSessionsCount] = useState(0);
+ const [totalTicketsSold, setTotalTicketsSold] = useState(0);
+ const [loadingSessionsCount, setLoadingSessionsCount] = useState(true);
+ const [loadingTicketsCount, setLoadingTicketsCount] = useState(true);
+
+ useEffect(() => {
+ const fetchTrendingEvents = async () => {
+ try {
+ setLoading(true);
+ const events = await getTrendingEvents(3);
+ setTrendingEvents(events);
+ setError(null);
+ } catch (err) {
+ console.error("Failed to fetch trending events:", err);
+ setError("Failed to load trending events. Please try again later.");
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const fetchTotalSessionsCount = async () => {
+ try {
+ setLoadingSessionsCount(true);
+ const count = await getTotalSessionsCount();
+ setTotalSessionsCount(count);
+ } catch (err) {
+ console.error("Failed to fetch total sessions count:", err);
+ setTotalSessionsCount(0);
+ } finally {
+ setLoadingSessionsCount(false);
+ }
+ };
+
+ const fetchTotalTicketsSold = async () => {
+ try {
+ setLoadingTicketsCount(true);
+ const count = await getTotalTicketsSold();
+ setTotalTicketsSold(count);
+ } catch (err) {
+ console.error("Failed to fetch total tickets sold:", err);
+ setTotalTicketsSold(0);
+ } finally {
+ setLoadingTicketsCount(false);
+ }
+ };
+
+ fetchTrendingEvents();
+ fetchTotalSessionsCount();
+ fetchTotalTicketsSold();
+ }, []);
return (
@@ -79,7 +77,7 @@ export default function HomePage() {
Discover
- Amazing Events
+ Amazing Events
Find and book extraordinary experiences happening around you
@@ -92,7 +90,20 @@ export default function HomePage() {
className="flex items-center justify-center w-12 h-12 bg-primary/10 rounded-full mx-auto mb-3">
- 1000+
+
+ {loadingSessionsCount ? (
+
+ ) : (
+ <>
+
+ >
+ )}
+
Events
@@ -106,10 +117,23 @@ export default function HomePage() {
-
+
+
+
+ {loadingTicketsCount ? (
+
+ ) : (
+ <>
+
+ >
+ )}
-
50K+
-
Attendees
+
Tickets Sold
@@ -150,10 +174,65 @@ export default function HomePage() {
Don't miss out on the most popular events happening right now
-
- {trendingEvents.map((event) => (
-
- ))}
+
+ {error && (
+
+
{error}
+
+
+ )}
+
+
+ {loading ? (
+ // Skeleton loading UI
+ Array(3).fill(0).map((_, index) => (
+
+
+
+
+
+
+
+ ))
+ ) : trendingEvents.length > 0 ? (
+ // Display actual events
+ trendingEvents.map((event: EventThumbnailDTO) => (
+
+ ))
+ ) : (
+ // No events found
+
+
No trending events found at the moment.
+
+ )}
+
+
+ {/* View All Link */}
+
+
+
+
diff --git a/src/app/manage/_components/review/ReviewSessions.tsx b/src/app/manage/_components/review/ReviewSessions.tsx
index 19a4584..650b97f 100644
--- a/src/app/manage/_components/review/ReviewSessions.tsx
+++ b/src/app/manage/_components/review/ReviewSessions.tsx
@@ -3,7 +3,7 @@ import {format, parseISO} from 'date-fns';
import {Calendar, Clock, LinkIcon, MapPin, Tag} from 'lucide-react';
import {Accordion, AccordionContent, AccordionItem, AccordionTrigger,} from "@/components/ui/accordion";
import {Badge} from '@/components/ui/badge';
-import {SessionParsed, TierFormData} from '@/lib/validators/event';
+import {SessionDTO, TierFormData} from '@/lib/validators/event';
import dynamic from "next/dynamic";
import {SessionType} from "@/types/enums/sessionType";
@@ -13,7 +13,7 @@ const SeatingInformation = dynamic(
);
interface ReviewSessionsProps {
- sessions: SessionParsed[];
+ sessions: SessionDTO[];
tiers: TierFormData[];
}
@@ -38,7 +38,7 @@ export const ReviewSessions: React.FC = ({sessions, tiers})
};
interface SessionAccordionItemProps {
- session: SessionParsed;
+ session: SessionDTO;
tiers: TierFormData[];
index: number;
}
@@ -94,7 +94,7 @@ const SessionAccordionItem: React.FC = ({session, ind
};
interface SessionDetailsProps {
- session: SessionParsed;
+ session: SessionDTO;
}
const SessionDetails: React.FC = ({session}) => {
diff --git a/src/app/manage/_components/review/ReviewTicketTiers.tsx b/src/app/manage/_components/review/ReviewTicketTiers.tsx
index b0dc4a4..d53c8b5 100644
--- a/src/app/manage/_components/review/ReviewTicketTiers.tsx
+++ b/src/app/manage/_components/review/ReviewTicketTiers.tsx
@@ -38,7 +38,6 @@ export const TicketTierCard: React.FC = ({tier}) => {
{tier.name}
LKR {tier.price.toFixed(2)}
- {/**/}
diff --git a/src/app/manage/_components/review/SeatingInformation.tsx b/src/app/manage/_components/review/SeatingInformation.tsx
index b52f97e..7a17e94 100644
--- a/src/app/manage/_components/review/SeatingInformation.tsx
+++ b/src/app/manage/_components/review/SeatingInformation.tsx
@@ -1,6 +1,6 @@
"use client";
-import { SessionParsed, TierFormData} from "@/lib/validators/event";
+import { SessionDTO, TierFormData} from "@/lib/validators/event";
import * as React from "react";
import {MapContainer, TileLayer, Marker, Popup} from "react-leaflet";
import {Armchair, Users} from "lucide-react";
@@ -11,7 +11,7 @@ import L, {LatLngTuple} from "leaflet";
interface SeatingInformationProps {
isOnline: boolean;
- session: SessionParsed;
+ session: SessionDTO;
tiers: TierFormData[];
}
diff --git a/src/app/manage/_components/review/SeatingLayout.tsx b/src/app/manage/_components/review/SeatingLayout.tsx
index a68d4e1..c1b8f44 100644
--- a/src/app/manage/_components/review/SeatingLayout.tsx
+++ b/src/app/manage/_components/review/SeatingLayout.tsx
@@ -2,12 +2,12 @@ import * as React from "react";
import {Popover, PopoverContent, PopoverTrigger} from "@/components/ui/popover";
import {Button} from "@/components/ui/button";
import {Badge} from "@/components/ui/badge";
-import {SessionParsed, TierFormData} from "@/lib/validators/event";
+import {SessionDTO, TierFormData} from "@/lib/validators/event";
import {getTierColor, getTierName} from "@/lib/utils";
import {SessionType} from "@/types/enums/sessionType";
interface SeatingLayoutProps {
- session: SessionParsed;
+ session: SessionDTO;
tiers: TierFormData[];
}
diff --git a/src/app/manage/organization/[organization_id]/event/[eventId]/analytics/[sessionId]/page.tsx b/src/app/manage/organization/[organization_id]/event/[eventId]/analytics/[sessionId]/page.tsx
deleted file mode 100644
index f74530a..0000000
--- a/src/app/manage/organization/[organization_id]/event/[eventId]/analytics/[sessionId]/page.tsx
+++ /dev/null
@@ -1,55 +0,0 @@
-"use client"
-
-import {useEffect, useState} from "react";
-import {SessionAnalytics} from "@/types/eventAnalytics";
-import {SessionAnalyticsView} from "../_components/SessionAnalyticsView";
-import {Skeleton} from "@/components/ui/skeleton";
-import {getSessionAnalytics} from "@/lib/actions/public/analyticsActions";
-import {useParams} from "next/navigation";
-
-export default function SessionAnalyticsPage() {
- const [sessionAnalytics, setSessionAnalytics] = useState(null);
- const [isLoading, setIsLoading] = useState(true);
- const [error, setError] = useState(null);
- const params = useParams();
- const eventId = params.eventId as string;
- const sessionId = params.sessionId as string;
-
- useEffect(() => {
- const fetchData = async () => {
- try {
- setIsLoading(true);
- const data = await getSessionAnalytics(eventId, sessionId);
- setSessionAnalytics(data);
- } catch (err) {
- setError("Failed to load session analytics.");
- console.error(err);
- } finally {
- setIsLoading(false);
- }
- };
- fetchData();
- }, [eventId, sessionId]);
-
- if (error) {
- return {error}
;
- }
-
- if (isLoading || !sessionAnalytics) {
- return (
-
-
-
-
- {Array.from({length: 4}).map((_, i) => )}
-
-
-
-
-
-
- );
- }
-
- return ;
-}
\ No newline at end of file
diff --git a/src/app/manage/organization/[organization_id]/event/[eventId]/analytics/_components/DailySalesChart.tsx b/src/app/manage/organization/[organization_id]/event/[eventId]/analytics/_components/DailySalesChart.tsx
new file mode 100644
index 0000000..1073629
--- /dev/null
+++ b/src/app/manage/organization/[organization_id]/event/[eventId]/analytics/_components/DailySalesChart.tsx
@@ -0,0 +1,163 @@
+"use client"
+
+import React from 'react';
+import { DailySalesMetrics } from '@/lib/actions/analyticsActions';
+import { Card, CardContent, CardHeader, CardTitle, CardDescription, CardFooter } from '@/components/ui/card';
+import { formatCurrency } from '@/lib/utils';
+import { LineChart, Line, XAxis, YAxis, CartesianGrid, ResponsiveContainer } from 'recharts';
+import { TrendingUp } from "lucide-react";
+
+import {
+ ChartConfig,
+ ChartContainer,
+ ChartTooltip,
+ ChartTooltipContent,
+} from "@/components/ui/chart";
+
+interface DailySalesChartProps {
+ data: DailySalesMetrics[];
+}
+
+export const DailySalesChart: React.FC = ({ data }) => {
+ // Format the data for the chart
+ const formattedData = data.map(item => ({
+ date: new Date(item.date).toLocaleDateString('en-US', { month: 'short', day: 'numeric' }),
+ revenue: item.revenue,
+ tickets: item.tickets_sold
+ }));
+
+ // Calculate trend if there's enough data
+ const calculateTrend = () => {
+ if (data.length < 2) return { percentage: 0, increasing: true };
+
+ // Use the last 7 days for trend calculation if available, otherwise last 2
+ const trendData = data.length > 7 ? data.slice(-7) : data.slice(-2);
+ const startRevenue = trendData[0].revenue;
+ const endRevenue = trendData[trendData.length - 1].revenue;
+
+ const difference = endRevenue - startRevenue;
+ const percentage = startRevenue !== 0
+ ? (difference / startRevenue) * 100
+ : (endRevenue > 0 ? 100 : 0); // Handle division by zero
+
+ return {
+ percentage: Math.abs(percentage),
+ increasing: difference >= 0
+ };
+ };
+
+ const trend = calculateTrend();
+
+ // Get the date range for display
+ const getDateRange = () => {
+ if (data.length === 0) return "No data available";
+ if (data.length === 1) return new Date(data[0].date).toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' });
+
+ const firstDate = new Date(data[0].date);
+ const lastDate = new Date(data[data.length - 1].date);
+
+ return `${firstDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })} - ${lastDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })}`;
+ };
+
+ const chartConfig = {
+ revenue: {
+ label: "Revenue",
+ color: "var(--color-chart-1)",
+ },
+ tickets: {
+ label: "Tickets Sold",
+ color: "var(--color-chart-2)",
+ },
+ } satisfies ChartConfig;
+
+ return (
+
+
+ Daily Sales & Revenue
+ {getDateRange()}
+
+
+
+
+
+ value.slice(0, 3)} // Show abbreviated month
+ />
+ `${formatCurrency(value, 'LKR').slice(0, -3)}`} // Shorten currency
+ />
+
+ {
+ const datePoint = data.find(d => new Date(d.date).toLocaleDateString('en-US', { month: 'short', day: 'numeric' }) === label);
+ return datePoint ? new Date(datePoint.date).toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' }) : label;
+ }}
+ formatter={(value, name) => {
+ if (name === 'revenue') {
+ return formatCurrency(value as number, 'LKR', 'en-LK');
+ }
+ return value.toLocaleString();
+ }}
+ />
+ }
+ />
+
+
+
+
+
+
+
+
+ {data.length >= 2 && (
+
+ {trend.increasing ? 'Trending up' : 'Trending down'} by {trend.percentage.toFixed(1)}% {' '}
+
+
+ )}
+
+ Showing daily sales and revenue over the event's sales period
+
+
+
+
+
+ );
+};
diff --git a/src/app/manage/organization/[organization_id]/event/[eventId]/analytics/_components/EventAnalyticsView.tsx b/src/app/manage/organization/[organization_id]/event/[eventId]/analytics/_components/EventAnalyticsView.tsx
index d13393a..8baa5a3 100644
--- a/src/app/manage/organization/[organization_id]/event/[eventId]/analytics/_components/EventAnalyticsView.tsx
+++ b/src/app/manage/organization/[organization_id]/event/[eventId]/analytics/_components/EventAnalyticsView.tsx
@@ -12,33 +12,65 @@ import {AnalyticsCard} from "./AnalyticsCard";
import {
TierDistributionChart
} from "@/app/manage/organization/[organization_id]/event/[eventId]/analytics/_components/TierDistribution";
+import { DailySalesChart } from "./DailySalesChart";
+import { DailySalesMetrics, TierSalesMetrics } from "@/lib/actions/analyticsActions";
interface EventAnalyticsViewProps {
analytics: EventAnalytics;
+ revenueAnalytics?: {
+ total_revenue: number;
+ total_before_discounts: number;
+ total_tickets_sold: number;
+ daily_sales: DailySalesMetrics[];
+ sales_by_tier?: TierSalesMetrics[];
+ };
isGaLoading?: boolean;
+ isRevenueLoading?: boolean;
}
-export const EventAnalyticsView: React.FC = ({analytics, isGaLoading = false}) => {
+export const EventAnalyticsView: React.FC = ({
+ analytics,
+ revenueAnalytics,
+ isGaLoading = false,
+ isRevenueLoading = false
+}) => {
+ // Calculate average revenue per ticket
+ const avgRevenuePerTicket = revenueAnalytics && revenueAnalytics.total_tickets_sold > 0
+ ? revenueAnalytics.total_revenue / revenueAnalytics.total_tickets_sold
+ : 0;
+
+ // Calculate discount savings
+ const totalDiscountSavings = revenueAnalytics
+ ? revenueAnalytics.total_before_discounts - revenueAnalytics.total_revenue
+ : 0;
+
+ const total_before_discounts = revenueAnalytics?.total_before_discounts || analytics.totalRevenue + totalDiscountSavings;
+
return (
{/* Top Level KPI Cards */}
-
- {/* Revenue Performance Card */}
+
+ {/* Revenue & Discount Card */}
0
+ ? `${((totalDiscountSavings / total_before_discounts) * 100).toFixed(1)}% discount savings`
+ : "No discounts applied"}
icon={
}
+ isLoading={isRevenueLoading}
/>
{/* Sales Performance Card */}
= ({analytics
}
+ isLoading={isRevenueLoading}
/>
{/* Audience Engagement Card */}
@@ -63,44 +96,38 @@ export const EventAnalyticsView: React.FC
= ({analytics
{/* Detailed Chart Grid */}
-
+
+ {/* Daily Sales Chart - Spanning full width */}
+ {revenueAnalytics?.daily_sales && revenueAnalytics.daily_sales.length > 0 && (
+
+ {isRevenueLoading ? (
+
+ ) : (
+
+ )}
+
+ )}
+
+ {/* Tier Charts */}
-
-
+
-
+ {/* Google Analytics Charts */}
{isGaLoading ? (
<>
+
>
-
) : (
<>
{analytics.viewsTimeSeries && }
{analytics.deviceBreakdown && }
- >
- )}
-
-
- {/* GA Insights Grid */}
-
- {isGaLoading ? (
- <>
-
-
- >
- ) : (
- <>
{analytics.trafficSources &&
}
- {analytics.audienceGeography &&
-
- }
+ {analytics.audienceGeography && }
>
- )
- }
+ )}
- )
- ;
-};
\ No newline at end of file
+ );
+};
diff --git a/src/app/manage/organization/[organization_id]/event/[eventId]/analytics/_components/SessionAnalyticsView.tsx b/src/app/manage/organization/[organization_id]/event/[eventId]/analytics/_components/SessionAnalyticsView.tsx
index d79edbe..53fd48f 100644
--- a/src/app/manage/organization/[organization_id]/event/[eventId]/analytics/_components/SessionAnalyticsView.tsx
+++ b/src/app/manage/organization/[organization_id]/event/[eventId]/analytics/_components/SessionAnalyticsView.tsx
@@ -1,29 +1,53 @@
import {SessionAnalytics} from "@/types/eventAnalytics";
import {formatCurrency, formatDate, formatDateTimeShort, formatISODuration} from "@/lib/utils";
import {AnalyticsCard} from "./AnalyticsCard";
-import {ArrowLeft, Clock, DollarSign, Ticket} from "lucide-react";
+import {Clock, DollarSign, Ticket} from "lucide-react";
import {TierSalesChart} from "./TierSalesChart";
import {BlockOccupancyChart} from "./BlockOccupancyChart";
import {SessionStatusBadge} from "@/components/SessionStatusBadge";
-import Link from "next/link";
import {usePathname} from 'next/navigation';
import {SeatStatusChart} from "./SeatStatusChart";
+import {DailySalesChart} from "./DailySalesChart";
+import {Skeleton} from "@/components/ui/skeleton";
import {
TierDistributionChart
} from "@/app/manage/organization/[organization_id]/event/[eventId]/analytics/_components/TierDistribution";
-export const SessionAnalyticsView: React.FC<{ analytics: SessionAnalytics }> = ({analytics}) => {
+export const SessionAnalyticsView: React.FC<{
+ analytics: SessionAnalytics,
+ sessionAnalytics?: {
+ total_revenue: number;
+ total_before_discounts: number;
+ total_tickets_sold: number;
+ daily_sales: Array<{date: string; revenue: number; tickets_sold: number}>;
+ sales_by_tier?: Array<{
+ tier_id: string;
+ tier_name: string;
+ tier_color: string;
+ tickets_sold: number;
+ revenue: number;
+ }>;
+ },
+ isLoading?: boolean
+}> = ({analytics, sessionAnalytics, isLoading = false}) => {
const pathname = usePathname();
const eventAnalyticsPath = pathname.substring(0, pathname.lastIndexOf('/'));
+
+ // Calculate average revenue per ticket
+ const avgRevenuePerTicket = analytics.ticketsSold > 0
+ ? analytics.sessionRevenue / analytics.ticketsSold
+ : 0;
+
+ // Calculate discount savings if we have session analytics data
+ const totalDiscountSavings = sessionAnalytics
+ ? sessionAnalytics.total_before_discounts - sessionAnalytics.total_revenue
+ : 0;
+
+ const total_before_discounts = sessionAnalytics?.total_before_discounts || analytics.sessionRevenue + totalDiscountSavings;
return (
-
+
-
-
- Back to Event Overview
-
{analytics.eventTitle}
@@ -33,45 +57,68 @@ export const SessionAnalyticsView: React.FC<{ analytics: SessionAnalytics }> = (
- {/* ++ Metric Cards consolidated into a 3-column layout ++ */}
-
+ {/* Top Level KPI Cards */}
+
+ {/* Revenue & Discount Card */}
0
+ ? `${((totalDiscountSavings / total_before_discounts) * 100).toFixed(1)}% discount savings`
+ : "No discounts applied"}
icon={
-
+
}
+ isLoading={isLoading}
/>
+
+ {/* Sales Performance Card */}
-
+
}
-
+ isLoading={isLoading}
/>
+
+ {/* Sales Window Card */}
-
+
}
/>
+ {/* Daily Sales Chart */}
+ {sessionAnalytics?.daily_sales && sessionAnalytics.daily_sales.length > 0 && (
+
+ {isLoading ? (
+
+ ) : (
+
+
+
+ )}
+
+ )}
+
{/* Charts */}
-
+
diff --git a/src/app/manage/organization/[organization_id]/event/[eventId]/analytics/_components/SessionPerformanceCard.tsx b/src/app/manage/organization/[organization_id]/event/[eventId]/analytics/_components/SessionPerformanceCard.tsx
new file mode 100644
index 0000000..9e846b2
--- /dev/null
+++ b/src/app/manage/organization/[organization_id]/event/[eventId]/analytics/_components/SessionPerformanceCard.tsx
@@ -0,0 +1,142 @@
+"use client"
+
+import React from 'react';
+import { Card, CardContent, CardHeader } from "@/components/ui/card";
+import { Button } from "@/components/ui/button";
+import { format, parseISO } from "date-fns";
+import { formatCurrency } from "@/lib/utils";
+import { ArrowUpRightIcon, ClockIcon, MapPinIcon, UsersIcon } from "lucide-react";
+import { SessionSummary } from "@/types/eventAnalytics";
+import { SessionSummary as OrderSessionSummary } from "@/lib/actions/analyticsActions";
+import { useRouter } from "next/navigation";
+import { SessionDetailDTO } from "@/lib/validators/event";
+import { SessionStatusBadge } from "@/components/SessionStatusBadge";
+import Link from "next/link";
+
+interface SessionPerformanceCardProps {
+ session: SessionSummary;
+ sessionRevenue?: OrderSessionSummary;
+ sessionMetadata?: SessionDetailDTO;
+ organizationId: string;
+ eventId: string;
+}
+
+export const SessionPerformanceCard = ({
+ session,
+ sessionRevenue,
+ sessionMetadata,
+ organizationId,
+ eventId
+}: SessionPerformanceCardProps) => {
+ const router = useRouter();
+
+ // Basic session info
+ const sessionId = session.sessionId;
+ const startTime = parseISO(session.startTime);
+ const endTime = parseISO(session.endTime);
+ const duration = (endTime.getTime() - startTime.getTime()) / (1000 * 60); // Duration in minutes
+
+ // Revenue info
+ const revenue = session.sessionRevenue;
+ const totalRevenue = sessionRevenue?.total_revenue || revenue;
+ const beforeDiscounts = sessionRevenue?.total_before_discounts;
+ const discountAmount = beforeDiscounts ? beforeDiscounts - totalRevenue : 0;
+
+ // Capacity info
+ const capacity = session.sessionCapacity;
+ const ticketsSold = session.ticketsSold;
+ const sellOutPercentage = session.sellOutPercentage;
+
+ // Session metadata (from event context)
+ const isOnline = sessionMetadata?.sessionType === "ONLINE";
+ const location = isOnline
+ ? "Online Event"
+ : sessionMetadata?.venueDetails?.name || "Venue not specified";
+
+ const handleViewDetails = () => {
+ router.push(`/manage/organization/${organizationId}/event/${eventId}/sessions/${sessionId}/analytics`);
+ };
+
+ return (
+
+
+
+
+
+
+ {format(startTime, "EEE, MMM d")}
+ •
+ {format(startTime, "h:mm a")} - {format(endTime, "h:mm a")}
+
+
+
+
+
+
+
+
+
+
+
Session Info
+
+
+
+ {location}
+
+
+
+ {Math.floor(duration / 60)}h {duration % 60}m
+
+
+
+ {capacity} capacity
+
+
+
+
+
+
Sales Performance
+
+
+ Revenue
+ {formatCurrency(totalRevenue, 'LKR', 'en-LK')}
+
+
+ {beforeDiscounts && (
+
+ Discounts
+ -{formatCurrency(discountAmount, 'LKR', 'en-LK')}
+
+ )}
+
+
+ Tickets sold
+ {ticketsSold} / {capacity}
+
+
+
+
+
+ {/* Sell-out progress bar */}
+
+
+ Sell-out Progress
+ {sellOutPercentage.toFixed(0)}%
+
+
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/src/app/manage/organization/[organization_id]/event/[eventId]/analytics/_components/SessionPerformanceGrid.tsx b/src/app/manage/organization/[organization_id]/event/[eventId]/analytics/_components/SessionPerformanceGrid.tsx
new file mode 100644
index 0000000..bac9940
--- /dev/null
+++ b/src/app/manage/organization/[organization_id]/event/[eventId]/analytics/_components/SessionPerformanceGrid.tsx
@@ -0,0 +1,83 @@
+"use client"
+
+import React, { useEffect, useState } from 'react';
+import { SessionSummary } from "@/types/eventAnalytics";
+import { SessionPerformanceCard } from "./SessionPerformanceCard";
+import { SessionSummary as OrderSessionSummary, getSessionsRevenueAnalytics } from "@/lib/actions/analyticsActions";
+import { useParams } from "next/navigation";
+import { useEventContext } from "@/providers/EventProvider";
+import { Skeleton } from "@/components/ui/skeleton";
+
+interface SessionPerformanceGridProps {
+ sessions: SessionSummary[];
+ isLoading: boolean;
+}
+
+export const SessionPerformanceGrid = ({
+ sessions,
+ isLoading
+}: SessionPerformanceGridProps) => {
+ const [sessionRevenueData, setSessionRevenueData] = useState([]);
+ const [isRevenueLoading, setIsRevenueLoading] = useState(true);
+ const params = useParams();
+ const { event } = useEventContext();
+
+ const organizationId = params.organization_id as string;
+ const eventId = params.eventId as string;
+
+ // Fetch revenue data for sessions
+ useEffect(() => {
+ const fetchRevenueData = async () => {
+ if (eventId) {
+ try {
+ setIsRevenueLoading(true);
+ const data = await getSessionsRevenueAnalytics(eventId);
+ setSessionRevenueData(data.sessions);
+ } catch (error) {
+ console.error("Error fetching session revenue data:", error);
+ } finally {
+ setIsRevenueLoading(false);
+ }
+ }
+ };
+
+ fetchRevenueData();
+ }, [eventId]);
+
+ // Match session revenue data with session metadata from event context
+ const getSessionRevenue = (sessionId: string) => {
+ return sessionRevenueData.find(s => s.session_id === sessionId);
+ };
+
+ const getSessionMetadata = (sessionId: string) => {
+ return event?.sessions?.find(s => s.id === sessionId);
+ };
+
+ if (isLoading) {
+ return (
+
+ {[1, 2, 3, 4, 5, 6].map((_, index) => (
+
+ ))}
+
+ );
+ }
+
+ return (
+
+
Session Performance
+
+ {sessions.map(session => (
+
+ ))}
+
+
+ );
+};
\ No newline at end of file
diff --git a/src/app/manage/organization/[organization_id]/event/[eventId]/analytics/_components/SessionTableColumnFactory.tsx b/src/app/manage/organization/[organization_id]/event/[eventId]/analytics/_components/SessionTableColumnFactory.tsx
new file mode 100644
index 0000000..546b951
--- /dev/null
+++ b/src/app/manage/organization/[organization_id]/event/[eventId]/analytics/_components/SessionTableColumnFactory.tsx
@@ -0,0 +1,86 @@
+"use client"
+
+import {ColumnDef} from "@tanstack/react-table";
+import {SessionSummary} from "@/types/eventAnalytics";
+import {format, parseISO} from "date-fns";
+import {formatCurrency} from "@/lib/utils";
+import {ArrowUpDown} from "lucide-react";
+import {Button} from "@/components/ui/button";
+import Link from "next/link";
+import {SessionStatusBadge} from "@/components/SessionStatusBadge";
+import {useParams} from "next/navigation";
+
+export function useSessionColumns(): ColumnDef[] {
+ // Get organization and event ID from params
+ const params = useParams();
+ const organizationId = params.organization_id as string;
+ const eventId = params.eventId as string;
+
+ return [
+ {
+ accessorKey: "startTime",
+ header: "Session Date",
+ cell: ({row}) => {
+ const session = row.original;
+ // Using params from the context to build the absolute URL
+ return (
+
+ {format(parseISO(session.startTime), "PPp")}
+
+ );
+ },
+ },
+ {
+ accessorKey: "sessionStatus",
+ header: "Status",
+ cell: ({row}) => ,
+ },
+ {
+ accessorKey: "ticketsSold",
+ header: ({column}) => (
+
+ ),
+ cell: ({row}) => {
+ const {ticketsSold, sessionCapacity} = row.original;
+ return {`${ticketsSold} / ${sessionCapacity}`}
;
+ }
+ },
+ {
+ accessorKey: "sessionRevenue",
+ header: ({column}) => (
+
+
+
+ ),
+ cell: ({row}) => {formatCurrency(row.original.sessionRevenue, 'LKR', 'en-LK')}
,
+ },
+ {
+ accessorKey: "sellOutPercentage",
+ header: "Sell-Out Progress",
+ cell: ({row}) => {
+ const percentage = row.original.sellOutPercentage;
+ return (
+
+
+
{percentage.toFixed(0)}%
+
+ );
+ }
+ },
+ ];
+}
\ No newline at end of file
diff --git a/src/app/manage/organization/[organization_id]/event/[eventId]/analytics/_components/SessionTableColumns.tsx b/src/app/manage/organization/[organization_id]/event/[eventId]/analytics/_components/SessionTableColumns.tsx
index 1054300..d3b1846 100644
--- a/src/app/manage/organization/[organization_id]/event/[eventId]/analytics/_components/SessionTableColumns.tsx
+++ b/src/app/manage/organization/[organization_id]/event/[eventId]/analytics/_components/SessionTableColumns.tsx
@@ -9,6 +9,9 @@ import {Button} from "@/components/ui/button";
import Link from "next/link";
import {SessionStatusBadge} from "@/components/SessionStatusBadge";
+// This file is deprecated - see SessionTableColumnFactory instead
+// Keeping this file to avoid breaking existing imports
+
export const columns: ColumnDef[] = [
{
accessorKey: "startTime",
@@ -16,7 +19,7 @@ export const columns: ColumnDef[] = [
cell: ({row}) => {
const session = row.original;
return (
-
{format(parseISO(session.startTime), "PPp")}
diff --git a/src/app/manage/organization/[organization_id]/event/[eventId]/analytics/_components/TierSalesChart.tsx b/src/app/manage/organization/[organization_id]/event/[eventId]/analytics/_components/TierSalesChart.tsx
index 0012c28..fef9d0a 100644
--- a/src/app/manage/organization/[organization_id]/event/[eventId]/analytics/_components/TierSalesChart.tsx
+++ b/src/app/manage/organization/[organization_id]/event/[eventId]/analytics/_components/TierSalesChart.tsx
@@ -2,6 +2,7 @@
import React, {useState, useMemo} from "react";
import {TierSales} from "@/types/eventAnalytics";
+import { TierSalesMetrics } from "@/lib/actions/analyticsActions";
import {
Card,
CardContent,
@@ -20,10 +21,34 @@ import {formatCurrency} from "@/lib/utils";
import {ToggleGroup, ToggleGroupItem} from "@/components/ui/toggle-group";
import {DollarSign, Ticket} from "lucide-react";
-export const TierSalesChart: React.FC<{ data: TierSales[] }> = ({data}) => {
+export const TierSalesChart: React.FC<{ data: TierSales[] | TierSalesMetrics[] }> = ({data}) => {
const [mode, setMode] = useState<"revenue" | "tickets">("revenue");
- const chartConfig = data.reduce(
+ // Process data to normalize between TierSales and TierSalesMetrics
+ const processedData = useMemo(() => {
+ return data.map(tier => {
+ // Check if it's a TierSalesMetrics object by checking for tier_id property
+ if ('tier_id' in tier) {
+ return {
+ tierId: tier.tier_id,
+ tierName: tier.tier_name,
+ tierColor: tier.tier_color,
+ ticketsSold: tier.tickets_sold,
+ revenue: tier.revenue
+ };
+ }
+ // It's already in TierSales format
+ return {
+ tierId: tier.tierId,
+ tierName: tier.tierName,
+ tierColor: tier.tierColor,
+ ticketsSold: tier.ticketsSold,
+ revenue: tier.totalRevenue
+ };
+ });
+ }, [data]);
+
+ const chartConfig = processedData.reduce(
(acc, tier) => {
acc[tier.tierName] = {
label: tier.tierName,
@@ -35,12 +60,12 @@ export const TierSalesChart: React.FC<{ data: TierSales[] }> = ({data}) => {
);
const totalValue = useMemo(() => {
- return data.reduce(
+ return processedData.reduce(
(acc, curr) =>
- acc + (mode === "revenue" ? curr.totalRevenue : curr.ticketsSold),
+ acc + (mode === "revenue" ? curr.revenue : curr.ticketsSold),
0
);
- }, [data, mode]);
+ }, [processedData, mode]);
return (
@@ -89,13 +114,13 @@ export const TierSalesChart: React.FC<{ data: TierSales[] }> = ({data}) => {
content={}
/>
- {data.map((entry) => (
+ {processedData.map((entry) => (
))}
|