Spaces:
Build error
Build error
| 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' | |
| } | |
| } | |