RFP_Space_V05 / lib /utils.ts
Varun10000's picture
Upload 89 files
17cf14d verified
Raw
History Blame Contribute Delete
5.52 kB
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'
}
}