refactor: update app.vue and various components to enhance UI consistency, replace color classes for improved accessibility, and refine layout for better user experience
This commit is contained in:
parent
7b4fb6c2fd
commit
24e8b7a3a8
41 changed files with 2395 additions and 1603 deletions
|
|
@ -7,93 +7,126 @@
|
|||
<UIcon name="i-heroicons-user-group" class="h-5 w-5" />
|
||||
<h3 class="font-semibold">Member Needs Coverage</h3>
|
||||
</div>
|
||||
<UTooltip text="Shows how well the co-op can meet each member's stated financial needs">
|
||||
<UIcon name="i-heroicons-information-circle" class="h-4 w-4 text-gray-400 hover:text-gray-600 cursor-help" />
|
||||
<UTooltip
|
||||
text="Shows how well the co-op can meet each member's stated financial needs">
|
||||
<UIcon
|
||||
name="i-heroicons-information-circle"
|
||||
class="h-4 w-4 text-neutral-400 hover:text-neutral-600 cursor-help" />
|
||||
</UTooltip>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<div v-if="hasMembers" class="space-y-4">
|
||||
<!-- Team Summary -->
|
||||
<div class="text-center">
|
||||
<div class="text-2xl font-semibold" :class="statusColor">
|
||||
{{ fullyCoveredCount }} of {{ totalMembers }}
|
||||
</div>
|
||||
<div class="text-sm text-gray-600">
|
||||
members fully covered
|
||||
</div>
|
||||
<div class="text-sm text-neutral-600">members fully covered</div>
|
||||
</div>
|
||||
|
||||
<!-- Coverage Stats -->
|
||||
<div class="flex justify-between text-sm">
|
||||
<div class="text-center">
|
||||
<div class="font-medium">{{ median }}%</div>
|
||||
<div class="text-gray-600">Median</div>
|
||||
<div class="text-neutral-600">Median</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="font-medium" :class="underCoveredColor">{{ stats.under100 }}</div>
|
||||
<div class="text-gray-600">Under 100%</div>
|
||||
<div class="font-medium" :class="underCoveredColor">
|
||||
{{ stats.under100 }}
|
||||
</div>
|
||||
<div class="text-neutral-600">Under 100%</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="font-medium">{{ formatCurrency(availablePayroll) }}</div>
|
||||
<div class="text-gray-600">Available</div>
|
||||
<div class="text-neutral-600">Available</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Intelligent Financial Analysis -->
|
||||
<div v-if="hasMembers" class="space-y-2">
|
||||
<!-- Coverage gap analysis -->
|
||||
<div v-if="stats.under100 > 0" class="text-xs bg-amber-50 p-3 rounded border-l-4 border-amber-400">
|
||||
<div
|
||||
v-if="stats.under100 > 0"
|
||||
class="text-xs bg-amber-50 p-3 rounded border-l-4 border-amber-400">
|
||||
<div class="flex items-start gap-2">
|
||||
<UIcon name="i-heroicons-exclamation-triangle" class="h-4 w-4 text-amber-600 mt-0.5 flex-shrink-0" />
|
||||
<UIcon
|
||||
name="i-heroicons-exclamation-triangle"
|
||||
class="h-4 w-4 text-amber-600 mt-0.5 flex-shrink-0" />
|
||||
<div class="space-y-1">
|
||||
<p class="font-medium text-amber-800">Coverage Gap Analysis</p>
|
||||
<p class="text-amber-700">
|
||||
To meet member needs, you need <strong>{{ formatCurrency(totalNeeds) }}</strong> based on their stated requirements,
|
||||
but you have <strong>{{ formatCurrency(availablePayroll) }}</strong> available for payroll.
|
||||
To meet member needs, you need
|
||||
<strong>{{ formatCurrency(totalNeeds) }}</strong> based on their
|
||||
stated requirements, but you have
|
||||
<strong>{{ formatCurrency(availablePayroll) }}</strong>
|
||||
available for payroll.
|
||||
</p>
|
||||
<p class="text-amber-600">
|
||||
<strong>Shortfall: {{ formatCurrency(Math.max(0, totalNeeds - availablePayroll)) }}</strong>
|
||||
<strong
|
||||
>Shortfall:
|
||||
{{
|
||||
formatCurrency(Math.max(0, totalNeeds - availablePayroll))
|
||||
}}</strong
|
||||
>
|
||||
</p>
|
||||
<p class="text-xs text-amber-600 mt-2">
|
||||
💡 Note: This reflects member-stated needs. Check your Budget page for detailed payroll planning.
|
||||
💡 Note: This reflects member-stated needs. Check your Budget
|
||||
page for detailed payroll planning.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Surplus analysis -->
|
||||
<div v-else-if="availablePayroll > totalNeeds && totalNeeds > 0" class="text-xs bg-green-50 p-3 rounded border-l-4 border-green-400">
|
||||
<div
|
||||
v-else-if="availablePayroll > totalNeeds && totalNeeds > 0"
|
||||
class="text-xs bg-green-50 p-3 rounded border-l-4 border-green-400">
|
||||
<div class="flex items-start gap-2">
|
||||
<UIcon name="i-heroicons-check-circle" class="h-4 w-4 text-green-600 mt-0.5 flex-shrink-0" />
|
||||
<UIcon
|
||||
name="i-heroicons-check-circle"
|
||||
class="h-4 w-4 text-green-600 mt-0.5 flex-shrink-0" />
|
||||
<div class="space-y-1">
|
||||
<p class="font-medium text-green-800">Healthy Coverage</p>
|
||||
<p class="text-green-700">
|
||||
You have <strong>{{ formatCurrency(availablePayroll) }}</strong> available to cover
|
||||
<strong>{{ formatCurrency(totalNeeds) }}</strong> in member needs.
|
||||
You have
|
||||
<strong>{{ formatCurrency(availablePayroll) }}</strong>
|
||||
available to cover
|
||||
<strong>{{ formatCurrency(totalNeeds) }}</strong> in member
|
||||
needs.
|
||||
</p>
|
||||
<p class="text-green-600">
|
||||
<strong>Surplus: {{ formatCurrency(availablePayroll - totalNeeds) }}</strong>
|
||||
<strong
|
||||
>Surplus:
|
||||
{{ formatCurrency(availablePayroll - totalNeeds) }}</strong
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- No payroll available -->
|
||||
<div v-else-if="availablePayroll === 0 && totalNeeds > 0" class="text-xs bg-red-50 p-3 rounded border-l-4 border-red-400">
|
||||
<div
|
||||
v-else-if="availablePayroll === 0 && totalNeeds > 0"
|
||||
class="text-xs bg-red-50 p-3 rounded border-l-4 border-red-400">
|
||||
<div class="flex items-start gap-2">
|
||||
<UIcon name="i-heroicons-x-circle" class="h-4 w-4 text-red-600 mt-0.5 flex-shrink-0" />
|
||||
<UIcon
|
||||
name="i-heroicons-x-circle"
|
||||
class="h-4 w-4 text-red-600 mt-0.5 flex-shrink-0" />
|
||||
<div class="space-y-1">
|
||||
<p class="font-medium text-red-800">No Funds for Payroll</p>
|
||||
<p class="text-red-700">
|
||||
Member needs total <strong>{{ formatCurrency(totalNeeds) }}</strong> based on their stated requirements,
|
||||
but current revenue minus costs leaves $0 for payroll.
|
||||
Member needs total
|
||||
<strong>{{ formatCurrency(totalNeeds) }}</strong> based on their
|
||||
stated requirements, but current revenue minus costs leaves $0
|
||||
for payroll.
|
||||
</p>
|
||||
<p class="text-red-600">
|
||||
Consider increasing revenue or reducing overhead costs.
|
||||
</p>
|
||||
<p class="text-xs text-red-600 mt-2">
|
||||
💡 Note: This reflects member-stated needs. Your Budget page may show different payroll amounts.
|
||||
💡 Note: This reflects member-stated needs. Your Budget page may
|
||||
show different payroll amounts.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -102,7 +135,7 @@
|
|||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div v-else class="text-center py-6 text-gray-500">
|
||||
<div v-else class="text-center py-6 text-neutral-500">
|
||||
<UIcon name="i-heroicons-users" class="h-8 w-8 mx-auto mb-2 opacity-50" />
|
||||
<p class="text-sm">Add members in setup to see coverage</p>
|
||||
</div>
|
||||
|
|
@ -110,56 +143,60 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const { members, teamCoverageStats, allocatePayroll, streams } = useCoopBuilder()
|
||||
const coopStore = useCoopBuilderStore()
|
||||
const { members, teamCoverageStats, allocatePayroll, streams } =
|
||||
useCoopBuilder();
|
||||
const coopStore = useCoopBuilderStore();
|
||||
|
||||
const stats = computed(() => teamCoverageStats())
|
||||
const allocatedMembers = computed(() => allocatePayroll())
|
||||
const median = computed(() => Math.round(stats.value.median ?? 0))
|
||||
const stats = computed(() => teamCoverageStats());
|
||||
const allocatedMembers = computed(() => allocatePayroll());
|
||||
const median = computed(() => Math.round(stats.value.median ?? 0));
|
||||
|
||||
// Team-level calculations
|
||||
const hasMembers = computed(() => members.value.length > 0)
|
||||
const totalMembers = computed(() => members.value.length)
|
||||
const fullyCoveredCount = computed(() => totalMembers.value - stats.value.under100)
|
||||
const hasMembers = computed(() => members.value.length > 0);
|
||||
const totalMembers = computed(() => members.value.length);
|
||||
const fullyCoveredCount = computed(
|
||||
() => totalMembers.value - stats.value.under100
|
||||
);
|
||||
|
||||
// Financial calculations
|
||||
const totalNeeds = computed(() =>
|
||||
const totalNeeds = computed(() =>
|
||||
allocatedMembers.value.reduce((sum, m) => sum + (m.minMonthlyNeeds || 0), 0)
|
||||
)
|
||||
);
|
||||
|
||||
const totalRevenue = computed(() =>
|
||||
const totalRevenue = computed(() =>
|
||||
streams.value.reduce((sum, s) => sum + (s.monthly || 0), 0)
|
||||
)
|
||||
);
|
||||
|
||||
const overheadCosts = computed(() =>
|
||||
const overheadCosts = computed(() =>
|
||||
coopStore.overheadCosts.reduce((sum, c) => sum + (c.amount || 0), 0)
|
||||
)
|
||||
);
|
||||
|
||||
const availablePayroll = computed(() =>
|
||||
const availablePayroll = computed(() =>
|
||||
Math.max(0, totalRevenue.value - overheadCosts.value)
|
||||
)
|
||||
);
|
||||
|
||||
// Status colors based on coverage
|
||||
const statusColor = computed(() => {
|
||||
const ratio = fullyCoveredCount.value / Math.max(1, totalMembers.value)
|
||||
if (ratio === 1) return 'text-green-600'
|
||||
if (ratio >= 0.8) return 'text-amber-600'
|
||||
return 'text-red-600'
|
||||
})
|
||||
const ratio = fullyCoveredCount.value / Math.max(1, totalMembers.value);
|
||||
if (ratio === 1) return "text-green-600";
|
||||
if (ratio >= 0.8) return "text-amber-600";
|
||||
return "text-red-600";
|
||||
});
|
||||
|
||||
const underCoveredColor = computed(() => {
|
||||
if (stats.value.under100 === 0) return 'text-green-600'
|
||||
if (stats.value.under100 <= Math.ceil(totalMembers.value * 0.2)) return 'text-amber-600'
|
||||
return 'text-red-600'
|
||||
})
|
||||
if (stats.value.under100 === 0) return "text-green-600";
|
||||
if (stats.value.under100 <= Math.ceil(totalMembers.value * 0.2))
|
||||
return "text-amber-600";
|
||||
return "text-red-600";
|
||||
});
|
||||
|
||||
// Currency formatting
|
||||
function formatCurrency(amount: number): string {
|
||||
return new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: 'USD',
|
||||
return new Intl.NumberFormat("en-US", {
|
||||
style: "currency",
|
||||
currency: "USD",
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 0,
|
||||
}).format(amount)
|
||||
}).format(amount);
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue