Major additions: - Territory manager system with application workflow - Custom pricing and page builder with Craft.js - Enhanced Stripe Connect onboarding - CodeReadr QR scanning integration - Kiosk mode for venue sales - Super admin dashboard and analytics - MCP integration for AI-powered operations Infrastructure improvements: - Centralized API client and routing system - Enhanced authentication with organization context - Comprehensive theme management system - Advanced event management with custom tabs - Performance monitoring and accessibility features Database schema updates: - Territory management tables - Custom pages and pricing structures - Kiosk PIN system - Enhanced organization profiles - CodeReadr integration tables 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
561 lines
14 KiB
TypeScript
561 lines
14 KiB
TypeScript
import { territoryManagerAPI } from './territory-manager-api';
|
|
import { TerritoryManagerAuth } from './territory-manager-auth';
|
|
import type {
|
|
TerritoryManager,
|
|
TerritoryManagerStats,
|
|
TerritoryApplication,
|
|
Lead,
|
|
Commission,
|
|
Achievement,
|
|
ExpenseReport,
|
|
TMNotification,
|
|
ReferralLink,
|
|
MarketingMaterial,
|
|
LeaderboardEntry,
|
|
ApplicationFormData,
|
|
LeadFormData,
|
|
ExpenseReportFormData
|
|
} from './territory-manager-types';
|
|
|
|
/**
|
|
* Centralized Territory Manager API router
|
|
* Browser-friendly API router that handles all Territory Manager API calls
|
|
* This is designed to be used in Astro client-side scripts and React components
|
|
*/
|
|
export class TerritoryManagerRouter {
|
|
|
|
/**
|
|
* Authentication methods
|
|
*/
|
|
static async signIn(email: string, password: string) {
|
|
try {
|
|
return await TerritoryManagerAuth.signIn(email, password);
|
|
} catch (error) {
|
|
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
static async signOut() {
|
|
try {
|
|
return await TerritoryManagerAuth.signOut();
|
|
} catch (error) {
|
|
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
static async checkAuth(): Promise<{
|
|
authenticated: boolean;
|
|
user: any;
|
|
territoryManager: TerritoryManager | null;
|
|
}> {
|
|
try {
|
|
const isTM = await TerritoryManagerAuth.isTerritoryManager();
|
|
const territoryManager = isTM ? await TerritoryManagerAuth.getCurrentTerritoryManager() : null;
|
|
|
|
return {
|
|
authenticated: isTM,
|
|
user: territoryManager ? { email: territoryManager.user_id } : null,
|
|
territoryManager
|
|
};
|
|
} catch (error) {
|
|
|
|
return {
|
|
authenticated: false,
|
|
user: null,
|
|
territoryManager: null
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Dashboard data loading
|
|
*/
|
|
static async loadDashboard(): Promise<{
|
|
stats: TerritoryManagerStats | null;
|
|
territoryManager: TerritoryManager | null;
|
|
recent_activity: any[];
|
|
active_leads: Lead[];
|
|
achievements: Achievement[];
|
|
notifications: TMNotification[];
|
|
earnings_history: any[];
|
|
error: string | null;
|
|
}> {
|
|
try {
|
|
const territoryManager = await TerritoryManagerAuth.getCurrentTerritoryManager();
|
|
if (!territoryManager) {
|
|
return {
|
|
stats: null,
|
|
territoryManager: null,
|
|
recent_activity: [],
|
|
active_leads: [],
|
|
achievements: [],
|
|
notifications: [],
|
|
earnings_history: [],
|
|
error: 'Not authenticated as territory manager'
|
|
};
|
|
}
|
|
|
|
// Load all dashboard data in parallel
|
|
const [
|
|
stats,
|
|
leads,
|
|
achievements,
|
|
notifications,
|
|
commissions
|
|
] = await Promise.all([
|
|
territoryManagerAPI.getTerritoryManagerStats(territoryManager.id),
|
|
territoryManagerAPI.getLeads(territoryManager.id),
|
|
territoryManagerAPI.getAchievements(),
|
|
territoryManagerAPI.getNotifications(territoryManager.id),
|
|
territoryManagerAPI.getCommissions(territoryManager.id)
|
|
]);
|
|
|
|
// Generate recent activity from commissions and leads
|
|
const recent_activity = [
|
|
...commissions.slice(0, 3).map(c => ({
|
|
type: 'commission',
|
|
message: `Earned $${c.total_commission} commission`,
|
|
created_at: c.created_at
|
|
})),
|
|
...leads.slice(0, 2).map(l => ({
|
|
type: 'lead',
|
|
message: `${l.status === 'converted' ? 'Converted' : 'Added'} lead: ${l.event_name}`,
|
|
created_at: l.created_at
|
|
}))
|
|
].sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
|
|
|
|
// Generate earnings history from commissions
|
|
const earnings_history = this.generateEarningsHistory(commissions);
|
|
|
|
return {
|
|
stats,
|
|
territoryManager,
|
|
recent_activity,
|
|
active_leads: leads.filter(l => l.status !== 'converted' && l.status !== 'dead'),
|
|
achievements,
|
|
notifications,
|
|
earnings_history,
|
|
error: null
|
|
};
|
|
} catch (error) {
|
|
|
|
return {
|
|
stats: null,
|
|
territoryManager: null,
|
|
recent_activity: [],
|
|
active_leads: [],
|
|
achievements: [],
|
|
notifications: [],
|
|
earnings_history: [],
|
|
error: 'Failed to load dashboard data'
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Application management
|
|
*/
|
|
static async submitApplication(data: ApplicationFormData): Promise<{
|
|
success: boolean;
|
|
applicationId?: string;
|
|
error?: string;
|
|
}> {
|
|
try {
|
|
const application = await territoryManagerAPI.submitApplication(data);
|
|
return {
|
|
success: true,
|
|
applicationId: application.id
|
|
};
|
|
} catch (error) {
|
|
|
|
return {
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Failed to submit application'
|
|
};
|
|
}
|
|
}
|
|
|
|
static async getApplications(status?: string): Promise<TerritoryApplication[]> {
|
|
try {
|
|
return await territoryManagerAPI.getApplications(status);
|
|
} catch (error) {
|
|
|
|
return [];
|
|
}
|
|
}
|
|
|
|
static async reviewApplication(id: string, action: 'approve' | 'reject', notes?: string): Promise<{
|
|
success: boolean;
|
|
error?: string;
|
|
}> {
|
|
try {
|
|
await territoryManagerAPI.reviewApplication(id, action, notes);
|
|
return { success: true };
|
|
} catch (error) {
|
|
|
|
return {
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Failed to review application'
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Lead management
|
|
*/
|
|
static async loadLeads(): Promise<Lead[]> {
|
|
try {
|
|
const territoryManager = await TerritoryManagerAuth.getCurrentTerritoryManager();
|
|
if (!territoryManager) return [];
|
|
|
|
return await territoryManagerAPI.getLeads(territoryManager.id);
|
|
} catch (error) {
|
|
|
|
return [];
|
|
}
|
|
}
|
|
|
|
static async createLead(data: LeadFormData): Promise<{
|
|
success: boolean;
|
|
lead?: Lead;
|
|
error?: string;
|
|
}> {
|
|
try {
|
|
const lead = await territoryManagerAPI.createLead(data);
|
|
return {
|
|
success: true,
|
|
lead
|
|
};
|
|
} catch (error) {
|
|
|
|
return {
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Failed to create lead'
|
|
};
|
|
}
|
|
}
|
|
|
|
static async updateLead(id: string, data: Partial<Lead>): Promise<{
|
|
success: boolean;
|
|
lead?: Lead;
|
|
error?: string;
|
|
}> {
|
|
try {
|
|
const lead = await territoryManagerAPI.updateLead(id, data);
|
|
return {
|
|
success: true,
|
|
lead
|
|
};
|
|
} catch (error) {
|
|
|
|
return {
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Failed to update lead'
|
|
};
|
|
}
|
|
}
|
|
|
|
static async deleteLead(id: string): Promise<{
|
|
success: boolean;
|
|
error?: string;
|
|
}> {
|
|
try {
|
|
await territoryManagerAPI.deleteLead(id);
|
|
return { success: true };
|
|
} catch (error) {
|
|
|
|
return {
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Failed to delete lead'
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Commission management
|
|
*/
|
|
static async loadCommissions(): Promise<Commission[]> {
|
|
try {
|
|
const territoryManager = await TerritoryManagerAuth.getCurrentTerritoryManager();
|
|
if (!territoryManager) return [];
|
|
|
|
return await territoryManagerAPI.getCommissions(territoryManager.id);
|
|
} catch (error) {
|
|
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Expense management
|
|
*/
|
|
static async submitExpenseReport(data: ExpenseReportFormData): Promise<{
|
|
success: boolean;
|
|
expenseReport?: ExpenseReport;
|
|
error?: string;
|
|
}> {
|
|
try {
|
|
const expenseReport = await territoryManagerAPI.submitExpenseReport(data);
|
|
return {
|
|
success: true,
|
|
expenseReport
|
|
};
|
|
} catch (error) {
|
|
|
|
return {
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Failed to submit expense report'
|
|
};
|
|
}
|
|
}
|
|
|
|
static async loadExpenseReports(): Promise<ExpenseReport[]> {
|
|
try {
|
|
const territoryManager = await TerritoryManagerAuth.getCurrentTerritoryManager();
|
|
if (!territoryManager) return [];
|
|
|
|
return await territoryManagerAPI.getExpenseReports(territoryManager.id);
|
|
} catch (error) {
|
|
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Referral management
|
|
*/
|
|
static async generateReferralLink(eventType?: string): Promise<{
|
|
success: boolean;
|
|
referralLink?: ReferralLink;
|
|
error?: string;
|
|
}> {
|
|
try {
|
|
const territoryManager = await TerritoryManagerAuth.getCurrentTerritoryManager();
|
|
if (!territoryManager) {
|
|
return {
|
|
success: false,
|
|
error: 'Not authenticated as territory manager'
|
|
};
|
|
}
|
|
|
|
const referralLink = await territoryManagerAPI.generateReferralLink(territoryManager.id, eventType);
|
|
return {
|
|
success: true,
|
|
referralLink
|
|
};
|
|
} catch (error) {
|
|
|
|
return {
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Failed to generate referral link'
|
|
};
|
|
}
|
|
}
|
|
|
|
static async loadReferralLinks(): Promise<ReferralLink[]> {
|
|
try {
|
|
const territoryManager = await TerritoryManagerAuth.getCurrentTerritoryManager();
|
|
if (!territoryManager) return [];
|
|
|
|
return await territoryManagerAPI.getReferralLinks(territoryManager.id);
|
|
} catch (error) {
|
|
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Training and achievements
|
|
*/
|
|
static async loadAchievements(): Promise<Achievement[]> {
|
|
try {
|
|
return await territoryManagerAPI.getAchievements();
|
|
} catch (error) {
|
|
|
|
return [];
|
|
}
|
|
}
|
|
|
|
static async loadTrainingProgress(): Promise<any[]> {
|
|
try {
|
|
const territoryManager = await TerritoryManagerAuth.getCurrentTerritoryManager();
|
|
if (!territoryManager) return [];
|
|
|
|
return await territoryManagerAPI.getTrainingProgress(territoryManager.id);
|
|
} catch (error) {
|
|
|
|
return [];
|
|
}
|
|
}
|
|
|
|
static async updateTrainingProgress(moduleId: string, score?: number): Promise<{
|
|
success: boolean;
|
|
error?: string;
|
|
}> {
|
|
try {
|
|
const territoryManager = await TerritoryManagerAuth.getCurrentTerritoryManager();
|
|
if (!territoryManager) {
|
|
return {
|
|
success: false,
|
|
error: 'Not authenticated as territory manager'
|
|
};
|
|
}
|
|
|
|
await territoryManagerAPI.updateTrainingProgress(territoryManager.id, moduleId, score);
|
|
return { success: true };
|
|
} catch (error) {
|
|
|
|
return {
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Failed to update training progress'
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Notifications
|
|
*/
|
|
static async loadNotifications(): Promise<TMNotification[]> {
|
|
try {
|
|
const territoryManager = await TerritoryManagerAuth.getCurrentTerritoryManager();
|
|
if (!territoryManager) return [];
|
|
|
|
return await territoryManagerAPI.getNotifications(territoryManager.id);
|
|
} catch (error) {
|
|
|
|
return [];
|
|
}
|
|
}
|
|
|
|
static async markNotificationAsRead(id: string): Promise<{
|
|
success: boolean;
|
|
error?: string;
|
|
}> {
|
|
try {
|
|
await territoryManagerAPI.markNotificationAsRead(id);
|
|
return { success: true };
|
|
} catch (error) {
|
|
|
|
return {
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Failed to mark notification as read'
|
|
};
|
|
}
|
|
}
|
|
|
|
static async markAllNotificationsAsRead(): Promise<{
|
|
success: boolean;
|
|
error?: string;
|
|
}> {
|
|
try {
|
|
const notifications = await this.loadNotifications();
|
|
const unreadNotifications = notifications.filter(n => !n.read);
|
|
|
|
await Promise.all(
|
|
unreadNotifications.map(n => territoryManagerAPI.markNotificationAsRead(n.id))
|
|
);
|
|
|
|
return { success: true };
|
|
} catch (error) {
|
|
|
|
return {
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Failed to mark all notifications as read'
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Leaderboard
|
|
*/
|
|
static async loadLeaderboard(period: 'monthly' | 'quarterly' | 'yearly' = 'monthly'): Promise<LeaderboardEntry[]> {
|
|
try {
|
|
return await territoryManagerAPI.getLeaderboard(period);
|
|
} catch (error) {
|
|
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Marketing materials
|
|
*/
|
|
static async loadMarketingMaterials(): Promise<MarketingMaterial[]> {
|
|
try {
|
|
return await territoryManagerAPI.getMarketingMaterials();
|
|
} catch (error) {
|
|
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Utility methods
|
|
*/
|
|
static formatCurrency(amount: number): string {
|
|
return new Intl.NumberFormat('en-US', {
|
|
style: 'currency',
|
|
currency: 'USD'
|
|
}).format(amount);
|
|
}
|
|
|
|
static formatDate(dateString: string): string {
|
|
return new Date(dateString).toLocaleDateString('en-US', {
|
|
weekday: 'long',
|
|
year: 'numeric',
|
|
month: 'long',
|
|
day: 'numeric'
|
|
});
|
|
}
|
|
|
|
static formatDateTime(dateString: string): string {
|
|
return new Date(dateString).toLocaleDateString('en-US', {
|
|
weekday: 'long',
|
|
year: 'numeric',
|
|
month: 'long',
|
|
day: 'numeric',
|
|
hour: 'numeric',
|
|
minute: '2-digit'
|
|
});
|
|
}
|
|
|
|
static getStatusColor(status: string): string {
|
|
const colors = {
|
|
'pending': 'bg-yellow-600 text-white',
|
|
'active': 'bg-green-600 text-white',
|
|
'suspended': 'bg-red-600 text-white',
|
|
'inactive': 'bg-gray-600 text-white',
|
|
'cold': 'bg-gray-600 text-white',
|
|
'contacted': 'bg-blue-600 text-white',
|
|
'confirmed': 'bg-green-600 text-white',
|
|
'converted': 'bg-purple-600 text-white',
|
|
'dead': 'bg-red-600 text-white'
|
|
};
|
|
return colors[status] || 'bg-gray-600 text-white';
|
|
}
|
|
|
|
/**
|
|
* Private utility methods
|
|
*/
|
|
private static generateEarningsHistory(commissions: Commission[]): any[] {
|
|
const monthlyEarnings = new Map<string, number>();
|
|
|
|
commissions.forEach(commission => {
|
|
const month = commission.created_at.substring(0, 7); // YYYY-MM
|
|
const current = monthlyEarnings.get(month) || 0;
|
|
monthlyEarnings.set(month, current + commission.total_commission);
|
|
});
|
|
|
|
return Array.from(monthlyEarnings.entries())
|
|
.map(([month, amount]) => ({
|
|
date: month + '-01',
|
|
amount
|
|
}))
|
|
.sort((a, b) => a.date.localeCompare(b.date));
|
|
}
|
|
}
|
|
|
|
// Export the router for global use
|
|
export const territoryManagerRouter = TerritoryManagerRouter; |