93 lines
No EOL
2.9 KiB
Vue
93 lines
No EOL
2.9 KiB
Vue
<template>
|
|
<div class="space-y-3">
|
|
<!-- Revenue streams table -->
|
|
<div class="space-y-2">
|
|
<div v-for="stream in sortedStreams" :key="stream.id"
|
|
class="flex justify-between items-center text-sm">
|
|
<div class="flex items-center gap-2">
|
|
<div
|
|
class="w-3 h-3 rounded"
|
|
:style="{ backgroundColor: getStreamColor(stream.id) }"
|
|
/>
|
|
<span class="font-medium">{{ stream.name }}</span>
|
|
</div>
|
|
<div class="flex items-center gap-2">
|
|
<span class="text-neutral-600">{{ formatCurrency(stream.targetMonthlyAmount || 0) }}</span>
|
|
<span class="font-medium min-w-[40px] text-right">{{ stream.percentage }}%</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Concentration warning -->
|
|
<div v-if="concentrationWarning"
|
|
class="p-3 bg-yellow-50 border border-yellow-200 rounded-md">
|
|
<div class="flex items-start gap-2">
|
|
<UIcon name="i-heroicons-exclamation-triangle" class="w-4 h-4 text-yellow-600 mt-0.5" />
|
|
<div class="text-sm text-yellow-800">
|
|
<span class="font-medium">{{ concentrationWarning.stream }}</span>
|
|
= {{ concentrationWarning.percentage }}% of total → consider balancing
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Totals -->
|
|
<div class="pt-2 border-t border-neutral-200 text-xs text-neutral-600">
|
|
<div class="flex justify-between">
|
|
<span>Total monthly target:</span>
|
|
<span class="font-medium">{{ formatCurrency(totalMonthly) }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { storeToRefs } from 'pinia'
|
|
|
|
const streamsStore = useStreamsStore()
|
|
const { streams } = storeToRefs(streamsStore)
|
|
|
|
const totalMonthly = computed(() => {
|
|
return streams.value.reduce((sum, s) => sum + (s.targetMonthlyAmount || 0), 0)
|
|
})
|
|
|
|
const sortedStreams = computed(() => {
|
|
const withPercentages = streams.value
|
|
.map(stream => {
|
|
const amount = stream.targetMonthlyAmount || 0
|
|
const percentage = totalMonthly.value > 0
|
|
? Math.round((amount / totalMonthly.value) * 100)
|
|
: 0
|
|
return { ...stream, percentage }
|
|
})
|
|
.filter(s => s.percentage > 0)
|
|
|
|
return withPercentages.sort((a, b) => b.percentage - a.percentage)
|
|
})
|
|
|
|
const concentrationWarning = computed(() => {
|
|
const topStream = sortedStreams.value[0]
|
|
if (topStream && topStream.percentage >= 50) {
|
|
return {
|
|
stream: topStream.name,
|
|
percentage: topStream.percentage
|
|
}
|
|
}
|
|
return null
|
|
})
|
|
|
|
const colors = ['#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', '#06b6d4', '#84cc16']
|
|
|
|
function getStreamColor(streamId: string) {
|
|
const index = streams.value.findIndex(s => s.id === streamId)
|
|
return colors[index % colors.length]
|
|
}
|
|
|
|
function formatCurrency(amount: number) {
|
|
return new Intl.NumberFormat('en-EU', {
|
|
style: 'currency',
|
|
currency: 'EUR',
|
|
minimumFractionDigits: 0,
|
|
maximumFractionDigits: 0
|
|
}).format(amount)
|
|
}
|
|
</script> |