227 lines
No EOL
8.5 KiB
Vue
227 lines
No EOL
8.5 KiB
Vue
<template>
|
|
<div class="hidden" data-ui="member_coverage_panel_v2" />
|
|
<UCard class="shadow-sm rounded-xl">
|
|
<template #header>
|
|
<div class="flex items-center justify-between">
|
|
<h3 class="font-semibold">Individual Member Coverage</h3>
|
|
<UTooltip text="Shows what each member needs from the co-op vs. what we can actually pay them">
|
|
<UIcon name="i-heroicons-information-circle" class="h-4 w-4 text-gray-400 hover:text-gray-600 cursor-help" />
|
|
</UTooltip>
|
|
</div>
|
|
</template>
|
|
|
|
<div v-if="allocatedMembers.length > 0" class="space-y-4">
|
|
<div
|
|
v-for="member in allocatedMembers"
|
|
:key="member.id"
|
|
class="space-y-2"
|
|
>
|
|
<!-- Member name and coverage percentage -->
|
|
<div class="flex items-center justify-between">
|
|
<div class="flex items-center gap-2">
|
|
<span class="font-medium text-gray-900">{{ member.displayName || member.name || 'Unnamed Member' }}</span>
|
|
<UBadge
|
|
:color="getCoverageColor(calculateCoverage(member))"
|
|
size="xs"
|
|
:ui="{ base: 'font-medium' }"
|
|
>
|
|
{{ Math.round(calculateCoverage(member)) }}% covered
|
|
</UBadge>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Financial breakdown -->
|
|
<div class="grid grid-cols-2 gap-4 text-sm">
|
|
<div class="space-y-1">
|
|
<div class="text-gray-600">Needs from co-op</div>
|
|
<div class="font-medium">{{ formatCurrency(member.minMonthlyNeeds || 0) }}</div>
|
|
</div>
|
|
<div class="space-y-1">
|
|
<div class="text-gray-600">Co-op can pay</div>
|
|
<div class="font-medium" :class="getAmountColor(member.monthlyPayPlanned, member.minMonthlyNeeds)">
|
|
{{ formatCurrency(member.monthlyPayPlanned || 0) }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Visual progress bar -->
|
|
<div class="space-y-1">
|
|
<div class="w-full bg-gray-200 rounded-full h-3 relative overflow-hidden">
|
|
<div
|
|
class="h-3 rounded-full transition-all duration-300"
|
|
:class="getBarColor(calculateCoverage(member))"
|
|
:style="{ width: `${Math.min(100, calculateCoverage(member))}%` }"
|
|
/>
|
|
<!-- 100% marker line -->
|
|
<div class="absolute top-0 h-3 w-0.5 bg-gray-600 opacity-75" style="left: 100%" v-if="calculateCoverage(member) < 100">
|
|
<div class="absolute -top-1 -left-1 w-2 h-2 bg-gray-600 rounded-full opacity-75" />
|
|
</div>
|
|
</div>
|
|
<div class="flex justify-between text-xs text-gray-500">
|
|
<span>0%</span>
|
|
<span>100%</span>
|
|
<span>200%+</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Gap/surplus indicator -->
|
|
<div v-if="getGapAmount(member) !== 0" class="flex items-center gap-1 text-xs">
|
|
<UIcon
|
|
:name="getGapAmount(member) > 0 ? 'i-heroicons-arrow-trending-down' : 'i-heroicons-arrow-trending-up'"
|
|
class="h-3 w-3"
|
|
:class="getGapAmount(member) > 0 ? 'text-red-500' : 'text-green-500'"
|
|
/>
|
|
<span :class="getGapAmount(member) > 0 ? 'text-red-600' : 'text-green-600'">
|
|
{{ getGapAmount(member) > 0 ? 'Gap: ' : 'Surplus: ' }}{{ formatCurrency(Math.abs(getGapAmount(member))) }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Empty State -->
|
|
<div v-else class="text-center py-8 text-gray-500">
|
|
<UIcon name="i-heroicons-users" class="h-8 w-8 mx-auto mb-2 opacity-50" />
|
|
<p class="text-sm mb-2">No members added yet</p>
|
|
<p class="text-xs">Complete setup wizard to add team members</p>
|
|
</div>
|
|
|
|
<template #footer v-if="allocatedMembers.length > 0">
|
|
<!-- Summary Stats -->
|
|
<div class="flex justify-between items-center text-sm text-gray-600 pb-3 border-b border-gray-200">
|
|
<div class="flex items-center gap-4">
|
|
<span>Median coverage: {{ Math.round(stats.median || 0) }}%</span>
|
|
<span :class="stats.under100 === 0 ? 'text-green-600' : 'text-amber-600'">
|
|
{{ stats.under100 === 0 ? 'All covered ✓' : `${stats.under100} need more` }}
|
|
</span>
|
|
</div>
|
|
<div class="text-xs">
|
|
<UTooltip text="Based on available revenue after overhead costs">
|
|
<span class="cursor-help">Total sustainable payroll: {{ formatCurrency(totalPayroll) }}</span>
|
|
</UTooltip>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Actionable Insights -->
|
|
<div class="pt-3">
|
|
<div v-if="totalGap > 0" class="text-xs">
|
|
<div class="flex items-center gap-2 text-amber-700">
|
|
<UIcon name="i-heroicons-light-bulb" class="h-3 w-3" />
|
|
<span class="font-medium">To cover everyone:</span>
|
|
</div>
|
|
<p class="mt-1 text-gray-600 pl-5">
|
|
Increase available payroll by <strong>{{ formatCurrency(totalGap) }}</strong>
|
|
through higher revenue or lower overhead costs.
|
|
</p>
|
|
</div>
|
|
|
|
<div v-else-if="totalSurplus > 0" class="text-xs">
|
|
<div class="flex items-center gap-2 text-green-700">
|
|
<UIcon name="i-heroicons-check-circle" class="h-3 w-3" />
|
|
<span class="font-medium">Healthy position:</span>
|
|
</div>
|
|
<p class="mt-1 text-gray-600 pl-5">
|
|
You have <strong>{{ formatCurrency(totalSurplus) }}</strong> surplus after covering all member needs.
|
|
Consider growth opportunities or building reserves.
|
|
</p>
|
|
</div>
|
|
|
|
<div v-else class="text-xs">
|
|
<div class="flex items-center gap-2 text-green-700">
|
|
<UIcon name="i-heroicons-scales" class="h-3 w-3" />
|
|
<span class="font-medium">Perfect balance:</span>
|
|
</div>
|
|
<p class="mt-1 text-gray-600 pl-5">
|
|
Available payroll exactly matches member needs.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</UCard>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
const { allocatePayroll, coverage, teamCoverageStats } = useCoopBuilder()
|
|
|
|
const allocatedMembers = computed(() => {
|
|
const members = allocatePayroll()
|
|
console.log('🔍 allocatedMembers computed:', members)
|
|
return members
|
|
})
|
|
const stats = computed(() => teamCoverageStats())
|
|
|
|
// Calculate total payroll
|
|
const totalPayroll = computed(() =>
|
|
allocatedMembers.value.reduce((sum, m) => sum + (m.monthlyPayPlanned || 0), 0)
|
|
)
|
|
|
|
// Color functions for coverage display
|
|
function getBarColor(pct: number): string {
|
|
if (!pct || pct < 80) return 'bg-red-500'
|
|
if (pct < 100) return 'bg-amber-500'
|
|
return 'bg-green-500'
|
|
}
|
|
|
|
function getCoverageColor(pct: number): string {
|
|
if (!pct || pct < 80) return 'red'
|
|
if (pct < 100) return 'amber'
|
|
return 'green'
|
|
}
|
|
|
|
function getAmountColor(planned: number = 0, needed: number = 0): string {
|
|
if (!needed) return 'text-gray-900'
|
|
if (planned >= needed) return 'text-green-600'
|
|
if (planned >= needed * 0.8) return 'text-amber-600'
|
|
return 'text-red-600'
|
|
}
|
|
|
|
// Calculate gap between what's needed vs what can be paid
|
|
function getGapAmount(member: any): number {
|
|
const planned = member.monthlyPayPlanned || 0
|
|
const needed = member.minMonthlyNeeds || 0
|
|
return needed - planned // positive = gap, negative = surplus
|
|
}
|
|
|
|
// Calculate total gap/surplus across all members
|
|
const totalGap = computed(() => {
|
|
const totalNeeded = allocatedMembers.value.reduce((sum, m) => sum + (m.minMonthlyNeeds || 0), 0)
|
|
const totalPlanned = totalPayroll.value
|
|
const gap = totalNeeded - totalPlanned
|
|
return gap > 0 ? gap : 0
|
|
})
|
|
|
|
const totalSurplus = computed(() => {
|
|
const totalNeeded = allocatedMembers.value.reduce((sum, m) => sum + (m.minMonthlyNeeds || 0), 0)
|
|
const totalPlanned = totalPayroll.value
|
|
const surplus = totalPlanned - totalNeeded
|
|
return surplus > 0 ? surplus : 0
|
|
})
|
|
|
|
// Local coverage calculation for debugging
|
|
function calculateCoverage(member: any): number {
|
|
const coopPay = member.monthlyPayPlanned || 0
|
|
const needs = member.minMonthlyNeeds || 0
|
|
|
|
console.log(`Coverage calc for ${member.name || member.displayName || 'Unknown'}:`, {
|
|
member: JSON.stringify(member, null, 2),
|
|
coopPay,
|
|
needs,
|
|
coverage: needs > 0 ? (coopPay / needs) * 100 : 100
|
|
})
|
|
|
|
if (needs === 0) {
|
|
console.log(`⚠️ Member ${member.name} has NO minMonthlyNeeds - defaulting to 100%`)
|
|
return 100
|
|
}
|
|
return Math.min(200, (coopPay / needs) * 100)
|
|
}
|
|
|
|
// Currency formatting
|
|
function formatCurrency(amount: number): string {
|
|
return new Intl.NumberFormat('en-US', {
|
|
style: 'currency',
|
|
currency: 'USD',
|
|
minimumFractionDigits: 0,
|
|
maximumFractionDigits: 0,
|
|
}).format(amount)
|
|
}
|
|
</script> |