feat: add initial application structure with configuration, UI components, and state management
This commit is contained in:
parent
fadf94002c
commit
0af6b17792
56 changed files with 6137 additions and 129 deletions
273
pages/mix.vue
Normal file
273
pages/mix.vue
Normal file
|
|
@ -0,0 +1,273 @@
|
|||
<template>
|
||||
<section class="py-8 space-y-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-2xl font-semibold">Revenue Mix Planner</h2>
|
||||
<UButton color="primary" @click="sendToBudget">
|
||||
Send to Budget & Scenarios
|
||||
</UButton>
|
||||
</div>
|
||||
|
||||
<!-- Concentration Overview -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<UCard>
|
||||
<template #header>
|
||||
<h3 class="text-lg font-medium">Concentration Risk</h3>
|
||||
</template>
|
||||
<div class="space-y-4">
|
||||
<div class="text-center">
|
||||
<div class="text-4xl font-bold text-red-600 mb-2">65%</div>
|
||||
<div class="text-sm text-gray-600 mb-3">Top source percentage</div>
|
||||
<ConcentrationChip
|
||||
status="red"
|
||||
:top-source-pct="65"
|
||||
:show-percentage="false"
|
||||
variant="solid"
|
||||
size="md" />
|
||||
</div>
|
||||
<p class="text-sm text-gray-600 text-center">
|
||||
Most of your money comes from one place. Add another stream to
|
||||
reduce risk.
|
||||
</p>
|
||||
</div>
|
||||
</UCard>
|
||||
|
||||
<UCard>
|
||||
<template #header>
|
||||
<h3 class="text-lg font-medium">Payout Delay Exposure</h3>
|
||||
</template>
|
||||
<div class="space-y-4">
|
||||
<div class="text-center">
|
||||
<div class="text-4xl font-bold text-yellow-600 mb-2">35 days</div>
|
||||
<div class="text-sm text-gray-600 mb-3">Weighted average delay</div>
|
||||
<UBadge color="yellow" variant="subtle">Moderate Risk</UBadge>
|
||||
</div>
|
||||
<p class="text-sm text-gray-600 text-center">
|
||||
Money is earned now but arrives later. Delays can create mid-month
|
||||
dips.
|
||||
</p>
|
||||
</div>
|
||||
</UCard>
|
||||
</div>
|
||||
|
||||
<!-- Revenue Streams Table -->
|
||||
<UCard>
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-lg font-medium">Revenue Streams</h3>
|
||||
<UButton icon="i-heroicons-plus" size="sm" @click="addStream">
|
||||
Add Stream
|
||||
</UButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<UTable :rows="streams" :columns="columns">
|
||||
<template #name-data="{ row }">
|
||||
<div>
|
||||
<div class="font-medium">{{ row.name }}</div>
|
||||
<div class="text-xs text-gray-500">{{ row.category }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #targetPct-data="{ row }">
|
||||
<div class="flex items-center gap-2">
|
||||
<UInput
|
||||
v-model="row.targetPct"
|
||||
type="number"
|
||||
size="xs"
|
||||
class="w-16"
|
||||
@update:model-value="updateStream(row.id, 'targetPct', $event)" />
|
||||
<span class="text-xs text-gray-500">%</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #targetAmount-data="{ row }">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-xs text-gray-500">€</span>
|
||||
<UInput
|
||||
v-model="row.targetMonthlyAmount"
|
||||
type="number"
|
||||
size="xs"
|
||||
class="w-20"
|
||||
@update:model-value="
|
||||
updateStream(row.id, 'targetMonthlyAmount', $event)
|
||||
" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #fees-data="{ row }">
|
||||
<div class="text-sm">
|
||||
<div v-if="row.platformFeePct > 0">
|
||||
Platform: {{ row.platformFeePct }}%
|
||||
</div>
|
||||
<div v-if="row.revenueSharePct > 0">
|
||||
Share: {{ row.revenueSharePct }}%
|
||||
</div>
|
||||
<div
|
||||
v-if="row.platformFeePct === 0 && row.revenueSharePct === 0"
|
||||
class="text-gray-400">
|
||||
None
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #delay-data="{ row }">
|
||||
<div class="flex items-center gap-2">
|
||||
<UInput
|
||||
v-model="row.payoutDelayDays"
|
||||
type="number"
|
||||
size="xs"
|
||||
class="w-16"
|
||||
@update:model-value="
|
||||
updateStream(row.id, 'payoutDelayDays', $event)
|
||||
" />
|
||||
<span class="text-xs text-gray-500">days</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #restrictions-data="{ row }">
|
||||
<RestrictionChip :restriction="row.restrictions" />
|
||||
</template>
|
||||
|
||||
<template #certainty-data="{ row }">
|
||||
<UBadge
|
||||
:color="getCertaintyColor(row.certainty)"
|
||||
variant="subtle"
|
||||
size="xs">
|
||||
{{ row.certainty }}
|
||||
</UBadge>
|
||||
</template>
|
||||
|
||||
<template #actions-data="{ row }">
|
||||
<UDropdown :items="getRowActions(row)">
|
||||
<UButton
|
||||
icon="i-heroicons-ellipsis-horizontal"
|
||||
size="xs"
|
||||
variant="ghost" />
|
||||
</UDropdown>
|
||||
</template>
|
||||
</UTable>
|
||||
|
||||
<div class="mt-4 p-4 bg-gray-50 rounded-lg">
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="font-medium">Totals</span>
|
||||
<div class="flex gap-6">
|
||||
<span>{{ totalTargetPct }}%</span>
|
||||
<span>{{ $format.currency(totalMonthlyAmount) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</UCard>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const { $format } = useNuxtApp();
|
||||
const { loadStreams } = useFixtures();
|
||||
|
||||
// Load fixture data
|
||||
const fixtureData = await loadStreams();
|
||||
const streams = ref(
|
||||
fixtureData.revenueStreams.map((stream) => ({
|
||||
id: stream.id,
|
||||
name: stream.name,
|
||||
category: stream.category,
|
||||
targetPct: stream.targetPct,
|
||||
targetMonthlyAmount: stream.targetMonthlyAmount,
|
||||
certainty: stream.certainty,
|
||||
payoutDelayDays: stream.payoutDelayDays,
|
||||
platformFeePct: stream.platformFeePct || 0,
|
||||
revenueSharePct: stream.revenueSharePct || 0,
|
||||
restrictions: stream.restrictions,
|
||||
}))
|
||||
);
|
||||
|
||||
const columns = [
|
||||
{ id: "name", key: "name", label: "Stream" },
|
||||
{ id: "targetPct", key: "targetPct", label: "Target %" },
|
||||
{ id: "targetAmount", key: "targetAmount", label: "Monthly €" },
|
||||
{ id: "fees", key: "fees", label: "Fees" },
|
||||
{ id: "delay", key: "delay", label: "Payout Delay" },
|
||||
{ id: "restrictions", key: "restrictions", label: "Use" },
|
||||
{ id: "certainty", key: "certainty", label: "Certainty" },
|
||||
{ id: "actions", key: "actions", label: "" },
|
||||
];
|
||||
|
||||
const totalTargetPct = computed(() =>
|
||||
streams.value.reduce((sum, stream) => sum + (stream.targetPct || 0), 0)
|
||||
);
|
||||
|
||||
const totalMonthlyAmount = computed(() =>
|
||||
streams.value.reduce(
|
||||
(sum, stream) => sum + (stream.targetMonthlyAmount || 0),
|
||||
0
|
||||
)
|
||||
);
|
||||
|
||||
function getCertaintyColor(certainty: string) {
|
||||
switch (certainty) {
|
||||
case "Committed":
|
||||
return "green";
|
||||
case "Probable":
|
||||
return "blue";
|
||||
case "Aspirational":
|
||||
return "yellow";
|
||||
default:
|
||||
return "gray";
|
||||
}
|
||||
}
|
||||
|
||||
function getRowActions(row: any) {
|
||||
return [
|
||||
[
|
||||
{
|
||||
label: "Edit",
|
||||
icon: "i-heroicons-pencil",
|
||||
click: () => editStream(row),
|
||||
},
|
||||
{
|
||||
label: "Duplicate",
|
||||
icon: "i-heroicons-document-duplicate",
|
||||
click: () => duplicateStream(row),
|
||||
},
|
||||
{
|
||||
label: "Remove",
|
||||
icon: "i-heroicons-trash",
|
||||
click: () => removeStream(row),
|
||||
},
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
function updateStream(id: string, field: string, value: any) {
|
||||
const stream = streams.value.find((s) => s.id === id);
|
||||
if (stream) {
|
||||
stream[field] = Number(value) || value;
|
||||
}
|
||||
}
|
||||
|
||||
function addStream() {
|
||||
// Add stream logic
|
||||
console.log("Add new stream");
|
||||
}
|
||||
|
||||
function editStream(row: any) {
|
||||
// Edit stream logic
|
||||
console.log("Edit stream", row);
|
||||
}
|
||||
|
||||
function duplicateStream(row: any) {
|
||||
// Duplicate stream logic
|
||||
console.log("Duplicate stream", row);
|
||||
}
|
||||
|
||||
function removeStream(row: any) {
|
||||
const index = streams.value.findIndex((s) => s.id === row.id);
|
||||
if (index > -1) {
|
||||
streams.value.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
function sendToBudget() {
|
||||
navigateTo("/budget");
|
||||
}
|
||||
</script>
|
||||
Loading…
Add table
Add a link
Reference in a new issue