import type { Member, Offer } from "~/types/coaching"; interface StreamRow { id: string; name: string; category: string; subcategory: string; targetPct: number; targetMonthlyAmount: number; certainty: "Committed" | "Probable" | "Aspirational"; payoutDelayDays: number; terms: string; revenueSharePct: number; platformFeePct: number; restrictions: "Restricted" | "General"; seasonalityWeights: number[]; effortHoursPerMonth: number; // Extended fields for offer-derived streams unitPrice?: number; unitsPerMonth?: number; feePercent?: number; notes?: string; costBreakdown?: { totalHours: number; totalCost: number; memberCosts: Array<{ memberId: string; memberName: string; hours: number; hourlyRate: number; baseCost: number; onCostAmount: number; totalCost: number; }>; }; } interface OfferToStreamOptions { onCostPercent?: number; // Default 25% on-cost category?: string; // Default category for offer-derived streams } export function offerToStream( offer: Offer, members: Member[], options: OfferToStreamOptions = {} ): StreamRow { const { onCostPercent = 25, category = "services" } = options; // Create member lookup map const memberMap = new Map(members.map(m => [m.id, m])); // Determine units per month based on offer type const unitsPerMonth = getUnitsPerMonth(offer); // Calculate cost breakdown const costBreakdown = calculateCostBreakdown(offer, memberMap, onCostPercent); // Generate terms string const terms = offer.payoutDelayDays <= 15 ? "Net 15" : offer.payoutDelayDays <= 30 ? "Net 30" : offer.payoutDelayDays <= 45 ? "Net 45" : `Net ${offer.payoutDelayDays}`; // Calculate monthly amount const targetMonthlyAmount = unitsPerMonth > 0 ? offer.price.baseline * unitsPerMonth : 0; return { id: `offer-${offer.id}`, name: offer.name, category, subcategory: inferSubcategory(offer.name), targetPct: 0, // Will be calculated later when all streams are known targetMonthlyAmount, certainty: "Probable" as const, // Offers are more than aspirational but not fully committed payoutDelayDays: offer.payoutDelayDays, terms, revenueSharePct: 0, platformFeePct: 0, restrictions: "General" as const, seasonalityWeights: new Array(12).fill(1), effortHoursPerMonth: calculateEffortHours(offer), // Extended fields unitPrice: offer.price.baseline, unitsPerMonth, feePercent: 3, // Default 3% processing/platform fee notes: offer.whyThis.join(". "), costBreakdown }; } export function offersToStreams( offers: Offer[], members: Member[], options: OfferToStreamOptions = {} ): StreamRow[] { return offers.map(offer => offerToStream(offer, members, options)); } // Helper functions function getUnitsPerMonth(offer: Offer): number { const offerName = offer.name.toLowerCase(); // Clinics and workshops are typically one-time deliverables per month if (offerName.includes("clinic") || offerName.includes("workshop")) { return 1; } // Check for specific patterns that indicate recurring work if (offerName.includes("retainer") || offerName.includes("ongoing")) { return 1; } // Sprints and other project-based work default to 0 (custom/project basis) return 0; } function inferSubcategory(offerName: string): string { const name = offerName.toLowerCase(); if (name.includes("training") || name.includes("workshop")) { return "Workshops/teaching"; } if (name.includes("consulting") || name.includes("review") || name.includes("clinic")) { return "Consulting"; } if (name.includes("development") || name.includes("sprint") || name.includes("prototype")) { return "Contract development"; } if (name.includes("narrative") || name.includes("writing")) { return "Technical services"; } if (name.includes("art") || name.includes("pipeline")) { return "Technical services"; } return "Contract development"; // Default fallback } function calculateEffortHours(offer: Offer): number { // Sum up total hours across all members return offer.hoursByMember.reduce((total, allocation) => total + allocation.hours, 0); } function calculateCostBreakdown( offer: Offer, memberMap: Map, onCostPercent: number ): StreamRow["costBreakdown"] { const memberCosts = offer.hoursByMember.map(allocation => { const member = memberMap.get(allocation.memberId); if (!member) { throw new Error(`Member not found: ${allocation.memberId}`); } const baseCost = allocation.hours * member.hourly; const onCostAmount = baseCost * (onCostPercent / 100); const totalCost = baseCost + onCostAmount; return { memberId: member.id, memberName: member.name, hours: allocation.hours, hourlyRate: member.hourly, baseCost, onCostAmount, totalCost }; }); const totalHours = memberCosts.reduce((sum, mc) => sum + mc.hours, 0); const totalCost = memberCosts.reduce((sum, mc) => sum + mc.totalCost, 0); return { totalHours, totalCost, memberCosts }; } // Type export for use in other files export type { StreamRow };