chore: update application configuration and UI components for improved styling and functionality
This commit is contained in:
parent
0af6b17792
commit
37ab8d7bab
54 changed files with 23293 additions and 1666 deletions
|
|
@ -2,6 +2,15 @@
|
|||
<section class="py-8 space-y-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-2xl font-semibold">Scenarios & Runway</h2>
|
||||
<UButton
|
||||
variant="outline"
|
||||
color="red"
|
||||
size="sm"
|
||||
@click="restartWizard"
|
||||
:disabled="isResetting">
|
||||
<UIcon name="i-heroicons-arrow-path" class="mr-1" />
|
||||
Restart Setup (Testing)
|
||||
</UButton>
|
||||
</div>
|
||||
|
||||
<!-- 6-Month Preset Card -->
|
||||
|
|
@ -16,13 +25,15 @@
|
|||
</template>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div class="text-center">
|
||||
<div class="text-3xl font-bold text-blue-600 mb-2">18.2 months</div>
|
||||
<div class="text-sm text-gray-600 mb-3">Extended runway</div>
|
||||
<div class="text-3xl font-bold text-blue-600 mb-2">
|
||||
{{ sixMonthScenario.runway }} months
|
||||
</div>
|
||||
<div class="text-sm text-neutral-600 mb-3">Extended runway</div>
|
||||
<UProgress value="91" color="info" />
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="font-medium mb-3">Key Changes</h4>
|
||||
<ul class="text-sm text-gray-600 space-y-1">
|
||||
<ul class="text-sm text-neutral-600 space-y-1">
|
||||
<li>• Diversify revenue mix</li>
|
||||
<li>• Build 6-month savings buffer</li>
|
||||
<li>• Gradual capacity scaling</li>
|
||||
|
|
@ -59,8 +70,10 @@
|
|||
<h4 class="font-medium text-sm">Operate Current</h4>
|
||||
<UBadge color="success" variant="solid" size="xs">Active</UBadge>
|
||||
</div>
|
||||
<div class="text-2xl font-bold text-orange-600">2.8 months</div>
|
||||
<div class="text-xs text-gray-600">Baseline scenario</div>
|
||||
<div class="text-2xl font-bold text-orange-600">
|
||||
{{ currentScenario.runway }} months
|
||||
</div>
|
||||
<div class="text-xs text-neutral-600">Baseline scenario</div>
|
||||
<UButton size="xs" variant="ghost" @click="setScenario('current')">
|
||||
<UIcon name="i-heroicons-play" class="mr-1" />
|
||||
Continue
|
||||
|
|
@ -74,8 +87,10 @@
|
|||
<h4 class="font-medium text-sm">Quit Day Jobs</h4>
|
||||
<UBadge color="error" variant="subtle" size="xs">High Risk</UBadge>
|
||||
</div>
|
||||
<div class="text-2xl font-bold text-red-600">1.4 months</div>
|
||||
<div class="text-xs text-gray-600">Full-time co-op work</div>
|
||||
<div class="text-2xl font-bold text-red-600">
|
||||
{{ quitDayJobsScenario.runway }} months
|
||||
</div>
|
||||
<div class="text-xs text-neutral-600">Full-time co-op work</div>
|
||||
<UButton
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
|
|
@ -94,8 +109,10 @@
|
|||
>Medium Risk</UBadge
|
||||
>
|
||||
</div>
|
||||
<div class="text-2xl font-bold text-yellow-600">2.1 months</div>
|
||||
<div class="text-xs text-gray-600">Launch development</div>
|
||||
<div class="text-2xl font-bold text-yellow-600">
|
||||
{{ startProductionScenario.runway }} months
|
||||
</div>
|
||||
<div class="text-xs text-neutral-600">Launch development</div>
|
||||
<UButton
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
|
|
@ -112,8 +129,10 @@
|
|||
<h4 class="font-medium text-sm">6-Month Plan</h4>
|
||||
<UBadge color="info" variant="solid" size="xs">Planned</UBadge>
|
||||
</div>
|
||||
<div class="text-2xl font-bold text-blue-600">18.2 months</div>
|
||||
<div class="text-xs text-gray-600">Extended planning</div>
|
||||
<div class="text-2xl font-bold text-blue-600">
|
||||
{{ sixMonthScenario.runway }} months
|
||||
</div>
|
||||
<div class="text-xs text-neutral-600">Extended planning</div>
|
||||
<UButton size="xs" color="primary" @click="setScenario('sixMonth')">
|
||||
<UIcon name="i-heroicons-calendar" class="mr-1" />
|
||||
Plan
|
||||
|
|
@ -135,14 +154,14 @@
|
|||
<span class="text-sm">Savings Target Reached</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<UIcon name="i-heroicons-x-circle" class="text-red-500" />
|
||||
<span class="text-sm text-gray-600">€5,200 short</span>
|
||||
<span class="text-sm text-neutral-600">€5,200 short</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm">Cash Floor Maintained</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<UIcon name="i-heroicons-check-circle" class="text-green-500" />
|
||||
<span class="text-sm text-gray-600">Week 4+</span>
|
||||
<span class="text-sm text-neutral-600">Week 4+</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
|
|
@ -151,7 +170,9 @@
|
|||
<UIcon
|
||||
name="i-heroicons-exclamation-triangle"
|
||||
class="text-yellow-500" />
|
||||
<span class="text-sm text-gray-600">Top: 65%</span>
|
||||
<span class="text-sm text-neutral-600"
|
||||
>Top: {{ topSourcePct }}%</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -161,18 +182,22 @@
|
|||
<h4 class="font-medium mb-3">Key Dates</h4>
|
||||
<div class="space-y-3">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-sm text-gray-600">Savings gate clear:</span>
|
||||
<span class="text-sm font-medium">March 2024</span>
|
||||
<span class="text-sm text-neutral-600">Savings gate clear:</span>
|
||||
<span class="text-sm font-medium">{{
|
||||
keyDates.savingsGate || "Not projected"
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-sm text-gray-600">First cash breach:</span>
|
||||
<span class="text-sm font-medium text-red-600"
|
||||
>Week 7 (Feb 12)</span
|
||||
>
|
||||
<span class="text-sm text-neutral-600">First cash breach:</span>
|
||||
<span class="text-sm font-medium text-red-600">{{
|
||||
keyDates.firstBreach || "None projected"
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-sm text-gray-600">Deferred cap reset:</span>
|
||||
<span class="text-sm font-medium">April 1, 2024</span>
|
||||
<span class="text-sm text-neutral-600">Deferred cap reset:</span>
|
||||
<span class="text-sm font-medium">{{
|
||||
keyDates.deferredReset || "Not scheduled"
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -270,7 +295,7 @@
|
|||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="bg-gray-50 rounded-lg p-4">
|
||||
<div class="bg-neutral-50 rounded-lg p-4">
|
||||
<h4 class="font-medium text-sm mb-3">Impact on Runway</h4>
|
||||
<div class="text-center">
|
||||
<div
|
||||
|
|
@ -288,13 +313,13 @@
|
|||
|
||||
<div class="space-y-2">
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-gray-600">Monthly burn:</span>
|
||||
<span class="text-neutral-600">Monthly burn:</span>
|
||||
<span class="font-medium"
|
||||
>€{{ monthlyBurn.toLocaleString() }}</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-gray-600">Coverage ratio:</span>
|
||||
<span class="text-neutral-600">Coverage ratio:</span>
|
||||
<span class="font-medium"
|
||||
>{{ Math.round((paidHours / 400) * 100) }}%</span
|
||||
>
|
||||
|
|
@ -312,25 +337,176 @@ const route = useRoute();
|
|||
const router = useRouter();
|
||||
const scenariosStore = useScenariosStore();
|
||||
|
||||
const revenue = ref(12000);
|
||||
const paidHours = ref(320);
|
||||
const winRate = ref(70);
|
||||
// Restart wizard functionality
|
||||
const membersStore = useMembersStore();
|
||||
const policiesStore = usePoliciesStore();
|
||||
const streamsStore = useStreamsStore();
|
||||
const budgetStore = useBudgetStore();
|
||||
const cashStore = useCashStore();
|
||||
const sessionStore = useSessionStore();
|
||||
const wizardStore = useWizardStore();
|
||||
|
||||
// Calculate dynamic metrics
|
||||
const isResetting = ref(false);
|
||||
|
||||
// Get initial values from stores
|
||||
const initialRevenue = computed(() => streamsStore.totalMonthlyAmount || 0);
|
||||
const initialHours = computed(
|
||||
() => membersStore.capacityTotals.targetHours || 0
|
||||
);
|
||||
|
||||
const revenue = ref(initialRevenue.value || 0);
|
||||
const paidHours = ref(initialHours.value || 0);
|
||||
const winRate = ref(0);
|
||||
|
||||
// Watch for store changes and update sliders
|
||||
watch(initialRevenue, (newVal) => {
|
||||
if (newVal > 0) revenue.value = newVal;
|
||||
});
|
||||
watch(initialHours, (newVal) => {
|
||||
if (newVal > 0) paidHours.value = newVal;
|
||||
});
|
||||
|
||||
// Calculate dynamic metrics from real store data
|
||||
const monthlyBurn = computed(() => {
|
||||
const payroll = paidHours.value * 20 * 1.25; // €20/hr + 25% oncost
|
||||
const overhead = 1400;
|
||||
const production = 500;
|
||||
const hourlyWage = policiesStore.equalHourlyWage || 0;
|
||||
const oncostPct = policiesStore.payrollOncostPct || 0;
|
||||
const payroll = paidHours.value * hourlyWage * (1 + oncostPct / 100);
|
||||
const overhead =
|
||||
budgetStore.overheadCosts.reduce(
|
||||
(sum, cost) => sum + (cost.amount || 0),
|
||||
0
|
||||
) || 0;
|
||||
const production =
|
||||
budgetStore.productionCosts.reduce(
|
||||
(sum, cost) => sum + (cost.amount || 0),
|
||||
0
|
||||
) || 0;
|
||||
return payroll + overhead + production;
|
||||
});
|
||||
|
||||
const calculatedRunway = computed(() => {
|
||||
const totalCash = 13000; // cash + savings
|
||||
const totalCash = cashStore.currentCash + cashStore.currentSavings;
|
||||
const adjustedRevenue = revenue.value * (winRate.value / 100);
|
||||
const netPerMonth = adjustedRevenue - monthlyBurn.value;
|
||||
|
||||
if (netPerMonth >= 0) return 999; // Infinite/sustainable
|
||||
return Math.max(0, totalCash / Math.abs(netPerMonth));
|
||||
if (netPerMonth >= 0)
|
||||
return monthlyBurn.value > 0
|
||||
? Math.round((totalCash / monthlyBurn.value) * 100) / 100
|
||||
: 0;
|
||||
return Math.max(
|
||||
0,
|
||||
Math.round((totalCash / Math.abs(netPerMonth)) * 100) / 100
|
||||
);
|
||||
});
|
||||
|
||||
// Scenario calculations based on real data
|
||||
const totalCash = computed(
|
||||
() => cashStore.currentCash + cashStore.currentSavings
|
||||
);
|
||||
|
||||
const baseRunway = computed(() => {
|
||||
const baseBurn = monthlyBurn.value;
|
||||
return baseBurn > 0
|
||||
? Math.round((totalCash.value / baseBurn) * 100) / 100
|
||||
: 0;
|
||||
});
|
||||
|
||||
const currentScenario = computed(() => ({
|
||||
runway: baseRunway.value || 0,
|
||||
}));
|
||||
|
||||
const quitDayJobsScenario = computed(() => ({
|
||||
runway:
|
||||
monthlyBurn.value > 0
|
||||
? Math.max(
|
||||
0,
|
||||
Math.round((totalCash.value / (monthlyBurn.value * 1.8)) * 100) / 100
|
||||
)
|
||||
: 0, // Higher burn rate
|
||||
}));
|
||||
|
||||
const startProductionScenario = computed(() => ({
|
||||
runway:
|
||||
monthlyBurn.value > 0
|
||||
? Math.max(
|
||||
0,
|
||||
Math.round((totalCash.value / (monthlyBurn.value * 1.4)) * 100) / 100
|
||||
)
|
||||
: 0, // Medium higher burn
|
||||
}));
|
||||
|
||||
const sixMonthScenario = computed(() => ({
|
||||
runway:
|
||||
monthlyBurn.value > 0
|
||||
? Math.max(
|
||||
0,
|
||||
Math.round((totalCash.value / (monthlyBurn.value * 0.6)) * 100) / 100
|
||||
)
|
||||
: 0, // Lower burn with optimization
|
||||
}));
|
||||
|
||||
// Calculate concentration from real data
|
||||
const topSourcePct = computed(() => {
|
||||
if (streamsStore.streams.length === 0) return 0;
|
||||
const amounts = streamsStore.streams.map((s) => s.targetMonthlyAmount || 0);
|
||||
const total = amounts.reduce((sum, amt) => sum + amt, 0);
|
||||
return total > 0 ? Math.round((Math.max(...amounts) / total) * 100) : 0;
|
||||
});
|
||||
|
||||
// Calculate key dates from real data
|
||||
const keyDates = computed(() => {
|
||||
const currentDate = new Date();
|
||||
|
||||
// Calculate savings gate clear date based on current savings and target
|
||||
const savingsNeeded =
|
||||
(policiesStore.savingsTargetMonths || 0) * monthlyBurn.value;
|
||||
const currentSavings = cashStore.currentSavings;
|
||||
const monthlyNet = revenue.value - monthlyBurn.value;
|
||||
|
||||
let savingsGate = null;
|
||||
if (savingsNeeded > 0 && currentSavings < savingsNeeded && monthlyNet > 0) {
|
||||
const monthsToTarget = Math.ceil(
|
||||
(savingsNeeded - currentSavings) / monthlyNet
|
||||
);
|
||||
const targetDate = new Date(currentDate);
|
||||
targetDate.setMonth(targetDate.getMonth() + monthsToTarget);
|
||||
savingsGate = targetDate.toLocaleDateString("en-US", {
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
});
|
||||
}
|
||||
|
||||
// First cash breach from cash store projections
|
||||
const firstBreachWeek = cashStore.firstBreachWeek;
|
||||
let firstBreach = null;
|
||||
if (firstBreachWeek) {
|
||||
const breachDate = new Date(currentDate);
|
||||
breachDate.setDate(breachDate.getDate() + firstBreachWeek * 7);
|
||||
firstBreach = `Week ${firstBreachWeek} (${breachDate.toLocaleDateString(
|
||||
"en-US",
|
||||
{ month: "short", day: "numeric" }
|
||||
)})`;
|
||||
}
|
||||
|
||||
// Deferred cap reset - quarterly (every 3 months)
|
||||
let deferredReset = null;
|
||||
if (policiesStore.deferredCapHoursPerQtr > 0) {
|
||||
const nextQuarter = new Date(currentDate);
|
||||
const currentMonth = nextQuarter.getMonth();
|
||||
const quarterStartMonth = Math.floor(currentMonth / 3) * 3;
|
||||
nextQuarter.setMonth(quarterStartMonth + 3, 1);
|
||||
deferredReset = nextQuarter.toLocaleDateString("en-US", {
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
savingsGate,
|
||||
firstBreach,
|
||||
deferredReset,
|
||||
};
|
||||
});
|
||||
|
||||
function getRunwayColor(months: number) {
|
||||
|
|
@ -353,9 +529,41 @@ function setScenario(scenario: string) {
|
|||
}
|
||||
|
||||
function resetSliders() {
|
||||
revenue.value = 12000;
|
||||
paidHours.value = 320;
|
||||
winRate.value = 70;
|
||||
revenue.value = initialRevenue.value || 0;
|
||||
paidHours.value = initialHours.value || 0;
|
||||
winRate.value = 0;
|
||||
}
|
||||
|
||||
async function restartWizard() {
|
||||
isResetting.value = true;
|
||||
|
||||
// Clear all localStorage persistence
|
||||
if (typeof localStorage !== "undefined") {
|
||||
localStorage.removeItem("urgent-tools-members");
|
||||
localStorage.removeItem("urgent-tools-policies");
|
||||
localStorage.removeItem("urgent-tools-streams");
|
||||
localStorage.removeItem("urgent-tools-budget");
|
||||
localStorage.removeItem("urgent-tools-cash");
|
||||
localStorage.removeItem("urgent-tools-session");
|
||||
localStorage.removeItem("urgent-tools-scenarios");
|
||||
}
|
||||
|
||||
// Reset all stores
|
||||
membersStore.resetMembers();
|
||||
policiesStore.resetPolicies();
|
||||
streamsStore.resetStreams();
|
||||
budgetStore.resetBudgetOverhead();
|
||||
sessionStore.resetSession();
|
||||
|
||||
// Reset wizard state
|
||||
wizardStore.reset();
|
||||
|
||||
// Small delay for UX
|
||||
await new Promise((resolve) => setTimeout(resolve, 300));
|
||||
isResetting.value = false;
|
||||
|
||||
// Navigate to wizard
|
||||
await navigateTo("/wizard");
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue