94 lines
No EOL
3.2 KiB
Vue
94 lines
No EOL
3.2 KiB
Vue
<template>
|
|
<div class="space-y-3">
|
|
<div v-for="member in membersWithCoverage" :key="member.id" class="space-y-1">
|
|
<div class="flex justify-between text-xs font-medium text-gray-700">
|
|
<span>{{ member.displayName || 'Unnamed' }}</span>
|
|
<span>{{ Math.round(member.coverageMinPct || 0) }}%</span>
|
|
</div>
|
|
|
|
<div class="relative h-6 bg-gray-100 rounded overflow-hidden">
|
|
<!-- Min coverage bar -->
|
|
<div
|
|
class="absolute top-0 left-0 h-full transition-all duration-300"
|
|
:class="getBarColor(member.coverageMinPct)"
|
|
:style="{ width: `${Math.min(100, member.coverageMinPct || 0)}%` }"
|
|
/>
|
|
|
|
<!-- Target coverage tick/ghost -->
|
|
<div
|
|
v-if="member.coverageTargetPct"
|
|
class="absolute top-0 h-full w-0.5 bg-gray-400 opacity-50"
|
|
:style="{ left: `${Math.min(100, member.coverageTargetPct)}%` }"
|
|
>
|
|
<div class="absolute -top-1 -left-1 w-2 h-2 bg-gray-400 rounded-full opacity-50" />
|
|
</div>
|
|
|
|
<!-- 100% line -->
|
|
<div class="absolute top-0 left-0 h-full w-full pointer-events-none">
|
|
<div class="absolute top-0 h-full w-px bg-gray-600" style="left: 100%" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Summary stats -->
|
|
<div class="pt-3 border-t border-gray-200 text-xs text-gray-600">
|
|
<div class="flex justify-between">
|
|
<span>Team median: {{ Math.round(teamStats.median || 0) }}%</span>
|
|
<span v-if="teamStats.under100 > 0" class="text-yellow-600 font-medium">
|
|
{{ teamStats.under100 }} under minimum
|
|
</span>
|
|
<span v-else class="text-green-600 font-medium">
|
|
All covered ✓
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { storeToRefs } from 'pinia'
|
|
|
|
// Use coopBuilder store which has the actual data
|
|
const coopStore = useCoopBuilderStore()
|
|
const { members, equalHourlyWage } = storeToRefs(coopStore)
|
|
|
|
const membersWithCoverage = computed(() => {
|
|
return members.value.map(member => {
|
|
// Calculate coverage based on member hours vs pay
|
|
const hourlyWage = equalHourlyWage.value || 50
|
|
const monthlyHours = member.hoursPerMonth || 0
|
|
const expectedPay = monthlyHours * hourlyWage
|
|
const actualPay = member.monthlyPayPlanned || 0
|
|
|
|
const coverageMinPct = expectedPay > 0 ? Math.min(100, (actualPay / expectedPay) * 100) : 0
|
|
|
|
return {
|
|
...member,
|
|
displayName: member.name,
|
|
coverageMinPct: coverageMinPct,
|
|
coverageTargetPct: coverageMinPct // Same for now since we don't have separate min/target
|
|
}
|
|
})
|
|
})
|
|
|
|
const teamStats = computed(() => {
|
|
const coverageValues = membersWithCoverage.value.map(m => m.coverageMinPct).filter(v => v !== undefined)
|
|
|
|
if (coverageValues.length === 0) {
|
|
return { under100: 0, median: 0 }
|
|
}
|
|
|
|
const sorted = [...coverageValues].sort((a, b) => a - b)
|
|
const median = sorted[Math.floor(sorted.length / 2)]
|
|
const under100 = coverageValues.filter(v => v < 100).length
|
|
|
|
return { under100, median }
|
|
})
|
|
|
|
function getBarColor(coverage: number | undefined) {
|
|
const pct = coverage || 0
|
|
if (pct >= 100) return 'bg-green-500'
|
|
if (pct >= 80) return 'bg-yellow-500'
|
|
return 'bg-red-500'
|
|
}
|
|
</script> |