import { clsx, type ClassValue } from "clsx" import { twMerge } from "tailwind-merge" export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)) } // Calculate expected value export function calculateExpectedValue(tcv: number | null | undefined, winProbability: number | null | undefined): number | null { if (!tcv || !winProbability) return null return tcv * (winProbability / 100) } // Calculate expected gross profit export function calculateExpectedGrossProfit( tcv: number | null | undefined, marginPercent: number | null | undefined, winProbability: number | null | undefined ): number | null { if (!tcv || !marginPercent || !winProbability) return null return tcv * (marginPercent / 100) * (winProbability / 100) } // Calculate RFP Value Score (0-100) export function calculateValueScore(rfp: { scoreNewLogo: number scoreRegionExpansion: number scoreCaseStudy: number scoreReusableAssets: number scoreCompetitivePosition: number scoreRelationship: number scorePartnerCoSell: number estimatedTCV?: number | null expectedMarginPercent?: number | null winProbabilityPercent?: number | null }): number { // Financial: 40 points (normalized from TCV and expected value) let financialScore = 0 if (rfp.estimatedTCV) { // Normalize TCV to 0-20 (assuming $10M is max for 20 points) financialScore += Math.min(20, (rfp.estimatedTCV / 10000000) * 20) } if (rfp.expectedMarginPercent && rfp.winProbabilityPercent) { // Add margin × win% as additional 20 points financialScore += ((rfp.expectedMarginPercent / 100) * (rfp.winProbabilityPercent / 100)) * 20 } // Strategic: 35 points (7 items × 5 points each = 35 max) const strategicScore = ( rfp.scoreNewLogo + rfp.scoreRegionExpansion + rfp.scoreCaseStudy + rfp.scoreReusableAssets + rfp.scoreCompetitivePosition ) * (35 / 25) // Normalize 25 points (5 items × 5) to 35 // Partnership: 25 points (2 items × 5 = 10, normalize to 25) const partnershipScore = ( rfp.scoreRelationship + rfp.scorePartnerCoSell ) * (25 / 10) return Math.min(100, Math.round(financialScore + strategicScore + partnershipScore)) } // Get value tier based on score export function getValueTier(score: number): string { if (score >= 80) return 'Must Pursue' if (score >= 60) return 'Pursue if capacity' return 'Watchlist / No-Go' } // Calculate ROI Index export function calculateROIIndex(valueScore: number | null | undefined, bidEffortHours: number | null | undefined): number | null { if (!valueScore || !bidEffortHours || bidEffortHours === 0) return null return valueScore / bidEffortHours } // Format currency export function formatCurrency(amount: number | null | undefined): string { if (!amount) return '$0' return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', minimumFractionDigits: 0, maximumFractionDigits: 0, }).format(amount) } // Format date export function formatDate(date: Date | string | null | undefined): string { if (!date) return 'N/A' return new Date(date).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric', }) } // Calculate days until deadline export function daysUntilDeadline(deadline: Date | string | null | undefined): number | null { if (!deadline) return null const now = new Date() const deadlineDate = new Date(deadline) const diffTime = deadlineDate.getTime() - now.getTime() const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) return diffDays } // Get certification status badge color export function getCertStatusColor(status: string): string { switch (status) { case 'Valid': case 'Available & valid': return 'bg-green-100 text-green-800' case 'Expiring Soon': case 'Available but expiring soon': return 'bg-yellow-100 text-yellow-800' case 'Expired': case 'Missing': return 'bg-red-100 text-red-800' default: return 'bg-gray-100 text-gray-800' } } // Get status badge color export function getStatusColor(status: string): string { switch (status) { case 'Done': case 'Submitted': return 'bg-green-100 text-green-800' case 'In Progress': case 'Writing': case 'Review': return 'bg-blue-100 text-blue-800' case 'Blocked': return 'bg-red-100 text-red-800' case 'Not Started': case 'Intake': return 'bg-gray-100 text-gray-800' case 'Final QA': return 'bg-purple-100 text-purple-800' default: return 'bg-gray-100 text-gray-800' } } // Get Go/No-Go status color export function getGoNoGoColor(status: string): string { switch (status) { case 'Go': return 'bg-green-100 text-green-800' case 'No-Go': return 'bg-red-100 text-red-800' case 'Watchlist': return 'bg-yellow-100 text-yellow-800' case 'Pending': return 'bg-gray-100 text-gray-800' default: return 'bg-gray-100 text-gray-800' } } // Get risk level color export function getRiskColor(risk: string): string { switch (risk) { case 'High': return 'bg-red-100 text-red-800' case 'Med': case 'Medium': return 'bg-yellow-100 text-yellow-800' case 'Low': return 'bg-green-100 text-green-800' default: return 'bg-gray-100 text-gray-800' } }