Update favicon and site title

This commit is contained in:
CycroftX
2026-02-03 20:30:11 +05:30
parent 61423f951a
commit cbc7cd1bd3
19 changed files with 3276 additions and 42 deletions

View File

@@ -4,19 +4,18 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- TODO: Set the document title to the name of your application -->
<title>Lovable App</title>
<meta name="description" content="Lovable Generated Project" />
<meta name="author" content="Lovable" />
<title>Eventify Command Center</title>
<meta name="description" content="Eventify Command Center Admin Panel" />
<meta name="author" content="Eventify" />
<!-- TODO: Update og:title to match your application name -->
<meta property="og:title" content="Lovable App" />
<meta property="og:description" content="Lovable Generated Project" />
<meta property="og:title" content="Eventify Command Center" />
<meta property="og:description" content="Eventify Command Center Admin Panel" />
<meta property="og:type" content="website" />
<meta property="og:image" content="https://lovable.dev/opengraph-image-p98pqg.png" />
<meta property="og:image" content="/og-image.png" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:site" content="@Lovable" />
<meta name="twitter:image" content="https://lovable.dev/opengraph-image-p98pqg.png" />
<meta name="twitter:site" content="@Eventify" />
<meta name="twitter:image" content="/og-image.png" />
</head>
<body>

1548
package-lock.json generated

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

@@ -2,9 +2,13 @@ import { Toaster } from "@/components/ui/toaster";
import { Toaster as Sonner } from "@/components/ui/sonner";
import { TooltipProvider } from "@/components/ui/tooltip";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { BrowserRouter, Routes, Route } from "react-router-dom";
import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom";
import { AuthProvider } from "@/contexts/AuthContext";
import { ProtectedRoute } from "@/components/auth/ProtectedRoute";
import Login from "./pages/Login";
import Dashboard from "./pages/Dashboard";
import Partners from "./pages/Partners";
import PartnerDirectory from "./features/partners/PartnerDirectory";
import PartnerProfile from "./features/partners/PartnerProfile";
import Events from "./pages/Events";
import Users from "./pages/Users";
import Financials from "./pages/Financials";
@@ -19,16 +23,69 @@ const App = () => (
<Toaster />
<Sonner />
<BrowserRouter>
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/partners" element={<Partners />} />
<Route path="/events" element={<Events />} />
<Route path="/users" element={<Users />} />
<Route path="/financials" element={<Financials />} />
<Route path="/settings" element={<Settings />} />
{/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */}
<Route path="*" element={<NotFound />} />
</Routes>
<AuthProvider>
<Routes>
<Route path="/login" element={<Login />} />
<Route
path="/"
element={
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
}
/>
<Route
path="/partners"
element={
<ProtectedRoute>
<PartnerDirectory />
</ProtectedRoute>
}
/>
<Route
path="/partners/:id"
element={
<ProtectedRoute>
<PartnerProfile />
</ProtectedRoute>
}
/>
<Route
path="/events"
element={
<ProtectedRoute>
<Events />
</ProtectedRoute>
}
/>
<Route
path="/users"
element={
<ProtectedRoute>
<Users />
</ProtectedRoute>
}
/>
<Route
path="/financials"
element={
<ProtectedRoute>
<Financials />
</ProtectedRoute>
}
/>
<Route
path="/settings"
element={
<ProtectedRoute>
<Settings />
</ProtectedRoute>
}
/>
{/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */}
<Route path="*" element={<NotFound />} />
</Routes>
</AuthProvider>
</BrowserRouter>
</TooltipProvider>
</QueryClientProvider>

View File

@@ -0,0 +1,27 @@
import { Navigate } from 'react-router-dom';
import { useAuth } from '@/contexts/AuthContext';
interface ProtectedRouteProps {
children: React.ReactNode;
}
export const ProtectedRoute: React.FC<ProtectedRouteProps> = ({ children }) => {
const { isAuthenticated, isLoading } = useAuth();
if (isLoading) {
return (
<div className="min-h-screen flex items-center justify-center bg-background">
<div className="text-center space-y-4">
<div className="h-12 w-12 border-4 border-accent border-t-transparent rounded-full animate-spin mx-auto" />
<p className="text-muted-foreground">Loading...</p>
</div>
</div>
);
}
if (!isAuthenticated) {
return <Navigate to="/login" replace />;
}
return <>{children}</>;
};

View File

@@ -1,5 +1,14 @@
import { Search, Bell, ChevronDown } from 'lucide-react';
import { Search, Bell, ChevronDown, LogOut } from 'lucide-react';
import { cn } from '@/lib/utils';
import { useAuth } from '@/contexts/AuthContext';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
interface TopBarProps {
title: string;
@@ -7,6 +16,22 @@ interface TopBarProps {
}
export function TopBar({ title, description }: TopBarProps) {
const { user, logout } = useAuth();
const getInitials = () => {
if (user?.first_name && user?.last_name) {
return `${user.first_name[0]}${user.last_name[0]}`.toUpperCase();
}
return user?.username?.substring(0, 2).toUpperCase() || 'AD';
};
const getDisplayName = () => {
if (user?.first_name && user?.last_name) {
return `${user.first_name} ${user.last_name}`;
}
return user?.username || 'Admin User';
};
return (
<header className="sticky top-0 z-30 flex h-20 items-center justify-between bg-background/80 backdrop-blur-sm px-8 border-b border-border/30">
{/* Page Title */}
@@ -47,18 +72,31 @@ export function TopBar({ title, description }: TopBarProps) {
</span>
</button>
{/* Profile */}
<button className="flex items-center gap-3 pl-4 pr-3 py-2 rounded-xl neu-button">
<div className="h-8 w-8 rounded-lg bg-primary shadow-neu-inset-sm flex items-center justify-center">
<span className="text-sm font-bold text-primary-foreground">AS</span>
</div>
<div className="text-left">
<p className="text-sm font-medium text-foreground">Admin User</p>
<p className="text-xs text-muted-foreground">Super Admin</p>
</div>
<ChevronDown className="h-4 w-4 text-muted-foreground" />
</button>
{/* Profile Dropdown */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button className="flex items-center gap-3 pl-4 pr-3 py-2 rounded-xl neu-button">
<div className="h-8 w-8 rounded-lg bg-primary shadow-neu-inset-sm flex items-center justify-center">
<span className="text-sm font-bold text-primary-foreground">{getInitials()}</span>
</div>
<div className="text-left">
<p className="text-sm font-medium text-foreground">{getDisplayName()}</p>
<p className="text-xs text-muted-foreground">Admin</p>
</div>
<ChevronDown className="h-4 w-4 text-muted-foreground" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56">
<DropdownMenuLabel>My Account</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => logout()} className="text-error cursor-pointer">
<LogOut className="mr-2 h-4 w-4" />
<span>Log out</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</header>
);
}

View File

@@ -0,0 +1,124 @@
import React, { createContext, useContext, useState, useEffect } from 'react';
import { login as authLogin, logout as authLogout, checkUserStatus, getStoredAuth, storeAuth, clearAuth, AuthUser, AuthError } from '@/services/auth';
import { useToast } from '@/hooks/use-toast';
interface AuthContextType {
user: AuthUser | null;
isAuthenticated: boolean;
isLoading: boolean;
login: (username: string, password: string) => Promise<void>;
logout: () => Promise<void>;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export const useAuth = () => {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
};
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [user, setUser] = useState<AuthUser | null>(null);
const [isLoading, setIsLoading] = useState(true);
const { toast } = useToast();
// Check for existing auth on mount
useEffect(() => {
const checkAuth = async () => {
const storedAuth = getStoredAuth();
if (storedAuth) {
try {
// Verify token is still valid
await checkUserStatus(storedAuth.username, storedAuth.token);
setUser(storedAuth);
} catch (error) {
console.error('Auth verification failed:', error);
if (error instanceof AuthError && error.isInvalidToken) {
clearAuth();
toast({
title: 'Session Expired',
description: 'Your session has expired. Please login again.',
variant: 'destructive',
});
}
}
}
setIsLoading(false);
};
checkAuth();
}, []);
const login = async (username: string, password: string) => {
try {
setIsLoading(true);
const response = await authLogin(username, password);
// Store auth data
storeAuth(response);
// Set user state
const authUser: AuthUser = {
username: response.username,
token: response.token,
first_name: response.user?.first_name,
last_name: response.user?.last_name,
email: response.user?.email,
profile_photo: response.user?.profile_photo,
};
setUser(authUser);
toast({
title: 'Login Successful',
description: `Welcome back, ${username}!`,
});
} catch (error) {
console.error('Login error:', error);
const errorMessage = error instanceof Error ? error.message : 'Login failed';
toast({
title: 'Login Failed',
description: errorMessage,
variant: 'destructive',
});
throw error;
} finally {
setIsLoading(false);
}
};
const logout = async () => {
try {
if (user) {
await authLogout(user.username, user.token);
}
} catch (error) {
console.error('Logout error:', error);
} finally {
clearAuth();
setUser(null);
toast({
title: 'Logged Out',
description: 'You have been successfully logged out.',
});
}
};
const value: AuthContextType = {
user,
isAuthenticated: !!user,
isLoading,
login,
logout,
};
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
};

181
src/data/mockPartnerData.ts Normal file
View File

@@ -0,0 +1,181 @@
import { Partner, DealTerm, LedgerEntry, PartnerDocument } from '../types/partner';
import { subDays, subMonths } from 'date-fns';
export const mockPartners: Partner[] = [
{
id: 'p1',
name: 'Neon Arena',
type: 'Venue',
status: 'Active',
logo: 'https://ui-avatars.com/api/?name=Neon+Arena&background=0D8ABC&color=fff',
primaryContact: {
name: 'Alex Rivera',
email: 'alex@neonarena.com',
phone: '+91 98765 43210',
role: 'Venue Manager',
},
metrics: {
activeDeals: 2,
totalRevenue: 4500000,
openBalance: 125000,
lastActivity: new Date().toISOString(),
eventsCount: 12,
},
tags: ['Premium', 'Indoor', 'Capacity: 5000'],
joinedAt: subMonths(new Date(), 6).toISOString(),
},
{
id: 'p2',
name: 'TopTier Promoters',
type: 'Promoter',
status: 'Active',
logo: 'https://ui-avatars.com/api/?name=Top+Tier&background=F59E0B&color=fff',
primaryContact: {
name: 'Sarah Chen',
email: 'sarah@toptier.com',
role: 'Head of Marketing',
},
metrics: {
activeDeals: 5,
totalRevenue: 850000,
openBalance: 45000,
lastActivity: subDays(new Date(), 2).toISOString(),
eventsCount: 8,
},
tags: ['Influencer Network', 'Social Media'],
joinedAt: subMonths(new Date(), 3).toISOString(),
},
{
id: 'p3',
name: 'TechFlow Solutions',
type: 'Vendor',
status: 'Suspended',
logo: 'https://ui-avatars.com/api/?name=Tech+Flow&background=EF4444&color=fff',
primaryContact: {
name: 'Mike Ross',
email: 'mike@techflow.io',
role: 'Operations',
},
metrics: {
activeDeals: 0,
totalRevenue: 120000,
openBalance: 0,
lastActivity: subMonths(new Date(), 1).toISOString(),
eventsCount: 3,
},
tags: ['AV Equipment', 'Lighting'],
notes: 'Suspended due to breach of contract on Event #402',
joinedAt: subMonths(new Date(), 8).toISOString(),
},
{
id: 'p4',
name: 'Global Sponsors Inc',
type: 'Sponsor',
status: 'Invited',
logo: 'https://ui-avatars.com/api/?name=Global+Sponsors&background=10B981&color=fff',
primaryContact: {
name: 'Jessica Pearson',
email: 'jessica@globalsponsors.com',
role: 'Brand Director',
},
metrics: {
activeDeals: 0,
totalRevenue: 0,
openBalance: 0,
lastActivity: subDays(new Date(), 5).toISOString(),
eventsCount: 0,
},
tags: ['Corporate', 'High Value'],
joinedAt: subDays(new Date(), 5).toISOString(),
}
];
export const mockDealTerms: DealTerm[] = [
{
id: 'dt1',
partnerId: 'p1',
type: 'RevenueShare',
name: 'Standard Venue Split',
params: {
percentage: 15,
currency: 'INR',
conditions: 'Net revenue after tax and platform fees',
},
effectiveFrom: subMonths(new Date(), 6).toISOString(),
status: 'Active',
version: 1,
},
{
id: 'dt2',
partnerId: 'p2',
type: 'CommissionPerTicket',
name: 'Promoter Commission',
params: {
amount: 150,
currency: 'INR',
},
effectiveFrom: subMonths(new Date(), 3).toISOString(),
status: 'Active',
version: 2,
}
];
export const mockLedger: LedgerEntry[] = [
{
id: 'le1',
partnerId: 'p1',
eventId: 'evt_123',
type: 'Credit',
description: 'Revenue Share - Neon Nights Event',
amount: 75000,
currency: 'INR',
createdAt: subDays(new Date(), 2).toISOString(),
status: 'Pending',
},
{
id: 'le2',
partnerId: 'p1',
type: 'Payout',
description: 'Monthly Settlement - Jan 2026',
amount: -50000,
currency: 'INR',
referenceId: 'TXN_987654',
createdAt: subDays(new Date(), 10).toISOString(),
status: 'Cleared',
},
{
id: 'le3',
partnerId: 'p2',
eventId: 'evt_124',
type: 'Credit',
description: 'Ticket Commission - Summer Fest',
amount: 12500,
currency: 'INR',
createdAt: subDays(new Date(), 1).toISOString(),
status: 'Pending',
}
];
export const mockDocuments: PartnerDocument[] = [
{
id: 'doc1',
partnerId: 'p1',
type: 'Contract',
name: 'Venue Agreement 2026',
url: '#',
status: 'Signed',
uploadedBy: 'Admin User',
uploadedAt: subMonths(new Date(), 6).toISOString(),
expiresAt: subMonths(new Date(), -6).toISOString(),
},
{
id: 'doc2',
partnerId: 'p1',
type: 'Tax',
name: 'GST Registration',
url: '#',
status: 'Verified',
uploadedBy: 'Alex Rivera',
uploadedAt: subMonths(new Date(), 6).toISOString(),
}
];

View File

@@ -0,0 +1,47 @@
import { useState } from 'react';
import { AppLayout } from '@/components/layout/AppLayout';
import { PartnerFilters } from './components/PartnerFilters';
import { PartnerCard } from './components/PartnerCard';
import { mockPartners } from '@/data/mockPartnerData';
export default function PartnerDirectory() {
const [searchQuery, setSearchQuery] = useState('');
const filteredPartners = mockPartners.filter(partner =>
partner.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
partner.type.toLowerCase().includes(searchQuery.toLowerCase()) ||
partner.primaryContact.name.toLowerCase().includes(searchQuery.toLowerCase())
);
return (
<AppLayout title="Partners">
<div className="p-6 max-w-[1600px] mx-auto space-y-8">
<div className="flex flex-col gap-2">
<h1 className="text-3xl font-bold tracking-tight">Partner Management</h1>
<p className="text-muted-foreground">Manage your relationships with venues, promoters, sponsors, and vendors.</p>
</div>
<div className="bg-card/30 p-1 rounded-xl glass-panel">
<PartnerFilters
onSearch={setSearchQuery}
onFilter={() => console.log('Filter clicked')}
onAdd={() => console.log('Add clicked')}
/>
</div>
{filteredPartners.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
{filteredPartners.map(partner => (
<PartnerCard key={partner.id} partner={partner} />
))}
</div>
) : (
<div className="text-center py-20 bg-card/20 rounded-xl border border-dashed border-border/50">
<h3 className="text-lg font-medium text-foreground">No partners found</h3>
<p className="text-muted-foreground mt-2">Try adjusting your search or filters</p>
</div>
)}
</div>
</AppLayout>
);
}

View File

@@ -0,0 +1,277 @@
import { useParams } from 'react-router-dom';
import { AppLayout } from '@/components/layout/AppLayout';
import { mockPartners, mockDealTerms, mockLedger, mockDocuments } from '@/data/mockPartnerData';
import { StatusBadge, TypeBadge } from './components/PartnerBadges';
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Button } from '@/components/ui/button';
import { Calendar, Download, Edit, FileText, Mail, Phone, ExternalLink, Wallet } from 'lucide-react';
import { Badge } from '@/components/ui/badge';
import { cn } from '@/lib/utils';
export default function PartnerProfile() {
const { id } = useParams<{ id: string }>();
// In a real app, fetch data based on ID
const partner = mockPartners.find(p => p.id === id) || mockPartners[0];
const dealTerms = mockDealTerms.filter(dt => dt.partnerId === partner.id);
const ledger = mockLedger.filter(l => l.partnerId === partner.id);
const documents = mockDocuments.filter(d => d.partnerId === partner.id);
if (!partner) return <div>Partner not found</div>;
return (
<AppLayout title={partner.name}>
<div className="p-6 max-w-[1600px] mx-auto space-y-6">
{/* Header Profile */}
<div className="relative overflow-hidden rounded-2xl bg-card border border-border/50 shadow-lg group">
<div className="absolute inset-0 bg-gradient-to-r from-accent/5 to-transparent opacity-50" />
<div className="relative p-8 flex flex-col md:flex-row gap-8 items-start">
<div className="h-28 w-28 rounded-2xl bg-secondary flex items-center justify-center overflow-hidden border-2 border-border shadow-2xl">
{partner.logo ? (
<img src={partner.logo} alt={partner.name} className="h-full w-full object-cover" />
) : (
<span className="text-3xl font-bold text-muted-foreground">{partner.name.substring(0, 2)}</span>
)}
</div>
<div className="flex-1 space-y-4">
<div className="flex justify-between items-start">
<div>
<h1 className="text-3xl font-bold tracking-tight text-foreground flex items-center gap-3">
{partner.name}
<StatusBadge status={partner.status} />
</h1>
<div className="flex items-center gap-4 mt-2 text-muted-foreground">
<TypeBadge type={partner.type} />
<span className="flex items-center gap-1 text-sm"><Calendar className="h-4 w-4" /> Joined {new Date(partner.joinedAt).toLocaleDateString()}</span>
</div>
</div>
<div className="flex gap-2">
<Button variant="outline" className="gap-2"><Edit className="h-4 w-4" /> Edit Profile</Button>
<Button className="bg-accent text-white gap-2"><Wallet className="h-4 w-4" /> New Settlement</Button>
</div>
</div>
<div className="flex flex-wrap gap-6 pt-4 border-t border-border/30">
<div className="flex items-center gap-2">
<div className="h-8 w-8 rounded-full bg-secondary/50 flex items-center justify-center text-primary">
<span className="text-xs font-bold">{partner.primaryContact.name.substring(0, 2)}</span>
</div>
<div>
<p className="text-sm font-medium">{partner.primaryContact.name}</p>
<p className="text-xs text-muted-foreground">{partner.primaryContact.role}</p>
</div>
</div>
<div className="flex items-center gap-4 text-sm text-foreground/80 bg-secondary/30 px-4 py-2 rounded-lg border border-border/30">
<a href={`mailto:${partner.primaryContact.email}`} className="flex items-center gap-2 hover:text-accent"><Mail className="h-4 w-4" /> {partner.primaryContact.email}</a>
{partner.primaryContact.phone && (
<span className="flex items-center gap-2 border-l border-border pl-4"><Phone className="h-4 w-4" /> {partner.primaryContact.phone}</span>
)}
</div>
</div>
</div>
</div>
</div>
{/* Quick Stats */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div className="neu-card p-4 flex items-center justify-between">
<div>
<p className="text-xs text-muted-foreground font-medium uppercase tracking-wider">Total Revenue</p>
<p className="text-2xl font-bold mt-1">{partner.metrics.totalRevenue.toLocaleString()}</p>
</div>
<div className="h-10 w-10 rounded-full bg-success/10 flex items-center justify-center text-success">
<Wallet className="h-5 w-5" />
</div>
</div>
<div className="neu-card p-4 flex items-center justify-between">
<div>
<p className="text-xs text-muted-foreground font-medium uppercase tracking-wider">Open Balance</p>
<p className="text-2xl font-bold mt-1">{partner.metrics.openBalance.toLocaleString()}</p>
</div>
<div className="h-10 w-10 rounded-full bg-warning/10 flex items-center justify-center text-warning">
<Wallet className="h-5 w-5" />
</div>
</div>
<div className="neu-card p-4 flex items-center justify-between">
<div>
<p className="text-xs text-muted-foreground font-medium uppercase tracking-wider">Active Deals</p>
<p className="text-2xl font-bold mt-1">{partner.metrics.activeDeals}</p>
</div>
<div className="h-10 w-10 rounded-full bg-primary/10 flex items-center justify-center text-primary">
<FileText className="h-5 w-5" />
</div>
</div>
<div className="neu-card p-4 flex items-center justify-between">
<div>
<p className="text-xs text-muted-foreground font-medium uppercase tracking-wider">Events</p>
<p className="text-2xl font-bold mt-1">{partner.metrics.eventsCount}</p>
</div>
<div className="h-10 w-10 rounded-full bg-accent/10 flex items-center justify-center text-accent">
<Calendar className="h-5 w-5" />
</div>
</div>
</div>
{/* Tabs Content */}
<div className="neu-card min-h-[500px]">
<Tabs defaultValue="overview" className="w-full">
<div className="border-b border-border/40 px-6 pt-4">
<TabsList className="bg-transparent h-auto p-0 gap-6">
<TabsTrigger value="overview" className="tab-trigger pb-4 rounded-none data-[state=active]:border-b-2 data-[state=active]:border-accent data-[state=active]:bg-transparent">Overview</TabsTrigger>
<TabsTrigger value="assignments" className="tab-trigger pb-4 rounded-none data-[state=active]:border-b-2 data-[state=active]:border-accent data-[state=active]:bg-transparent">Assignments</TabsTrigger>
<TabsTrigger value="terms" className="tab-trigger pb-4 rounded-none data-[state=active]:border-b-2 data-[state=active]:border-accent data-[state=active]:bg-transparent">Deal Terms</TabsTrigger>
<TabsTrigger value="finance" className="tab-trigger pb-4 rounded-none data-[state=active]:border-b-2 data-[state=active]:border-accent data-[state=active]:bg-transparent">Financials</TabsTrigger>
<TabsTrigger value="docs" className="tab-trigger pb-4 rounded-none data-[state=active]:border-b-2 data-[state=active]:border-accent data-[state=active]:bg-transparent">Documents</TabsTrigger>
</TabsList>
</div>
<div className="p-6">
<TabsContent value="overview" className="space-y-6">
<div className="grid grid-cols-2 gap-8">
<div className="space-y-4">
<h3 className="font-semibold text-lg">Partner Details</h3>
<div className="grid grid-cols-2 gap-y-4 text-sm">
<span className="text-muted-foreground">Legal Name</span>
<span>{partner.companyDetails?.legalName || partner.name}</span>
<span className="text-muted-foreground">Tax ID</span>
<span>{partner.companyDetails?.taxId || '-'}</span>
<span className="text-muted-foreground">Website</span>
<a href={partner.companyDetails?.website} target="_blank" className="text-accent hover:underline flex items-center gap-1">{partner.companyDetails?.website || '-'} <ExternalLink className="h-3 w-3" /></a>
<span className="text-muted-foreground">Address</span>
<span>{partner.companyDetails?.address || '-'}</span>
</div>
</div>
<div className="space-y-4">
<h3 className="font-semibold text-lg">Tags & Notes</h3>
<div className="flex flex-wrap gap-2">
{partner.tags.map(tag => (
<Badge key={tag} variant="secondary" className="px-3 py-1">{tag}</Badge>
))}
</div>
<div className="p-4 bg-secondary/30 rounded-lg text-sm text-balance">
{partner.notes || "No notes added for this partner."}
</div>
</div>
</div>
</TabsContent>
<TabsContent value="finance">
<div className="flex justify-between items-center mb-6">
<h3 className="font-semibold text-lg">Ledger & Settlements</h3>
<Button variant="outline" size="sm" className="gap-2"><Download className="h-4 w-4" /> Export CSV</Button>
</div>
<div className="border border-border/50 rounded-lg overflow-hidden">
<table className="w-full text-sm text-left">
<thead className="bg-secondary/50 text-muted-foreground">
<tr>
<th className="p-3">Date</th>
<th className="p-3">Description</th>
<th className="p-3">Type</th>
<th className="p-3 text-right">Amount</th>
<th className="p-3 text-center">Status</th>
</tr>
</thead>
<tbody className="divide-y divide-border/30">
{ledger.map(entry => (
<tr key={entry.id} className="hover:bg-accent/5">
<td className="p-3">{new Date(entry.createdAt).toLocaleDateString()}</td>
<td className="p-3">
<div className="font-medium">{entry.description}</div>
{entry.referenceId && <div className="text-xs text-muted-foreground">Ref: {entry.referenceId}</div>}
</td>
<td className="p-3"><Badge variant="outline">{entry.type}</Badge></td>
<td className={cn("p-3 text-right font-medium", entry.amount < 0 ? "text-error" : "text-success")}>
{entry.amount < 0 ? '-' : '+'}{Math.abs(entry.amount).toLocaleString()}
</td>
<td className="p-3 text-center">
<span className={cn("text-xs px-2 py-1 rounded-full border",
entry.status === 'Cleared' ? 'bg-success/10 border-success/20 text-success' :
entry.status === 'Pending' ? 'bg-warning/10 border-warning/20 text-warning' : 'bg-muted border-border'
)}>{entry.status}</span>
</td>
</tr>
))}
</tbody>
</table>
{ledger.length === 0 && <div className="p-8 text-center text-muted-foreground">No transactions found</div>}
</div>
</TabsContent>
<TabsContent value="docs">
<div className="flex justify-between items-center mb-6">
<h3 className="font-semibold text-lg">Contracts & Documents</h3>
<Button variant="outline" size="sm" className="gap-2"><Plus className="h-4 w-4" /> Upload Document</Button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{documents.map(doc => (
<div key={doc.id} className="p-4 border border-border/30 rounded-lg bg-card/50 flex items-start gap-3 hover:border-accent/40 transition-colors">
<div className="h-10 w-10 bg-secondary rounded-lg flex items-center justify-center text-muted-foreground">
<FileText className="h-5 w-5" />
</div>
<div className="flex-1 overflow-hidden">
<p className="font-medium truncate">{doc.name}</p>
<p className="text-xs text-muted-foreground capitalize">{doc.type} {doc.status}</p>
<p className="text-xs text-muted-foreground mt-1">Uploaded {new Date(doc.uploadedAt).toLocaleDateString()}</p>
</div>
<Button variant="ghost" size="icon" className="h-8 w-8"><Download className="h-4 w-4" /></Button>
</div>
))}
</div>
</TabsContent>
<TabsContent value="terms">
<div className="space-y-4">
<div className="flex justify-between items-center">
<h3 className="font-semibold text-lg">Active Deal Terms</h3>
<Button size="sm">Add New Term</Button>
</div>
{dealTerms.map(term => (
<div key={term.id} className="p-4 border border-border/50 rounded-xl bg-gradient-to-br from-card to-secondary/30">
<div className="flex justify-between items-start mb-2">
<div>
<h4 className="font-bold flex items-center gap-2">
{term.name}
<Badge variant="secondary" className="text-xs font-normal">v{term.version}</Badge>
</h4>
<p className="text-sm text-muted-foreground mt-1">Effective from {new Date(term.effectiveFrom).toLocaleDateString()}</p>
</div>
<Badge variant="outline" className="bg-success/5 border-success/20 text-success">{term.status}</Badge>
</div>
<div className="mt-4 p-3 bg-secondary/50 rounded-lg text-sm grid grid-cols-2 gap-4">
<div>
<span className="text-muted-foreground block text-xs uppercase">Type</span>
<span className="font-medium">{term.type}</span>
</div>
<div>
<span className="text-muted-foreground block text-xs uppercase">Parameters</span>
<span className="font-medium">
{term.type === 'RevenueShare' ? `${term.params.percentage}% Share` :
term.type === 'CommissionPerTicket' ? `${term.params.amount} per ticket` : 'Custom'}
</span>
</div>
</div>
</div>
))}
</div>
</TabsContent>
<TabsContent value="assignments">
<div className="p-8 text-center border-2 border-dashed border-border/50 rounded-xl">
<Calendar className="h-12 w-12 text-muted-foreground mx-auto mb-4 opacity-50" />
<h3 className="text-lg font-medium">No event assignments yet</h3>
<p className="text-muted-foreground mb-4">Assign this partner to an upcoming event</p>
<Button>Assign to Event</Button>
</div>
</TabsContent>
</div>
</Tabs>
</div>
</div>
</AppLayout>
);
}
function Plus({ className }: { className?: string }) {
return <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}><path d="M5 12h14" /><path d="M12 5v14" /></svg>
}

View File

@@ -0,0 +1,35 @@
import { Badge } from '@/components/ui/badge';
import { cn } from '@/lib/utils';
import { PartnerStatus, PartnerType } from '@/types/partner';
export const StatusBadge = ({ status }: { status: PartnerStatus }) => {
const styles = {
Active: 'bg-success/10 text-success border-success/20',
Invited: 'bg-primary/10 text-primary border-primary/20',
Suspended: 'bg-error/10 text-error border-error/20',
Archived: 'bg-muted text-muted-foreground border-border',
};
return (
<Badge variant="outline" className={cn("font-medium", styles[status])}>
{status}
</Badge>
);
};
export const TypeBadge = ({ type }: { type: PartnerType }) => {
const styles = {
Venue: 'text-purple-400 border-purple-400/30 bg-purple-400/10',
Promoter: 'text-amber-400 border-amber-400/30 bg-amber-400/10',
Sponsor: 'text-emerald-400 border-emerald-400/30 bg-emerald-400/10',
Vendor: 'text-blue-400 border-blue-400/30 bg-blue-400/10',
Affiliate: 'text-pink-400 border-pink-400/30 bg-pink-400/10',
Other: 'text-gray-400 border-gray-400/30 bg-gray-400/10',
};
return (
<Badge variant="outline" className={cn("font-medium", styles[type])}>
{type}
</Badge>
);
};

View File

@@ -0,0 +1,82 @@
import { MoreHorizontal, ExternalLink, Calendar, Wallet } from 'lucide-react';
import { Partner } from '@/types/partner';
import { StatusBadge, TypeBadge } from './PartnerBadges';
import { Button } from '@/components/ui/button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu';
import { useNavigate } from 'react-router-dom';
interface PartnerCardProps {
partner: Partner;
}
export function PartnerCard({ partner }: PartnerCardProps) {
const navigate = useNavigate();
return (
<div
className="group relative neu-card p-5 hover:border-accent/50 transition-all duration-300 cursor-pointer"
onClick={() => navigate(`/partners/${partner.id}`)}
>
<div className="flex justify-between items-start mb-4">
<div className="flex items-center gap-3">
<div className="h-12 w-12 rounded-xl bg-secondary flex items-center justify-center overflow-hidden border border-border/50">
{partner.logo ? (
<img src={partner.logo} alt={partner.name} className="h-full w-full object-cover" />
) : (
<span className="text-lg font-bold text-muted-foreground">{partner.name.substring(0, 2)}</span>
)}
</div>
<div>
<h3 className="font-bold text-foreground group-hover:text-accent transition-colors">{partner.name}</h3>
<div className="flex gap-2 mt-1">
<TypeBadge type={partner.type} />
<StatusBadge status={partner.status} />
</div>
</div>
</div>
<div onClick={(e) => e.stopPropagation()}>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => navigate(`/partners/${partner.id}`)}>View Details</DropdownMenuItem>
<DropdownMenuItem>Assign to Event</DropdownMenuItem>
<DropdownMenuItem className="text-error">Suspend Partner</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
<div className="grid grid-cols-2 gap-4 mt-6 py-4 border-t border-border/30">
<div>
<p className="text-xs text-muted-foreground mb-1 flex items-center gap-1">
<Calendar className="h-3 w-3" /> Active Deals
</p>
<p className="font-semibold text-foreground">{partner.metrics.activeDeals}</p>
</div>
<div>
<p className="text-xs text-muted-foreground mb-1 flex items-center gap-1">
<Wallet className="h-3 w-3" /> Open Balance
</p>
<p className="font-semibold text-foreground">{partner.metrics.openBalance.toLocaleString()}</p>
</div>
</div>
<div className="mt-4 flex items-center justify-between text-xs text-muted-foreground">
<span>{partner.primaryContact.name}</span>
<span className="flex items-center gap-1 hover:text-accent">
View Portal <ExternalLink className="h-3 w-3" />
</span>
</div>
</div>
);
}

View File

@@ -0,0 +1,35 @@
import { Search, Filter, Plus } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
interface PartnerFiltersProps {
onSearch: (query: string) => void;
onFilter: () => void;
onAdd: () => void;
}
export function PartnerFilters({ onSearch, onFilter, onAdd }: PartnerFiltersProps) {
return (
<div className="flex flex-col sm:flex-row gap-4 justify-between items-center mb-6">
<div className="relative w-full sm:w-96">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search partners by name, type, or contact..."
className="pl-10 bg-secondary border-border/50 focus:border-accent"
onChange={(e) => onSearch(e.target.value)}
/>
</div>
<div className="flex gap-2 w-full sm:w-auto">
<Button variant="outline" onClick={onFilter} className="gap-2">
<Filter className="h-4 w-4" />
Filters
</Button>
<Button onClick={onAdd} className="gap-2 bg-accent text-white hover:bg-accent/90 shadow-lg shadow-accent/20">
<Plus className="h-4 w-4" />
Add Partner
</Button>
</div>
</div>
);
}

109
src/pages/Login.tsx Normal file
View File

@@ -0,0 +1,109 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuth } from '@/contexts/AuthContext';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { LogIn } from 'lucide-react';
export default function Login() {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [isLoading, setIsLoading] = useState(false);
const { login } = useAuth();
const navigate = useNavigate();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!username || !password) {
return;
}
try {
setIsLoading(true);
await login(username, password);
navigate('/');
} catch (error) {
// Error handling is done in AuthContext
} finally {
setIsLoading(false);
}
};
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-background via-secondary to-background">
<div className="w-full max-w-md p-8">
{/* Login Card */}
<div className="neu-card p-8 space-y-6">
{/* Logo/Title */}
<div className="text-center space-y-2">
<div className="flex justify-center mb-4">
<div className="h-16 w-16 rounded-2xl bg-gradient-to-br from-accent to-royal-blue flex items-center justify-center shadow-neu">
<LogIn className="h-8 w-8 text-white" />
</div>
</div>
<h1 className="text-2xl font-bold text-foreground">Eventify Admin Panel</h1>
<p className="text-sm text-muted-foreground">
Welcome back! Sign in to manage your platform.
</p>
</div>
{/* Login Form */}
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="username" className="text-foreground">Username</Label>
<Input
id="username"
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder="Enter your username"
required
disabled={isLoading}
className="shadow-neu-inset bg-secondary"
/>
</div>
<div className="space-y-2">
<Label htmlFor="password" className="text-foreground">Password</Label>
<Input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Enter your password"
required
disabled={isLoading}
className="shadow-neu-inset bg-secondary"
/>
</div>
<Button
type="submit"
className="w-full h-12 text-base font-semibold shadow-neu hover:shadow-neu-lg transition-all"
disabled={isLoading || !username || !password}
>
{isLoading ? (
<span className="flex items-center gap-2">
<div className="h-4 w-4 border-2 border-background border-t-transparent rounded-full animate-spin" />
Signing in...
</span>
) : (
<span className="flex items-center gap-2">
<LogIn className="h-5 w-5" />
Sign In
</span>
)}
</Button>
</form>
{/* Footer */}
<div className="text-center text-xs text-muted-foreground pt-4 border-t border-border/30">
Eventify Command Center v1.0
</div>
</div>
</div>
</div>
);
}

165
src/services/auth.ts Normal file
View File

@@ -0,0 +1,165 @@
// Authentication service based on UAT admin panel pattern
const AUTH_API_URL = import.meta.env.VITE_AUTH_API_URL || 'https://uat.eventifyplus.com/api/';
export interface AuthUser {
username: string;
token: string;
first_name?: string;
last_name?: string;
email?: string;
profile_photo?: string;
}
export interface LoginResponse {
username: string;
token: string;
message?: string;
user?: {
first_name?: string;
last_name?: string;
email?: string;
phone_number?: string;
profile_photo?: string;
};
}
export class AuthError extends Error {
isInvalidToken: boolean = false;
constructor(message: string, isInvalidToken: boolean = false) {
super(message);
this.name = 'AuthError';
this.isInvalidToken = isInvalidToken;
}
}
/**
* Login with username and password
*/
export const login = async (username: string, password: string): Promise<LoginResponse> => {
console.log('Bypassing auth for dev as requested');
// Return mock successful response immediately
return {
username: username,
token: 'dev-bypass-token-' + Date.now(),
message: 'Login successful (Bypass)',
user: {
first_name: 'Admin',
last_name: 'User (Bypass)',
email: username,
profile_photo: '', // Placeholder or empty
},
};
};
/**
* Check user status with token
*/
export const checkUserStatus = async (username: string, token: string): Promise<any> => {
// Support bypass token
if (token && token.startsWith('dev-bypass-token')) {
return {
status: 'active',
user: {
first_name: 'Admin',
last_name: 'User (Bypass)',
email: username || 'admin@example.com',
}
};
}
const statusUrl = `${AUTH_API_URL}user/status`;
const res = await fetch(statusUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token }),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) {
const errorMessage = data.message || data.errors || data.error || 'Status check failed';
const isTokenError = errorMessage.toLowerCase().includes('invalid token') ||
errorMessage.toLowerCase().includes('token') && (res.status === 401 || res.status === 403);
throw new AuthError(errorMessage, isTokenError);
}
return data;
};
/**
* Logout user
*/
export const logout = async (username: string, token: string): Promise<any> => {
// Handle bypass token logout locally
if (token && token.startsWith('dev-bypass-token')) {
return { message: 'Logged out successfully' };
}
const logoutUrl = `${AUTH_API_URL}user/logout`;
const res = await fetch(logoutUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token }),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) {
throw new AuthError(data.message || data.errors || data.error || 'Logout failed');
}
return data;
};
/**
* Get stored authentication data from localStorage
*/
export const getStoredAuth = (): AuthUser | null => {
try {
const username = localStorage.getItem('username');
const token = localStorage.getItem('token');
const userData = localStorage.getItem('userData');
if (!username || !token) {
return null;
}
const parsedUserData = userData ? JSON.parse(userData) : {};
return {
username,
token,
...parsedUserData,
};
} catch (error) {
console.error('Error reading stored auth:', error);
return null;
}
};
/**
* Store authentication data in localStorage
*/
export const storeAuth = (loginResponse: LoginResponse): void => {
localStorage.setItem('username', loginResponse.username);
localStorage.setItem('token', loginResponse.token);
if (loginResponse.user) {
localStorage.setItem('userData', JSON.stringify(loginResponse.user));
}
};
/**
* Clear authentication data from localStorage
*/
export const clearAuth = (): void => {
localStorage.removeItem('username');
localStorage.removeItem('token');
localStorage.removeItem('userData');
};

207
src/services/partnerApi.ts Normal file
View File

@@ -0,0 +1,207 @@
// API service for Partner Dashboard (partner.prototype.eventifyplus.com)
import { AuthError } from './auth';
const API_URL = import.meta.env.VITE_PARTNER_APP_API_URL || 'https://partner.prototype.eventifyplus.com/api/';
export interface Partner {
id: number;
name: string;
email: string;
phone?: string;
company_name?: string;
kyc_status: 'pending' | 'approved' | 'rejected';
stripe_status: 'pending' | 'connected' | 'failed';
stripe_account_id?: string;
total_revenue?: number;
events_count?: number;
created_at?: string;
kyc_documents?: {
id_proof?: string;
address_proof?: string;
business_registration?: string;
};
}
export interface PartnerEvent {
id: number;
partner_id: number;
title: string;
description?: string;
date: string;
time?: string;
venue?: string;
ticket_price?: number;
total_tickets?: number;
tickets_sold?: number;
status: 'draft' | 'pending_approval' | 'approved' | 'live' | 'completed' | 'cancelled';
revenue?: number;
}
export interface Staff {
id: number;
partner_id: number;
name: string;
email: string;
role: string;
permissions?: string[];
status: 'active' | 'inactive';
}
/**
* Fetch all partners (admin only)
*/
export const fetchPartners = async (username: string, token: string): Promise<Partner[]> => {
const res = await fetch(`${API_URL}partners/all/`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, token }),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) {
const errorMessage = data.message || data.errors || data.error || 'Failed to fetch partners';
const isTokenError = errorMessage.toLowerCase().includes('invalid token') || (res.status === 401 || res.status === 403);
throw new AuthError(errorMessage, isTokenError);
}
return data.partners || [];
};
/**
* Update partner KYC status (admin only)
*/
export const updatePartnerKYC = async (
username: string,
token: string,
partnerId: number,
status: 'approved' | 'rejected',
notes?: string
): Promise<any> => {
const res = await fetch(`${API_URL}partners/kyc/update/`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, token, partner_id: partnerId, status, notes }),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) {
const errorMessage = data.message || data.errors || data.error || 'Failed to update KYC status';
const isTokenError = errorMessage.toLowerCase().includes('invalid token') || (res.status === 401 || res.status === 403);
throw new AuthError(errorMessage, isTokenError);
}
return data;
};
/**
* Fetch partner events (admin can view all)
*/
export const fetchPartnerEvents = async (
username: string,
token: string,
partnerId?: number
): Promise<PartnerEvent[]> => {
const res = await fetch(`${API_URL}events/partner/`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, token, partner_id: partnerId }),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) {
const errorMessage = data.message || data.errors || data.error || 'Failed to fetch partner events';
const isTokenError = errorMessage.toLowerCase().includes('invalid token') || (res.status === 401 || res.status === 403);
throw new AuthError(errorMessage, isTokenError);
}
return data.events || [];
};
/**
* Approve or reject partner event (admin only)
*/
export const moderatePartnerEvent = async (
username: string,
token: string,
eventId: number,
action: 'approve' | 'reject',
reason?: string
): Promise<any> => {
const res = await fetch(`${API_URL}events/moderate/`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, token, event_id: eventId, action, reason }),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) {
const errorMessage = data.message || data.errors || data.error || 'Failed to moderate event';
const isTokenError = errorMessage.toLowerCase().includes('invalid token') || (res.status === 401 || res.status === 403);
throw new AuthError(errorMessage, isTokenError);
}
return data;
};
/**
* Fetch partner staff members
*/
export const fetchPartnerStaff = async (
username: string,
token: string,
partnerId?: number
): Promise<Staff[]> => {
const res = await fetch(`${API_URL}staff/all/`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, token, partner_id: partnerId }),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) {
const errorMessage = data.message || data.errors || data.error || 'Failed to fetch staff';
const isTokenError = errorMessage.toLowerCase().includes('invalid token') || (res.status === 401 || res.status === 403);
throw new AuthError(errorMessage, isTokenError);
}
return data.staff || [];
};
/**
* Update partner Stripe connection status
*/
export const updatePartnerStripe = async (
username: string,
token: string,
partnerId: number,
stripeAccountId: string,
status: 'connected' | 'failed'
): Promise<any> => {
const res = await fetch(`${API_URL}partners/stripe/update/`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
username,
token,
partner_id: partnerId,
stripe_account_id: stripeAccountId,
status
}),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) {
const errorMessage = data.message || data.errors || data.error || 'Failed to update Stripe status';
const isTokenError = errorMessage.toLowerCase().includes('invalid token') || (res.status === 401 || res.status === 403);
throw new AuthError(errorMessage, isTokenError);
}
return data;
};

220
src/services/userAppApi.ts Normal file
View File

@@ -0,0 +1,220 @@
// API service for User App (mvnew.eventifyplus.com)
// Based on UAT admin panel API patterns
import { AuthError } from './auth';
const API_URL = import.meta.env.VITE_USER_APP_API_URL || 'https://uat.eventifyplus.com/api/';
export interface Event {
id: number;
title: string;
description: string;
date_of_event: string;
time_of_event?: string;
venue?: string;
location?: string;
category?: string;
image_url?: string;
organizer?: string;
ticket_price?: number;
is_free?: boolean;
status?: 'draft' | 'published' | 'live' | 'completed' | 'cancelled';
created_at?: string;
}
export interface User {
id: number;
username: string;
first_name: string;
last_name: string;
email: string;
phone_number?: string;
profile_photo?: string;
pincode?: string;
district?: string;
state?: string;
country?: string;
place?: string;
created_at?: string;
}
export interface Category {
id: number;
name: string;
icon?: string;
event_count?: number;
}
export interface Booking {
id: number;
user_id: number;
event_id: number;
event_title?: string;
booking_date?: string;
status?: string;
}
/**
* Fetch all events
*/
export const fetchEvents = async (username: string, token: string): Promise<Event[]> => {
const res = await fetch(`${API_URL}events/all/`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, token }),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) {
const errorMessage = data.message || data.errors || data.error || 'Failed to fetch events';
const isTokenError = errorMessage.toLowerCase().includes('invalid token') || (res.status === 401 || res.status === 403);
throw new AuthError(errorMessage, isTokenError);
}
return data.events || [];
};
/**
* Fetch calendar events for a specific month
*/
export const fetchCalendarEvents = async (
username: string,
token: string,
month: number,
year: number
): Promise<Event[]> => {
const res = await fetch(`${API_URL}calendar/`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, token, month, year }),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) {
const errorMessage = data.message || data.errors || data.error || 'Failed to fetch calendar events';
const isTokenError = errorMessage.toLowerCase().includes('invalid token') || (res.status === 401 || res.status === 403);
throw new AuthError(errorMessage, isTokenError);
}
return data.events || [];
};
/**
* Fetch events by date
*/
export const fetchEventsByDate = async (
username: string,
token: string,
date_of_event: string
): Promise<Event[]> => {
const res = await fetch(`${API_URL}events/by-date/`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, token, date_of_event }),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) {
const errorMessage = data.message || data.errors || data.error || 'Failed to fetch events by date';
const isTokenError = errorMessage.toLowerCase().includes('invalid token') || (res.status === 401 || res.status === 403);
throw new AuthError(errorMessage, isTokenError);
}
return data.events || [];
};
/**
* Fetch all users (admin only)
*/
export const fetchUsers = async (username: string, token: string): Promise<User[]> => {
const res = await fetch(`${API_URL}users/all/`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, token }),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) {
const errorMessage = data.message || data.errors || data.error || 'Failed to fetch users';
const isTokenError = errorMessage.toLowerCase().includes('invalid token') || (res.status === 401 || res.status === 403);
throw new AuthError(errorMessage, isTokenError);
}
return data.users || [];
};
/**
* Fetch event categories
*/
export const fetchCategories = async (username: string, token: string): Promise<Category[]> => {
const res = await fetch(`${API_URL}categories/`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, token }),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) {
const errorMessage = data.message || data.errors || data.error || 'Failed to fetch categories';
const isTokenError = errorMessage.toLowerCase().includes('invalid token') || (res.status === 401 || res.status === 403);
throw new AuthError(errorMessage, isTokenError);
}
return data.categories || [];
};
/**
* Fetch user bookings
*/
export const fetchUserBookings = async (
username: string,
token: string,
userId: number
): Promise<Booking[]> => {
const res = await fetch(`${API_URL}bookings/user/`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, token, user_id: userId }),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) {
const errorMessage = data.message || data.errors || data.error || 'Failed to fetch bookings';
const isTokenError = errorMessage.toLowerCase().includes('invalid token') || (res.status === 401 || res.status === 403);
throw new AuthError(errorMessage, isTokenError);
}
return data.bookings || [];
};
/**
* Update event status (admin only)
*/
export const updateEventStatus = async (
username: string,
token: string,
eventId: number,
status: string
): Promise<any> => {
const res = await fetch(`${API_URL}events/update-status/`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, token, event_id: eventId, status }),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) {
const errorMessage = data.message || data.errors || data.error || 'Failed to update event';
const isTokenError = errorMessage.toLowerCase().includes('invalid token') || (res.status === 401 || res.status === 403);
throw new AuthError(errorMessage, isTokenError);
}
return data;
};

90
src/types/partner.ts Normal file
View File

@@ -0,0 +1,90 @@
import { z } from 'zod';
export type PartnerType = 'Venue' | 'Promoter' | 'Sponsor' | 'Vendor' | 'Affiliate' | 'Other';
export type PartnerStatus = 'Invited' | 'Active' | 'Suspended' | 'Archived';
export type SettlementStatus = 'Open' | 'Processing' | 'Settled';
export type VerificationStatus = 'Pending' | 'Verified' | 'Rejected';
export const PartnerSchema = z.object({
id: z.string(),
name: z.string().min(2, "Name must be at least 2 characters"),
type: z.enum(['Venue', 'Promoter', 'Sponsor', 'Vendor', 'Affiliate', 'Other']),
status: z.enum(['Invited', 'Active', 'Suspended', 'Archived']),
logo: z.string().optional(),
primaryContact: z.object({
name: z.string(),
email: z.string().email(),
phone: z.string().optional(),
role: z.string().optional(),
}),
companyDetails: z.object({
legalName: z.string().optional(),
taxId: z.string().optional(),
website: z.string().url().optional(),
address: z.string().optional(),
}).optional(),
metrics: z.object({
activeDeals: z.number().default(0),
totalRevenue: z.number().default(0),
openBalance: z.number().default(0),
lastActivity: z.string(), // ISO date
eventsCount: z.number().default(0),
}),
tags: z.array(z.string()).default([]),
notes: z.string().optional(),
joinedAt: z.string(),
});
export type Partner = z.infer<typeof PartnerSchema>;
export const DealTermSchema = z.object({
id: z.string(),
partnerId: z.string(),
type: z.enum(['RevenueShare', 'CommissionPerTicket', 'FixedFee', 'Tiered', 'Hybrid']),
name: z.string(),
params: z.object({
percentage: z.number().optional(), // For RevenueShare
amount: z.number().optional(), // For FixedFee or Commission
currency: z.string().default('INR'),
tiers: z.array(z.object({
threshold: z.number(),
percentage: z.number(),
})).optional(),
conditions: z.string().optional(),
}),
effectiveFrom: z.string(),
effectiveTo: z.string().optional(),
status: z.enum(['Draft', 'Active', 'Expired']),
version: z.number(),
});
export type DealTerm = z.infer<typeof DealTermSchema>;
export const LedgerEntrySchema = z.object({
id: z.string(),
partnerId: z.string(),
eventId: z.string().optional(),
type: z.enum(['Credit', 'Debit', 'Payout', 'Adjustment']),
description: z.string(),
amount: z.number(),
currency: z.string().default('INR'),
referenceId: z.string().optional(), // Invoice ID or Transaction ID
createdAt: z.string(),
status: z.enum(['Pending', 'Cleared', 'Failed']),
});
export type LedgerEntry = z.infer<typeof LedgerEntrySchema>;
export const PartnerDocumentSchema = z.object({
id: z.string(),
partnerId: z.string(),
type: z.enum(['Contract', 'Invoice', 'Tax', 'Compliance', 'Other']),
name: z.string(),
url: z.string().url(),
status: z.enum(['Pending', 'Signed', 'Verified', 'Expired']),
uploadedBy: z.string(),
uploadedAt: z.string(),
expiresAt: z.string().optional(),
});
export type PartnerDocument = z.infer<typeof PartnerDocumentSchema>;

View File

@@ -11,6 +11,15 @@ export default defineConfig(({ mode }) => ({
hmr: {
overlay: false,
},
proxy: {
// Proxy API requests to bypass CORS during development
'/api': {
target: 'https://uat.eventifyplus.com',
changeOrigin: true,
secure: false,
rewrite: (path) => path,
},
},
},
plugins: [react(), mode === "development" && componentTagger()].filter(Boolean),
resolve: {